diff --git a/packages/mobile-aelf/app.json b/packages/mobile-aelf/app.json index 450db7b60..e4a3342c2 100644 --- a/packages/mobile-aelf/app.json +++ b/packages/mobile-aelf/app.json @@ -8,6 +8,11 @@ "ios": { "googleServicesFile": "./GoogleService-Info.plist" }, - "plugins": ["@react-native-firebase/app", "@react-native-firebase/perf", "@react-native-firebase/crashlytics"] + "plugins": [ + "@react-native-firebase/app", + "@react-native-firebase/perf", + "@react-native-firebase/crashlytics", + "react-native-cloud-storage" + ] } } diff --git a/packages/mobile-aelf/js/pages/Home/HomeTab/index.tsx b/packages/mobile-aelf/js/pages/Home/HomeTab/index.tsx index d29f238aa..e20545a37 100644 --- a/packages/mobile-aelf/js/pages/Home/HomeTab/index.tsx +++ b/packages/mobile-aelf/js/pages/Home/HomeTab/index.tsx @@ -186,6 +186,12 @@ const HomeTab: React.FC = ({ _ }) => { style={{ marginTop: 40 }}> Manual Backup + navigationService.push('CloudBackup')} style={{ marginTop: 40 }}> + CloudBackup + + navigationService.push('CloudBackupDev')} style={{ marginTop: 10 }}> + CloudBackupDev + navigationService.push('Referral')} style={{ marginTop: 20 }}> Referral diff --git a/packages/mobile-aelf/js/pages/Login/CloudBackup/cases.tsx b/packages/mobile-aelf/js/pages/Login/CloudBackup/cases.tsx new file mode 100644 index 000000000..68d08dc2e --- /dev/null +++ b/packages/mobile-aelf/js/pages/Login/CloudBackup/cases.tsx @@ -0,0 +1,308 @@ +// https://github.com/kuatsu/react-native-cloud-storage/blob/master/example/src/views/Home.tsx +import React, { useCallback, useEffect, useState } from 'react'; +import { Text, View } from 'react-native'; +import PageContainer from 'components/PageContainer'; +import { isIOS } from '@portkey-wallet/utils/mobile/device'; +import aes from '@portkey-wallet/utils/aes'; +import { makeStyles } from '@rneui/themed'; +import { pTd } from 'utils/unit'; +import fonts from 'assets/theme/fonts'; +import Svg from 'components/Svg'; +import Touchable from 'components/Touchable'; +import CommonButton from 'components/CommonButton'; +import GStyles from 'mobile-did/js/assets/theme/GStyles'; +import CommonInput from 'mobile-did/js/components/CommonInput'; +import CheckBox from 'components/CheckBox'; +import { OfficialWebsite } from '@portkey-wallet/constants/constants-ca/network'; +import navigationService from 'utils/navigationService'; +import { useCurrentWallet } from '@portkey-wallet/hooks/hooks-eoa/wallet'; +import { useCredentials } from 'hooks/store'; +import { useCloudStorage } from './useCloudStorage'; +import CommonToast from 'components/CommonToast'; +import { getPasswords, passwordShowFormat } from './index'; + +export default function CloudBackupCases() { + const styles = getStyles(); + // const { theme } = useTheme(); + // const [copied, setCopied] = useState(false); + // + // // const currentAccount = useCurrentAccount(); + const currentWallet = useCurrentWallet(); + const credentials = useCredentials(); + const [password, setPassword] = useState(''); + const [passwordShow, setPasswordShow] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [confirmPasswordShow, setConfirmPasswordShow] = useState(''); + const [secureTextEntry, setSecureTextEntry] = useState(true); + const [confirmSecureTextEntry, setConfirmSecureTextEntry] = useState(true); + const [errorMessage, setErrorMessage] = useState(''); + + const [isChecked, setIsChecked] = useState(false); + const onClickCheckBox = useCallback(() => { + setIsChecked(!isChecked); + }, [isChecked]); + + useEffect(() => { + console.log('cloud backup - currentWallet: ', currentWallet, credentials); + }, [credentials, currentWallet]); + + const { + // cloudStorage, + readFile, + handleCreateDirectory, + handleDeleteDirectory, + handleListContents, + handleCreateFile, + loading, + } = useCloudStorage(); + + return ( + + + + handleCreateDirectory + + { + if (!currentWallet) { + CommonToast.fail('Can not found wallet'); + return; + } + readFile(currentWallet.key); + }} + style={{ marginTop: 10 }}> + readFile + + handleDeleteDirectory(true)} + style={{ marginTop: 10 }}> + handleDeleteDirectory + + + handleListContents/Wallet List in Cloud + + { + if (!currentWallet) { + CommonToast.fail('Can not found wallet'); + return; + } + handleCreateFile({ + filename: currentWallet.key, + // TODO, encrypt before create + input: JSON.stringify(currentWallet), + }); + }} + style={{ marginTop: 10 }}> + handleCreateFile + + + Create password + + This password will secure your seed phrase in the cloud. We cannot reset it if you lose it, so please keep it + safe. + + + { + const { newPassword, passwordShow: _passwordShow } = getPasswords(value, password, secureTextEntry); + setPassword(newPassword); + setPasswordShow(_passwordShow); + if (confirmPassword !== newPassword) { + setErrorMessage('Not match, please try again.'); + } else { + setErrorMessage(''); + } + }} + rightIcon={ + + { + setPassword(''); + setPasswordShow(''); + }}> + + + { + const newSecureTextEntry = !secureTextEntry; + setSecureTextEntry(newSecureTextEntry); + setPasswordShow(passwordShowFormat(newSecureTextEntry, password)); + }}> + + + + } + /> + { + const { newPassword, passwordShow: _passwordShow } = getPasswords( + value, + confirmPassword, + confirmSecureTextEntry, + ); + setConfirmPassword(newPassword); + setConfirmPasswordShow(_passwordShow); + if (password !== newPassword) { + setErrorMessage('Not match, please try again.'); + } else { + setErrorMessage(''); + } + }} + rightIcon={ + + { + setConfirmPassword(''); + setConfirmPasswordShow(''); + }}> + + + { + const newSecureTextEntry = !confirmSecureTextEntry; + setConfirmSecureTextEntry(newSecureTextEntry); + setConfirmPasswordShow(passwordShowFormat(newSecureTextEntry, confirmPassword)); + }}> + + + + } + /> + + + + + + + onClickCheckBox()} boxStyle={styles.checkBox} /> + + + I understand that if I lose my password, I will not be able to access my backup, which could result in the + loss of all my assets. I agree to{' '} + { + navigationService.navigate('ViewOnWebView', { + title: 'Terms of Service', + url: `${OfficialWebsite}/terms-of-service`, + }); + }}> + terms + {' '} + and{' '} + { + navigationService.navigate('ViewOnWebView', { + title: 'Privacy Policy', + url: `${OfficialWebsite}/privacy-policy`, + }); + }}> + privacy policy + {' '} + for using aelf wallet. + + + { + // TODO: + // if directory exists, will not create again. + await handleCreateDirectory(); + if (!currentWallet) { + CommonToast.fail('Can not found wallet'); + return; + } + await handleCreateFile({ + filename: currentWallet.key, + // TODO, encrypt before create + input: aes.encrypt(JSON.stringify(currentWallet), password), + }); + }}> + Continue + + + + ); +} + +const getStyles = makeStyles(theme => ({ + containerStyles: { + backgroundColor: theme.colors.bgBase1, + justifyContent: 'space-between', + }, + title: { + marginTop: pTd(24), + fontSize: pTd(32), + ...fonts.BGMediumFont, + }, + desc: { + color: theme.colors.textBase2, + marginTop: pTd(16), + fontSize: pTd(14), + }, + inputContainer: { + marginTop: pTd(24), + flexDirection: 'row', + flexWrap: 'wrap', + }, + inputLabel: { + paddingLeft: 0, + }, + understandContainer: { + flexDirection: 'row', + alignItems: 'center', + }, + checkBox: { + backgroundColor: theme.colors.bgBase1, + marginRight: pTd(12), + }, + understandText: { + flex: 1, + fontSize: pTd(14), + lineHeight: pTd(14) * 1.4, + }, + link: { + fontSize: pTd(14), + color: theme.colors.textBrand3, + }, + continueButton: { + marginTop: pTd(24), + marginBottom: pTd(16), + }, +})); diff --git a/packages/mobile-aelf/js/pages/Login/CloudBackup/index.tsx b/packages/mobile-aelf/js/pages/Login/CloudBackup/index.tsx new file mode 100644 index 000000000..35b962b40 --- /dev/null +++ b/packages/mobile-aelf/js/pages/Login/CloudBackup/index.tsx @@ -0,0 +1,309 @@ +// https://github.com/kuatsu/react-native-cloud-storage/blob/master/example/src/views/Home.tsx +import React, { useCallback, useState } from 'react'; +import { Text, View } from 'react-native'; +import PageContainer from 'components/PageContainer'; +import { isIOS } from '@portkey-wallet/utils/mobile/device'; +import aes from '@portkey-wallet/utils/aes'; +import { makeStyles } from '@rneui/themed'; +import { pTd } from 'utils/unit'; +import fonts from 'assets/theme/fonts'; +import Svg from 'components/Svg'; +import Touchable from 'components/Touchable'; +import CommonButton from 'components/CommonButton'; +import GStyles from 'mobile-did/js/assets/theme/GStyles'; +import CommonInput from 'mobile-did/js/components/CommonInput'; +import CheckBox from 'components/CheckBox'; +import { OfficialWebsite } from '@portkey-wallet/constants/constants-ca/network'; +import navigationService from 'utils/navigationService'; +import { useCurrentWallet } from '@portkey-wallet/hooks/hooks-eoa/wallet'; +// import { useCredentials } from 'hooks/store'; +import { useCloudStorage } from './useCloudStorage'; +import CommonToast from 'components/CommonToast'; +import { useCredentials } from 'hooks/store'; +import { TWalletInfo } from '@portkey-wallet/types/types-eoa/wallet'; + +function generateDots(length: number) { + if (length < 0) { + throw new Error('Length must be a non-negative number'); + } + return '•'.repeat(length); +} + +export function passwordShowFormat(isSecure: boolean, password: string) { + return isSecure ? generateDots(password.length) : password; +} + +export function getPasswords(newValue: string, prePassword: string, isSecure: boolean) { + const _password = newValue.trim(); + const _passwordLength = _password.length; + const prePasswordLength = prePassword.length; + // let newPassword = _password ? prePassword.slice(0, _passwordLength - 1) + _password[_passwordLength - 1] : ''; + let newPassword = prePassword.slice(0, prePasswordLength - 1); + if (!_password) { + newPassword = ''; + } else if (prePasswordLength < _passwordLength) { + newPassword = prePassword.slice(0, _passwordLength - 1) + _password[_passwordLength - 1]; + } + return { + newPassword, + passwordShow: passwordShowFormat(isSecure, newPassword), + }; +} + +export default function CloudBackup() { + const styles = getStyles(); + // const { theme } = useTheme(); + // const [copied, setCopied] = useState(false); + // + // // const currentAccount = useCurrentAccount(); + const currentWallet = useCurrentWallet(); + const credentials = useCredentials(); + const [password, setPassword] = useState(''); + const [passwordShow, setPasswordShow] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [confirmPasswordShow, setConfirmPasswordShow] = useState(''); + const [secureTextEntry, setSecureTextEntry] = useState(true); + const [confirmSecureTextEntry, setConfirmSecureTextEntry] = useState(true); + const [errorMessage, setErrorMessage] = useState(''); + + const [isChecked, setIsChecked] = useState(false); + const onClickCheckBox = useCallback(() => { + setIsChecked(!isChecked); + }, [isChecked]); + + const { handleCreateDirectory, handleCreateFile, loading } = useCloudStorage(); + + return ( + + + Create password + + This password will secure your seed phrase in the cloud. We cannot reset it if you lose it, so please keep it + safe. + + + { + const { newPassword, passwordShow: _passwordShow } = getPasswords(value, password, secureTextEntry); + setPassword(newPassword); + setPasswordShow(_passwordShow); + if (confirmPassword !== newPassword) { + setErrorMessage('Not match, please try again.'); + } else { + setErrorMessage(''); + } + }} + rightIcon={ + + { + setPassword(''); + setPasswordShow(''); + }}> + + + { + const newSecureTextEntry = !secureTextEntry; + setSecureTextEntry(newSecureTextEntry); + setPasswordShow(passwordShowFormat(newSecureTextEntry, password)); + }}> + + + + } + /> + { + const { newPassword, passwordShow: _passwordShow } = getPasswords( + value, + confirmPassword, + confirmSecureTextEntry, + ); + setConfirmPassword(newPassword); + setConfirmPasswordShow(_passwordShow); + if (password !== newPassword) { + setErrorMessage('Not match, please try again.'); + } else { + setErrorMessage(''); + } + }} + rightIcon={ + + { + setConfirmPassword(''); + setConfirmPasswordShow(''); + }}> + + + { + const newSecureTextEntry = !confirmSecureTextEntry; + setConfirmSecureTextEntry(newSecureTextEntry); + setConfirmPasswordShow(passwordShowFormat(newSecureTextEntry, confirmPassword)); + }}> + + + + } + /> + + + + + + + onClickCheckBox()} boxStyle={styles.checkBox} /> + + + I understand that if I lose my password, I will not be able to access my backup, which could result in the + loss of all my assets. I agree to{' '} + { + navigationService.navigate('ViewOnWebView', { + title: 'Terms of Service', + url: `${OfficialWebsite}/terms-of-service`, + }); + }}> + terms + {' '} + and{' '} + { + navigationService.navigate('ViewOnWebView', { + title: 'Privacy Policy', + url: `${OfficialWebsite}/privacy-policy`, + }); + }}> + privacy policy + {' '} + for using aelf wallet. + + + { + console.log('currentWallet: ', currentWallet); + if (!currentWallet) { + CommonToast.fail('Can not found wallet'); + return; + } + const _currentWallet: TWalletInfo = JSON.parse(JSON.stringify(currentWallet)); + if (!credentials?.pin) { + // almost impossible + CommonToast.fail('Wallet is locked'); + return; + } + const { pin } = credentials; + if (_currentWallet.AESEncryptMnemonic) { + const result = aes.decrypt(_currentWallet.AESEncryptMnemonic, pin); + if (!result) { + CommonToast.fail('Decrypt failed'); + } + _currentWallet.AESEncryptMnemonic = aes.encrypt(result as string, password); + } + _currentWallet.accountList.map(account => { + const result = aes.decrypt(account.AESEncryptPrivateKey, pin); + if (!result) { + CommonToast.fail('Decrypt failed'); + } + account.AESEncryptPrivateKey = aes.encrypt(result as string, password); + }); + console.log('_currentWallet: ', _currentWallet, currentWallet); + // if directory exists, will not create again. + await handleCreateDirectory(); + + await handleCreateFile({ + filename: _currentWallet.key, + input: JSON.stringify({ + updateTime: Date.now(), + wallet: aes.encrypt(JSON.stringify(_currentWallet), password), + }), + }); + navigationService.navigate('Home'); + CommonToast.success('Backup completed'); + }}> + Continue + + + + ); +} + +const getStyles = makeStyles(theme => ({ + containerStyles: { + backgroundColor: theme.colors.bgBase1, + justifyContent: 'space-between', + }, + title: { + marginTop: pTd(24), + fontSize: pTd(32), + ...fonts.BGMediumFont, + }, + desc: { + color: theme.colors.textBase2, + marginTop: pTd(16), + fontSize: pTd(14), + }, + inputContainer: { + marginTop: pTd(24), + flexDirection: 'row', + flexWrap: 'wrap', + }, + inputLabel: { + paddingLeft: 0, + }, + understandContainer: { + flexDirection: 'row', + alignItems: 'center', + }, + checkBox: { + backgroundColor: theme.colors.bgBase1, + marginRight: pTd(12), + }, + understandText: { + flex: 1, + fontSize: pTd(14), + lineHeight: pTd(14) * 1.4, + }, + link: { + fontSize: pTd(14), + color: theme.colors.textBrand3, + }, + continueButton: { + marginTop: pTd(24), + marginBottom: pTd(16), + }, +})); diff --git a/packages/mobile-aelf/js/pages/Login/CloudBackup/useCloudStorage.tsx b/packages/mobile-aelf/js/pages/Login/CloudBackup/useCloudStorage.tsx new file mode 100644 index 000000000..ff743388e --- /dev/null +++ b/packages/mobile-aelf/js/pages/Login/CloudBackup/useCloudStorage.tsx @@ -0,0 +1,191 @@ +import { useState, useEffect, useMemo, useCallback } from 'react'; +import { + CloudStorage, + CloudStorageError, + CloudStorageErrorCode, + // type CloudStorageFileStat, + CloudStorageProvider, + // CloudStorageScope, + useIsCloudAvailable, +} from 'react-native-cloud-storage'; +import CommonToast from '../../../components/CommonToast'; + +function commonCloudStorageError(e: any) { + console.warn('commonCloudStorageError', e); + if (e instanceof CloudStorageError) { + if (e.code === CloudStorageErrorCode.READ_ERROR) { + CommonToast.fail('No backup found'); + } else { + CommonToast.fail(e.code); + } + } else { + CommonToast.fail('Something went wrong. Please try backing up later.'); + } +} + +export const useCloudStorage = () => { + // const [provider, setProvider] = useState(CloudStorage.getDefaultProvider()); + const [provider] = useState(CloudStorage.getDefaultProvider()); + // const [scope, setScope] = useState(CloudStorageScope.AppData); + // const [parentDirectory, setParentDirectory] = useState('/portkey-eoa/wallet'); + const [parentDirectory] = useState('/portkey-eoa/wallet'); + const [isParentDirectoryExist, setIsParentDirectoryExist] = useState(); + // const [filename, setFilename] = useState('test.txt'); + // const [stats, setStats] = useState(null); + // const [input, setInput] = useState(''); + // const [appendInput, setAppendInput] = useState(''); + // const [accessToken, setAccessToken] = useState(''); + const [loading, setLoading] = useState(false); + + const cloudStorage = useMemo(() => { + return new CloudStorage( + provider, + provider === CloudStorageProvider.GoogleDrive ? { strictFilenames: true } : undefined, + ); + }, [provider]); + const cloudAvailable = useIsCloudAvailable(cloudStorage); + + const isDirectoryExists = useCallback(async () => { + setLoading(true); + try { + const exists = await cloudStorage.exists(parentDirectory); + console.log('useCloudStorage - isDirectoryExists: ', exists); + setIsParentDirectoryExist(exists); + } catch (e) { + // console.warn(e); + setIsParentDirectoryExist(false); + commonCloudStorageError(e); + } finally { + setLoading(false); + } + }, [cloudStorage, parentDirectory]); + + const readFile = useCallback( + // eslint-disable-next-line @typescript-eslint/no-shadow + async (filename: string) => { + setLoading(true); + try { + const newStats = await cloudStorage.stat(parentDirectory + '/' + filename); + // setStats(newStats); + console.log('File stats', newStats); + if (newStats.isDirectory()) { + return; + } + const fileContent = await cloudStorage.readFile(parentDirectory + '/' + filename); + console.log('File content', fileContent); + return fileContent; + } catch (e) { + // console.log('readFile: ', e); + commonCloudStorageError(e); + return false; + // if (e instanceof CloudStorageError) { + // // TODO: return to page use, show Toast. + // if (e.code === CloudStorageErrorCode.FILE_NOT_FOUND) { + // // setStats(null); + // // setInput(''); + // CommonToast.fail(e.code); + // } else { + // console.warn('Native storage error', e.code, e.message); + // } + // } else { + // console.warn('Unknown error', e); + // } + } finally { + setLoading(false); + } + }, + [cloudStorage, parentDirectory], + ); + + const handleCreateFile = useCallback( + // eslint-disable-next-line @typescript-eslint/no-shadow + async ({ filename, input }: { filename: string; input: string }) => { + setLoading(true); + try { + await cloudStorage.writeFile(parentDirectory + '/' + filename, input); + readFile(filename); + } catch (e) { + // console.warn(e); + commonCloudStorageError(e); + } finally { + setLoading(false); + } + }, + [cloudStorage, parentDirectory, readFile], + ); + + const handleCreateDirectory = useCallback(async () => { + console.log('useCloudStorage - handleCreateDirectory: ', 'isParentDirectoryExist: ', isParentDirectoryExist); + if (isParentDirectoryExist) { + console.log('useCloudStorage - handleCreateDirectory: ', 'skip'); + return; + } + console.log('useCloudStorage - handleCreateDirectory: ', 'create'); + setLoading(true); + try { + await cloudStorage.mkdir(parentDirectory); + await isDirectoryExists(); + // readFile(filename); + } catch (e) { + // console.warn(e); + commonCloudStorageError(e); + } finally { + setLoading(false); + } + }, [cloudStorage, isDirectoryExists, isParentDirectoryExist, parentDirectory]); + + // List parent wallet/ privateKey Wallet addresses. + const handleListContents = useCallback(async () => { + setLoading(true); + try { + const contents = await cloudStorage.readdir(parentDirectory); + console.log('useCloudStorage - Directory contents', contents.length, contents.map(c => `• ${c}`).join('\n')); + return contents; + } catch (e) { + // console.warn(e); + commonCloudStorageError(e); + return false; + } finally { + setLoading(false); + } + }, [cloudStorage, parentDirectory]); + + const handleDeleteDirectory = async (recursive?: boolean) => { + if (recursive === undefined) { + handleDeleteDirectory(false); + // Alert.alert('Delete directory', 'Do you want to delete the directory and all its contents (recursively)?', [ + // { text: 'Cancel', style: 'cancel' }, + // { text: 'Directory only', onPress: () => handleDeleteDirectory(false) }, + // { text: 'Recursively', onPress: () => handleDeleteDirectory(true) }, + // ]); + } else { + setLoading(true); + try { + await cloudStorage.rmdir(parentDirectory, { recursive }); + await isDirectoryExists(); + // setStats(null); + // setInput(''); + } catch (e) { + // console.warn(e); + commonCloudStorageError(e); + } finally { + setLoading(false); + } + } + }; + + useEffect(() => { + isDirectoryExists(); + }, [isDirectoryExists]); + + return { + loading, + cloudStorage, + cloudAvailable, + handleCreateDirectory, + handleDeleteDirectory, + handleListContents, + handleCreateFile, + readFile, + }; +}; diff --git a/packages/mobile-aelf/js/pages/Login/ImportWallet/ImportByCloud/Decrypt/index.tsx b/packages/mobile-aelf/js/pages/Login/ImportWallet/ImportByCloud/Decrypt/index.tsx new file mode 100644 index 000000000..1e0dff0b3 --- /dev/null +++ b/packages/mobile-aelf/js/pages/Login/ImportWallet/ImportByCloud/Decrypt/index.tsx @@ -0,0 +1,149 @@ +// https://github.com/kuatsu/react-native-cloud-storage/blob/master/example/src/views/Home.tsx +import React, { useState } from 'react'; +import { Text, View } from 'react-native'; +import PageContainer from 'components/PageContainer'; +import { isIOS } from '@portkey-wallet/utils/mobile/device'; +import aes from '@portkey-wallet/utils/aes'; +import { makeStyles } from '@rneui/themed'; +import { pTd } from 'utils/unit'; +import fonts from 'assets/theme/fonts'; +import Svg from 'components/Svg'; +import Touchable from 'components/Touchable'; +import CommonButton from 'components/CommonButton'; +import GStyles from 'mobile-did/js/assets/theme/GStyles'; +import CommonInput from 'mobile-did/js/components/CommonInput'; +import CommonToast from 'components/CommonToast'; +import { getPasswords, passwordShowFormat } from '../../../CloudBackup'; +import useRouterParams from '@portkey-wallet/hooks/useRouterParams'; +import { TWalletInfo } from '@portkey-wallet/types/types-eoa/wallet'; +import { useImportWallet } from '../../../hooks/useImportWallet'; + +export default function DecryptByPassword() { + const styles = getStyles(); + + const { walletInCloud } = useRouterParams<{ + walletInCloud: string; + }>(); + + console.log('walletInCloud: ', walletInCloud); + const [password, setPassword] = useState(''); + const [passwordShow, setPasswordShow] = useState(''); + const [secureTextEntry, setSecureTextEntry] = useState(true); + const { importWalletByPrivateKey, importWalletByMnemonic } = useImportWallet(); + + return ( + + + Password + Enter the password that protects your seed phrase in the cloud. + + { + const { newPassword, passwordShow: _passwordShow } = getPasswords(value, password, secureTextEntry); + setPassword(newPassword); + setPasswordShow(_passwordShow); + }} + rightIcon={ + + { + setPassword(''); + setPasswordShow(''); + }}> + + + { + const newSecureTextEntry = !secureTextEntry; + setSecureTextEntry(newSecureTextEntry); + setPasswordShow(passwordShowFormat(newSecureTextEntry, password)); + }}> + + + + } + /> + + + + + { + const result = aes.decrypt(walletInCloud, password); + if (!result) { + CommonToast.fail('Password error'); + return; + } + console.log('decrypt result: ', result, JSON.parse(result)); + const wallet: TWalletInfo = JSON.parse(result); + if (wallet.AESEncryptMnemonic) { + const mnemonic = aes.decrypt(wallet.AESEncryptMnemonic, password); + if (!mnemonic) { + CommonToast.fail('Password error'); + return; + } + console.log('mnemonic: ', mnemonic); + importWalletByMnemonic(mnemonic); + } else { + const privateKey = aes.decrypt(wallet.accountList[0].AESEncryptPrivateKey, password); + if (!privateKey) { + CommonToast.fail('Password error'); + return; + } + console.log('privateKey: ', privateKey); + importWalletByPrivateKey(privateKey); + } + }}> + Continue + + + + ); +} + +const getStyles = makeStyles(theme => ({ + containerStyles: { + backgroundColor: theme.colors.bgBase1, + justifyContent: 'space-between', + }, + title: { + marginTop: pTd(24), + fontSize: pTd(32), + ...fonts.BGMediumFont, + }, + desc: { + color: theme.colors.textBase2, + marginTop: pTd(16), + fontSize: pTd(14), + }, + inputContainer: { + marginTop: pTd(24), + flexDirection: 'row', + flexWrap: 'wrap', + }, + inputLabel: { + paddingLeft: 0, + }, + continueButton: { + marginTop: pTd(24), + marginBottom: pTd(16), + }, +})); diff --git a/packages/mobile-aelf/js/pages/Login/ImportWallet/ImportByCloud/index.tsx b/packages/mobile-aelf/js/pages/Login/ImportWallet/ImportByCloud/index.tsx new file mode 100644 index 000000000..56a94517e --- /dev/null +++ b/packages/mobile-aelf/js/pages/Login/ImportWallet/ImportByCloud/index.tsx @@ -0,0 +1,323 @@ +import React, { useEffect, useState } from 'react'; +import { Text, View } from 'react-native'; +import PageContainer from 'components/PageContainer'; +import { isIOS } from '@portkey-wallet/utils/mobile/device'; +import { makeStyles } from '@rneui/themed'; +import { pTd } from 'utils/unit'; +import { useCardStyles, useWalletCommonStyles } from '../../styles'; +import CommonAvatar from 'components/CommonAvatar'; +import navigationService from 'utils/navigationService'; +import Touchable from 'components/Touchable'; +import fonts from 'assets/theme/fonts'; +import { useCloudStorage } from '../../CloudBackup/useCloudStorage'; +// import LottieLoading from 'components/LottieLoading'; +import { addressFormat, formatStr2EllipsisStr } from '@portkey-wallet/utils'; +import dayjs from 'dayjs'; +import ActionSheet from 'components/ActionSheet'; +import * as Clipboard from 'expo-clipboard'; +import CommonToast from 'components/CommonToast'; +import { ChainId } from '@portkey-wallet/types'; +import { IconName } from 'components/Svg'; + +interface IAddressInfo { + address: string; + addressShow: string; + info?: { + updateTime: string; + wallet: string; + }; +} + +const modalAddressInfo: { + chain: ChainId; + name: string; + icon: IconName; + addressFormatted: string; + addressShow: string; +}[] = [ + { + chain: 'tDVV', + icon: 'Chain=AELF Side', + name: 'aelf dAppChain', + addressFormatted: '', + addressShow: '', + }, + { + chain: 'AELF', + icon: 'Chain=AELF Main', + name: 'aelf MainChain', + addressFormatted: '', + addressShow: '', + }, +]; + +export default function ImportByCloud() { + const styles = getStyles(); + const addressCardStyles = getAddressCardStyles(); + const commonStyles = useWalletCommonStyles(); + const cardStyles = useCardStyles(); + + const { handleListContents, readFile } = useCloudStorage(); + + const [addressesInfo, setAddressesInfo] = useState([]); + const [addresses, setAddresses] = useState([]); + + useEffect(() => { + const getAddressList = async () => { + const addressList = await handleListContents(); + console.log('address list', addressList); + if (addressList) { + const _addressesInfo = addressList.map(item => { + return { + address: item, + addressShow: formatStr2EllipsisStr(addressFormat(item), 8), + // addressShow: addressFormat(item), + info: { + updateTime: '', + wallet: '', + }, + }; + }); + setAddressesInfo(_addressesInfo); + setAddresses(addressList); + // getAddressInfo(addressList, _addressesInfo); + } + }; + getAddressList(); + }, [handleListContents, readFile]); + + useEffect(() => { + if (addresses.length === 0) { + return; + } + const getAddressInfo = async (addressList: string[]) => { + const promiseList = []; + for (const address of addressList) { + promiseList.push(readFile(address)); + } + const _addressesInfo = JSON.parse(JSON.stringify(addressesInfo)) as IAddressInfo[]; + await Promise.all(promiseList).then(results => { + results.forEach((result, index) => { + const address = addressList[index]; + const _index = _addressesInfo.findIndex(item => item.address === address); + if (result && _index >= 0) { + console.log('addressInfo result ', result, _index); + _addressesInfo[_index].info = JSON.parse(result); + } + }); + }); + setAddressesInfo(_addressesInfo); + }; + getAddressInfo(addresses); + }, [addresses, readFile]); + + return ( + + Choose backup + Select the backup you wish to import. + + {/*{loading ? : null}*/} + {addressesInfo.map((item, index) => { + return ( + { + if (!item.info?.wallet) { + return; + } + navigationService.push('ImportByCloudDecrypt', { + walletInCloud: item.info?.wallet, + }); + }}> + + + { + const addressesShowInfo = modalAddressInfo.map(_addressInfo => { + const addressFormatted = addressFormat(item.address, _addressInfo.chain); + return { + ..._addressInfo, + addressFormatted, + addressShow: formatStr2EllipsisStr(addressFormatted, 8), + }; + }); + ActionSheet.alert({ + isCloseShow: false, + title: ( + + Multichain addresses + + {addressesShowInfo.map((addressShowInfo, addressesShowInfoIndex) => { + return ( + + + + + {addressShowInfo.name} + {addressShowInfo.addressShow} + + + { + const isCopy = await Clipboard.setStringAsync(addressShowInfo.addressFormatted); + if (isCopy) { + CommonToast.success('Copied'); + } + }}> + + + + ); + })} + + + ), + buttonGroupDirection: 'column', + buttons: [], + }); + }}> + Multichain + + + {item.addressShow} + + {item.info?.updateTime ? 'Added on ' + dayjs(item.info.updateTime).format('MMM DD, YYYY') : ' '} + + + + + + + + ); + })} + + ); +} + +const getStyles = makeStyles(theme => ({ + rightContainer: { + justifyContent: 'center', + }, + icon: { + // marginRight: pTd(12), + backgroundColor: 'transparent', + }, + marginBottom40: { + marginBottom: pTd(40), + }, + marginVertical16: { + marginVertical: pTd(8), + }, + tagContainer: { + flexDirection: 'row', + borderColor: theme.colors.borderBase1, + borderRadius: pTd(4), + borderWidth: pTd(1), + // paddingHorizontal: pTd(4), + // paddingVertical: pTd(6), + width: pTd(90), + height: pTd(24), + backgroundColor: theme.colors.bgBase1, + alignItems: 'center', + // justifyContent: 'center', + }, + tagText: { + ...fonts.SGMediumFont, + color: theme.colors.textBase1, + fontSize: pTd(12), + lineHeight: pTd(12) * 1.4, + marginLeft: pTd(6), + marginRight: pTd(4), + }, + tagIcon: { + marginRight: pTd(16), + backgroundColor: 'transparent', + }, + title: { + ...fonts.SGMediumFont, + color: theme.colors.textBase1, + fontSize: pTd(16), + fontWeight: 'bold', + lineHeight: pTd(16) * 1.4, + marginTop: pTd(8), + }, + subtitle: { + color: theme.colors.textBase1Opacity07, + fontSize: pTd(14), + lineHeight: pTd(14) * 1.4, + marginTop: pTd(12), + }, +})); +const getAddressCardStyles = makeStyles(theme => ({ + header: { + ...fonts.BGMediumFont, + color: theme.colors.textBase1, + fontSize: pTd(20), + lineHeight: pTd(16) * 1.2, + }, + cardContainer: { + flexDirection: 'column', + }, + card: { + flexDirection: 'row', + paddingVertical: pTd(16), + // paddingHorizontal: pTd(12), + justifyContent: 'space-between', + width: '100%', + marginTop: pTd(12), + }, + info: { + flexDirection: 'row', + }, + chainIcon: { + marginRight: pTd(12), + }, + title: { + color: theme.colors.textBase1, + fontSize: pTd(16), + lineHeight: pTd(16) * 1.4, + // marginTop: pTd(8), + }, + subtitle: { + color: theme.colors.textBase1Opacity07, + fontSize: pTd(14), + lineHeight: pTd(14) * 1.4, + marginTop: pTd(5), + width: pTd(250), + }, +})); diff --git a/packages/mobile-aelf/js/pages/Login/ImportWallet/PrivateKey/index.tsx b/packages/mobile-aelf/js/pages/Login/ImportWallet/PrivateKey/index.tsx index 9d20cedbc..065aa32b4 100644 --- a/packages/mobile-aelf/js/pages/Login/ImportWallet/PrivateKey/index.tsx +++ b/packages/mobile-aelf/js/pages/Login/ImportWallet/PrivateKey/index.tsx @@ -6,9 +6,7 @@ import Touchable from 'components/Touchable'; import CommonButton from 'components/CommonButton'; import Svg from 'components/Svg'; import * as Clipboard from 'expo-clipboard'; -import navigationService from 'utils/navigationService'; -import { SetBiometricsTypeEnum } from 'pages/Pin/SetBiometrics'; -import { authenticationReady } from '@portkey-wallet/utils/mobile/authentication'; +import { useImportWallet } from '../../hooks/useImportWallet'; export default function RecoveryPhrase() { const styles = getStyles(); @@ -64,20 +62,7 @@ export default function RecoveryPhrase() { ); }, [onClear, styles.button, styles.buttonText, theme.colors.iconBase2]); - const importWalletByPrivateKey = useCallback(async () => { - const isReady = await authenticationReady(); - if (isReady) { - navigationService.push('SetBiometrics', { - type: SetBiometricsTypeEnum.create, - privateKey: inputText.trim(), - }); - return; - } - - navigationService.navigate('SetPin', { - privateKey: inputText.trim(), - }); - }, [inputText]); + const { importWalletByPrivateKey } = useImportWallet(); return ( @@ -96,7 +81,7 @@ export default function RecoveryPhrase() { style={styles.importButton} disabledStyle={styles.importButtonDisable} disabled={!isPrivateKeyValid} - onPress={importWalletByPrivateKey}> + onPress={() => importWalletByPrivateKey(inputText)}> Import diff --git a/packages/mobile-aelf/js/pages/Login/ImportWallet/RecoveryPhrase/index.tsx b/packages/mobile-aelf/js/pages/Login/ImportWallet/RecoveryPhrase/index.tsx index 5b386d68d..06fdccc4c 100644 --- a/packages/mobile-aelf/js/pages/Login/ImportWallet/RecoveryPhrase/index.tsx +++ b/packages/mobile-aelf/js/pages/Login/ImportWallet/RecoveryPhrase/index.tsx @@ -9,9 +9,7 @@ import CommonButton from 'components/CommonButton'; import CommonToast from 'components/CommonToast'; import * as bip39 from 'bip39'; import * as Clipboard from 'expo-clipboard'; -import { authenticationReady } from '@portkey-wallet/utils/mobile/authentication'; -import navigationService from 'utils/navigationService'; -import { SetBiometricsTypeEnum } from 'pages/Pin/SetBiometrics'; +import { useImportWallet } from '../../hooks/useImportWallet'; const MnemonicsWordCount = 12; let invalidMnemonicsToastTimer: NodeJS.Timeout; @@ -84,20 +82,7 @@ export default function RecoveryPhrase() { ); }, [onClear, styles.button, styles.buttonText, theme.colors.iconBase2]); - const importWalletByMnemonic = useCallback(async () => { - const isReady = await authenticationReady(); - if (isReady) { - navigationService.push('SetBiometrics', { - type: SetBiometricsTypeEnum.create, - mnemonics: mnemonics.join(' '), - }); - return; - } - - navigationService.navigate('SetPin', { - mnemonics: mnemonics.join(' '), - }); - }, [mnemonics]); + const { importWalletByMnemonic } = useImportWallet(); return ( @@ -130,7 +115,7 @@ export default function RecoveryPhrase() { style={styles.importButton} disabledStyle={styles.importButtonDisable} disabled={!isMnemonicsValid} - onPress={importWalletByMnemonic}> + onPress={() => importWalletByMnemonic(mnemonics)}> Import diff --git a/packages/mobile-aelf/js/pages/Login/WalletImportTypeSelect/index.tsx b/packages/mobile-aelf/js/pages/Login/WalletImportTypeSelect/index.tsx index 89b6afb53..82502bba6 100644 --- a/packages/mobile-aelf/js/pages/Login/WalletImportTypeSelect/index.tsx +++ b/packages/mobile-aelf/js/pages/Login/WalletImportTypeSelect/index.tsx @@ -24,12 +24,14 @@ const ListItem: { isAndroid: true, svgName: 'google-drive', title: 'Google Drive', + importType: 'google', subTitle: 'Import your seed phrase from Google Drive.', }, { isIOS: true, localImage: iCloudImage, title: 'iCloud', + importType: 'iCloud', subTitle: 'Import your seed phrase from iCloud.', }, { @@ -75,9 +77,15 @@ export default function WalletImportTypeSelect() { if (!item.importType) { return; } - navigationService.push('ImportWallet', { - importType: item.importType, - }); + if (item.isAndroid || item.isIOS) { + navigationService.push('ImportByCloud', { + importType: item.importType, + }); + } else { + navigationService.push('ImportWallet', { + importType: item.importType, + }); + } }}> {/* TODO: iCloud, google drive loading */} diff --git a/packages/mobile-aelf/js/pages/Login/hooks/useImportWallet.tsx b/packages/mobile-aelf/js/pages/Login/hooks/useImportWallet.tsx new file mode 100644 index 000000000..f257f3ec9 --- /dev/null +++ b/packages/mobile-aelf/js/pages/Login/hooks/useImportWallet.tsx @@ -0,0 +1,44 @@ +import { useCallback } from 'react'; +import navigationService from 'utils/navigationService'; +import { SetBiometricsTypeEnum } from 'pages/Pin/SetBiometrics'; +import { authenticationReady } from '@portkey-wallet/utils/mobile/authentication'; + +function formatMnemonics(mnemonics: string[] | string) { + return typeof mnemonics === 'string' ? mnemonics.trim() : mnemonics.join(' '); +} +export const useImportWallet = () => { + const importWalletByMnemonic = useCallback(async (mnemonics: string[] | string) => { + const isReady = await authenticationReady(); + if (isReady) { + navigationService.push('SetBiometrics', { + type: SetBiometricsTypeEnum.create, + mnemonics: formatMnemonics(mnemonics), + }); + return; + } + + navigationService.navigate('SetPin', { + mnemonics: formatMnemonics(mnemonics), + }); + }, []); + + const importWalletByPrivateKey = useCallback(async (privateKey: string) => { + const isReady = await authenticationReady(); + if (isReady) { + navigationService.push('SetBiometrics', { + type: SetBiometricsTypeEnum.create, + privateKey: privateKey.trim(), + }); + return; + } + + navigationService.navigate('SetPin', { + privateKey: privateKey.trim(), + }); + }, []); + + return { + importWalletByMnemonic, + importWalletByPrivateKey, + }; +}; diff --git a/packages/mobile-aelf/js/pages/Login/index.ts b/packages/mobile-aelf/js/pages/Login/index.ts index 8701f93d2..b01d0929a 100644 --- a/packages/mobile-aelf/js/pages/Login/index.ts +++ b/packages/mobile-aelf/js/pages/Login/index.ts @@ -10,6 +10,10 @@ import ConfirmBackup from './ConfirmBackup'; import ManualBackup from './ManualBackup'; import ManualBackupSuccess from './ManualBackup/Success'; import WalletImportTypeSelect from './WalletImportTypeSelect'; +import ImportByCloud from './ImportWallet/ImportByCloud'; +import ImportByCloudDecrypt from './ImportWallet/ImportByCloud/Decrypt'; +import CloudBackup from './CloudBackup'; +import CloudBackupDev from './CloudBackup/cases'; const stackNav = [ { name: 'LoginEmail', component: LoginEmail }, @@ -21,8 +25,12 @@ const stackNav = [ { name: 'PrepareWallet', component: PrepareWallet, options: { gestureEnabled: false } }, { name: 'ImportWallet', component: ImportWallet }, { name: 'WalletImportTypeSelect', component: WalletImportTypeSelect }, + { name: 'ImportByCloud', component: ImportByCloud }, + { name: 'ImportByCloudDecrypt', component: ImportByCloudDecrypt }, { name: 'ConfirmBackup', component: ConfirmBackup }, { name: 'ManualBackup', component: ManualBackup }, + { name: 'CloudBackup', component: CloudBackup }, + { name: 'CloudBackupDev', component: CloudBackupDev }, { name: 'ManualBackupSuccess', component: ManualBackupSuccess }, ] as const; diff --git a/packages/mobile-aelf/package.json b/packages/mobile-aelf/package.json index 316920c80..39414a20b 100644 --- a/packages/mobile-aelf/package.json +++ b/packages/mobile-aelf/package.json @@ -129,7 +129,7 @@ "react": "^18.3.1", "react-native": "^0.75.0", "react-native-background-timer": "^2.4.1", - "react-native-cloud-storage": "^2.2.1", + "react-native-cloud-storage": "^2.2.2", "react-native-config": "^1.4.6", "react-native-crypto": "^2.2.0", "react-native-device-info": "^10.11.0",