Files
inji-wallet/shared/CloudBackupAndRestoreUtils.ts
abhip2565 47a97c1783 [INJIMOB-1874] add ecr1,eck1 and ed key support (#1594)
* [INJIMOB-1874] add ecr1,eck1 and ed key support

Signed-off-by: Abhishek Paul <paul.apaul.abhishek.ap@gmail.com>

* [INJIMOB-1874] refactor changes

Signed-off-by: Abhishek Paul <paul.apaul.abhishek.ap@gmail.com>

[INJIMOB-1874] refactor changes

Signed-off-by: Abhishek Paul <paul.apaul.abhishek.ap@gmail.com>

---------

Signed-off-by: Abhishek Paul <paul.apaul.abhishek.ap@gmail.com>
2024-09-10 14:18:52 +05:30

503 lines
15 KiB
TypeScript

import {
GoogleSignin,
statusCodes,
} from '@react-native-google-signin/google-signin';
import {CloudStorage, CloudStorageScope} from 'react-native-cloud-storage';
import {GOOGLE_ANDROID_CLIENT_ID} from 'react-native-dotenv';
import {readFile, writeFile} from 'react-native-fs';
import {BackupDetails} from '../types/backup-and-restore/backup';
import {appleAuth} from '@invertase/react-native-apple-authentication';
import {bytesToMB, sleep} from './commonUtil';
import {
IOS_SIGNIN_FAILED,
isAndroid,
isIOS,
NETWORK_REQUEST_FAILED,
} from './constants';
import fileStorage, {backupDirectoryPath, zipFilePath} from './fileStorage';
import {API} from './api';
import {NativeModules} from 'react-native';
class Cloud {
static status = {
DECLINED: 'DECLINED',
SUCCESS: 'SUCCESS',
FAILURE: 'FAILURE',
};
static readonly timeout = 10000;
private static readonly requiredScopes = [
'https://www.googleapis.com/auth/drive.appdata',
'https://www.googleapis.com/auth/drive.file',
];
private static readonly BACKUP_FILE_REG_EXP = /backup_[0-9]*.zip$/g;
private static readonly ALL_BACKUP_FILE_REG_EXP = /backup_[0-9]*.zip/g;
private static readonly UNSYNCED_BACKUP_FILE_REG_EXP =
/backup_[0-9]*.zip.icloud/g;
private static readonly RETRY_SLEEP_TIME = 5000;
static readonly NO_BACKUP_FILE = 'Backup files not available';
private static configure() {
GoogleSignin.configure({
scopes: this.requiredScopes,
androidClientId: GOOGLE_ANDROID_CLIENT_ID,
});
}
private static async profileInfo(): Promise<ProfileInfo | undefined> {
try {
const accessToken = await this.getAccessToken();
const profileResponse = await API.getGoogleAccountProfileInfo(
accessToken,
);
return {
email: profileResponse.email,
picture: profileResponse.picture,
};
} catch (error) {
console.error('Error while getting profile info ', error);
throw error;
}
}
private static getBackupFilesList = async () => {
return await CloudStorage.readdir(`/`, CloudStorageScope.AppData);
};
private static getLatestFileName = (allFiles: string[]): string => {
const sortedFiles = allFiles.sort((a, b) => {
const dateA = new Date(Number(a.split('.')[0].split('_')[1]));
const dateB = new Date(Number(b.split('.')[0].split('_')[1]));
return dateB > dateA ? 1 : dateB < dateA ? -1 : 0;
});
return `/${sortedFiles[0]}`;
};
/**
* This method is specific to iOS for downloading files from iCloud, which can be processed after
* Ref - https://react-native-cloud-storage.vercel.app/docs/api/CloudStorage#downloadfilepath-scope
*/
private static async syncBackupFiles() {
const isSyncDone = await this.downloadUnSyncedBackupFiles();
if (isSyncDone) return;
await sleep(this.RETRY_SLEEP_TIME);
await this.syncBackupFiles();
}
/**
* TODO: Remove the test call(get profile info) to reduce extra api calls
*/
static async getAccessToken() {
try {
const tokenResult = await GoogleSignin.getTokens();
try {
await API.getGoogleAccountProfileInfo(tokenResult.accessToken);
return tokenResult.accessToken;
} catch (error) {
console.error('Error while using the current token ', error);
if (
error.toString().includes('401') &&
error.toString().includes('UNAUTHENTICATED')
) {
await GoogleSignin.revokeAccess();
await GoogleSignin.signOut();
} else if (
error.toString().includes('401') ||
error.toString().includes('Unauthorized')
) {
return await refreshToken(tokenResult);
} else if (this.isNetworkError(error)) {
throw new Error(NETWORK_REQUEST_FAILED);
}
throw error;
}
} catch (error) {
console.error('Error while getting access token ', error);
throw error;
}
async function refreshToken(tokenResult: {
idToken: string;
accessToken: string;
}) {
await GoogleSignin.clearCachedAccessToken(tokenResult.accessToken);
await GoogleSignin.signInSilently();
const {accessToken} = await GoogleSignin.getTokens();
return accessToken;
}
}
static async signIn(): Promise<SignInResult | IsIOSResult> {
const {RNSecureKeystoreModule} = NativeModules;
if (isIOS()) {
let profileInfo;
// start a login request
try {
const appleAuthRequestResponse = await appleAuth.performRequest({
requestedOperation: appleAuth.Operation.LOGIN,
requestedScopes: [appleAuth.Scope.FULL_NAME, appleAuth.Scope.EMAIL],
});
const {email, nonce, identityToken, realUserStatus /* etc */} =
appleAuthRequestResponse;
profileInfo = {email: email, picture: null};
await RNSecureKeystoreModule.storeData(
'userIdentifier',
JSON.stringify(appleAuthRequestResponse),
);
return {status: this.status.SUCCESS, profileInfo: profileInfo};
} catch (error) {
if (error.code === appleAuth.Error.CANCELED) {
console.warn('User canceled Apple Sign in.');
return {
status: this.status.DECLINED,
error,
};
} else {
console.error(error);
return {
status: this.status.FAILURE,
error,
};
}
}
}
this.configure();
try {
const {scopes: userProvidedScopes} = await GoogleSignin.signIn();
const userNotProvidedRequiredAccessInConsent = !this.requiredScopes.every(
requiredScope => userProvidedScopes?.includes(requiredScope),
);
if (userNotProvidedRequiredAccessInConsent) {
return {
status: this.status.DECLINED,
};
}
const profileInfo = await this.profileInfo();
return {
status: this.status.SUCCESS,
profileInfo,
};
} catch (error) {
console.error('Cloud sign in failed due to ', error);
//User cancelled confirmation / manually closed sign in WebView
if (error.code === statusCodes.SIGN_IN_CANCELLED) {
return {
status: this.status.DECLINED,
error,
};
} else if (this.isNetworkError(error)) {
error = NETWORK_REQUEST_FAILED;
}
return {
status: this.status.FAILURE,
error,
};
}
}
static async isSignedInAlready(): Promise<isSignedInResult> {
const {RNSecureKeystoreModule} = NativeModules;
try {
if (isIOS()) {
const isSignedIn = await CloudStorage.isCloudAvailable();
const userIdentifier = await RNSecureKeystoreModule.getData(
'userIdentifier',
)[0];
const userToken = JSON.parse(userIdentifier + '');
const user = userToken.user;
const email = userToken.email;
const credentialState = await appleAuth.getCredentialStateForUser(user);
const profileInfo = {email: email, picture: undefined};
if (
credentialState === appleAuth.State.AUTHORIZED &&
isSignedIn === true
) {
return {
isSignedIn: true,
isAuthorised: true,
profileInfo: profileInfo,
};
} else {
return {
isSignedIn: false,
isAuthorised: true,
profileInfo: profileInfo,
};
}
}
this.configure();
const isSignedIn = await GoogleSignin.isSignedIn();
if (!isSignedIn) {
await GoogleSignin.signInSilently();
const profileInfo = await this.profileInfo();
return {
isSignedIn: true,
profileInfo,
isAuthorised: true,
};
} else {
const profileInfo = await this.profileInfo();
return {
isSignedIn: true,
profileInfo,
isAuthorised: true,
};
}
} catch (error) {
console.error(
'Error while checking if user is already signed in ',
error,
);
let errorReason: null | string = null;
if (this.isNetworkError(error)) errorReason = NETWORK_REQUEST_FAILED;
//TODO: resolve and reject promise so it can be handled in onError
return {
error: errorReason || error,
isSignedIn: false,
isAuthorised: false,
};
}
}
static async downloadUnSyncedBackupFiles(): Promise<Boolean> {
if (isIOS()) {
const unSyncedFiles = (await this.getBackupFilesList()).filter(file =>
file.match(this.UNSYNCED_BACKUP_FILE_REG_EXP),
);
if (unSyncedFiles.length > 0) {
await CloudStorage.downloadFile(`/${unSyncedFiles[0]}`);
return false;
}
}
return true;
}
static async lastBackupDetails(
cloudFileName?: string | undefined,
): Promise<BackupDetails> {
CloudStorage.setTimeout(this.timeout);
if (isAndroid()) {
const tokenResult = await Cloud.getAccessToken();
CloudStorage.setGoogleDriveAccessToken(tokenResult);
}
if (isIOS()) {
const isCloudAvailable = await CloudStorage.isCloudAvailable();
if (!isCloudAvailable) {
return Promise.reject({
status: this.status.FAILURE,
error: IOS_SIGNIN_FAILED,
});
}
await this.syncBackupFiles();
}
if (!cloudFileName) {
const availableBackupFilesInCloud = (
await this.getBackupFilesList()
).filter(file => file.match(this.BACKUP_FILE_REG_EXP));
if (availableBackupFilesInCloud.length === 0) {
throw new Error(this.NO_BACKUP_FILE);
}
cloudFileName = this.getLatestFileName(availableBackupFilesInCloud);
}
const {birthtimeMs: creationTime, size} = await CloudStorage.stat(
cloudFileName,
CloudStorageScope.AppData,
);
return {
backupCreationTime: creationTime,
backupFileSize: bytesToMB(size),
};
}
static async removeOldDriveBackupFiles(fileName: string) {
const toBeRemovedFiles = (await this.getBackupFilesList())
.filter(file => file !== fileName)
.filter(file => file.match(this.ALL_BACKUP_FILE_REG_EXP));
for (const oldFileName of toBeRemovedFiles) {
await CloudStorage.unlink(`/${oldFileName}`);
}
}
static async uploadBackupFileToDrive(
fileName: string,
retryCounter: number,
error: string | undefined = undefined,
): Promise<CloudUploadResult> {
if (retryCounter < 0 || error === NETWORK_REQUEST_FAILED) {
return Promise.reject({
status: this.status.FAILURE,
error: error || 'Retry limit reached',
});
}
const cloudFileName = `/${fileName}.zip`;
let uploadError: string | undefined = undefined;
try {
CloudStorage.setTimeout(this.timeout);
if (isAndroid()) {
const tokenResult = await Cloud.getAccessToken();
CloudStorage.setGoogleDriveAccessToken(tokenResult);
}
if (isIOS()) {
const isCloudAvailable = await CloudStorage.isCloudAvailable();
if (!isCloudAvailable) {
return Promise.reject({
status: this.status.FAILURE,
error: IOS_SIGNIN_FAILED,
});
}
}
const filePath = zipFilePath(fileName);
const fileContent = await readFile(filePath, 'base64');
await CloudStorage.writeFile(
cloudFileName,
fileContent,
CloudStorageScope.AppData,
);
const isFileUploaded = await CloudStorage.exists(
cloudFileName,
CloudStorageScope.AppData,
);
if (isFileUploaded) {
const backupDetails = await this.lastBackupDetails(cloudFileName);
await this.removeOldDriveBackupFiles(`${fileName}.zip`);
return Promise.resolve({
status: this.status.SUCCESS,
error: null,
backupDetails,
});
}
} catch (error) {
console.error(
`Error occurred while cloud upload.. retrying ${retryCounter} : Error : ${error}`,
);
if (this.isNetworkError(error)) {
uploadError = NETWORK_REQUEST_FAILED;
} else {
uploadError = error;
}
}
return this.uploadBackupFileToDrive(
fileName,
retryCounter - 1,
uploadError,
);
}
static async downloadLatestBackup(): Promise<string | null> {
try {
CloudStorage.setTimeout(this.timeout);
if (isAndroid()) {
const tokenResult = await Cloud.getAccessToken();
CloudStorage.setGoogleDriveAccessToken(tokenResult);
}
if (isIOS()) {
const isCloudAvailable = await CloudStorage.isCloudAvailable();
if (!isCloudAvailable) {
return Promise.reject({
status: this.status.FAILURE,
error: IOS_SIGNIN_FAILED,
});
}
await this.syncBackupFiles();
}
// TODO: do basic sanity about this .zip file
const availableBackupFilesInCloud = (
await this.getBackupFilesList()
).filter(file => file.match(this.BACKUP_FILE_REG_EXP));
if (availableBackupFilesInCloud.length === 0) {
throw new Error(Cloud.NO_BACKUP_FILE);
}
const fileName = `/${this.getLatestFileName(
availableBackupFilesInCloud,
)}`;
const fileContent = await CloudStorage.readFile(
fileName,
CloudStorageScope.AppData,
);
if (fileContent.length === 0) return Promise.resolve(null);
// write the file content in the backup directory path, create backup directory if not exists
const isDirectoryExists = await fileStorage.exists(backupDirectoryPath);
if (!isDirectoryExists) {
await fileStorage.createDirectory(backupDirectoryPath);
}
await writeFile(
backupDirectoryPath + '/' + fileName,
fileContent,
'base64',
);
// return the path
return Promise.resolve(fileName.split('.zip')[0]);
} catch (error) {
console.error('error while downloading backup file ', error);
let downloadError;
if (this.isNetworkError(error)) {
downloadError = NETWORK_REQUEST_FAILED;
} else if (
error.code?.toString() === 'ERR_DIRECTORY_NOT_FOUND' ||
error.toString().includes(this.NO_BACKUP_FILE)
) {
downloadError = this.NO_BACKUP_FILE;
} else {
downloadError = error;
}
return Promise.reject({error: downloadError});
}
}
private static isNetworkError(error: Error): boolean {
if (
error.toString().includes('NETWORK_ERROR') ||
error.toString().includes('Network Error') ||
error.toString().includes('NetworkError') ||
error.toString().includes(NETWORK_REQUEST_FAILED)
)
return true;
return false;
}
}
export default Cloud;
export type ProfileInfo = {
email: string;
picture: string | undefined;
};
export type SignInResult = {
status: (typeof Cloud.status)[keyof typeof Cloud.status];
profileInfo?: ProfileInfo;
error?: string;
};
export type IsIOSResult = {
isIOS?: boolean;
};
export type isSignedInResult = {
isSignedIn: boolean;
error?: string | null;
profileInfo?: ProfileInfo;
isAuthorised?: boolean;
};
export type CloudUploadResult = {
status: (typeof Cloud.status)[keyof typeof Cloud.status];
error: string | null;
backupDetails?: BackupDetails;
};