diff --git a/app/index.js b/app/index.js index 4845493fb..40dc2cfa0 100644 --- a/app/index.js +++ b/app/index.js @@ -10,6 +10,7 @@ import { TamaguiProvider, createTamagui } from 'tamagui'; import App from './App'; import { name as appName } from './app.json'; +import './src/utils/ethers'; const tamaguiConfig = createTamagui(config); diff --git a/app/package.json b/app/package.json index c3a70a1f3..7b9122e7d 100644 --- a/app/package.json +++ b/app/package.json @@ -68,6 +68,7 @@ "react-native-localize": "^3.4.1", "react-native-nfc-manager": "^3.15.1", "react-native-passport-reader": "^1.0.3", + "react-native-quick-crypto": "^0.7.12", "react-native-safe-area-context": "^5.2.0", "react-native-screens": "^4.6.0", "react-native-svg": "^15.11.1", diff --git a/app/src/hooks/useMnemonic.ts b/app/src/hooks/useMnemonic.ts new file mode 100644 index 000000000..0aa33d532 --- /dev/null +++ b/app/src/hooks/useMnemonic.ts @@ -0,0 +1,24 @@ +import { useCallback, useState } from 'react'; + +import { ethers } from 'ethers'; + +import { useAuth } from '../stores/authProvider'; + +export default function useMnemonic() { + const { getOrCreateMnemonic } = useAuth(); + const [mnemonic, setMnemonic] = useState(); + + const loadMnemonic = useCallback(async () => { + const storedMnemonic = await getOrCreateMnemonic(); + if (!storedMnemonic) { + return; + } + const { entropy } = storedMnemonic.data; + setMnemonic(ethers.Mnemonic.fromEntropy(entropy).phrase.split(' ')); + }, []); + + return { + loadMnemonic, + mnemonic, + }; +} diff --git a/app/src/screens/AccountFlow/AccountRecoveryChoiceScreen.tsx b/app/src/screens/AccountFlow/AccountRecoveryChoiceScreen.tsx index 3362f0b9c..28bccae2d 100644 --- a/app/src/screens/AccountFlow/AccountRecoveryChoiceScreen.tsx +++ b/app/src/screens/AccountFlow/AccountRecoveryChoiceScreen.tsx @@ -13,7 +13,7 @@ import RestoreAccountSvg from '../../images/icons/restore_account.svg'; import { ExpandableBottomLayout } from '../../layouts/ExpandableBottomLayout'; import { useAuth } from '../../stores/authProvider'; import { useSettingStore } from '../../stores/settingStore'; -import { STORAGE_NAME, useBackupPrivateKey } from '../../utils/cloudBackup'; +import { STORAGE_NAME, useBackupMnemonic } from '../../utils/cloudBackup'; import { black, slate500, slate600, white } from '../../utils/colors'; interface AccountRecoveryChoiceScreenProps {} @@ -21,11 +21,11 @@ interface AccountRecoveryChoiceScreenProps {} const AccountRecoveryChoiceScreen: React.FC< AccountRecoveryChoiceScreenProps > = ({}) => { - const { restoreAccountFromPrivateKey } = useAuth(); + const { restoreAccountFromMnemonic } = useAuth(); const [restoring, setRestoring] = useState(false); const { cloudBackupEnabled, toggleCloudBackupEnabled, biometricsAvailable } = useSettingStore(); - const { download } = useBackupPrivateKey(); + const { download } = useBackupMnemonic(); const onRestoreFromCloudNext = useHapticNavigation('AccountVerifiedSuccess'); const onEnterRecoveryPress = useHapticNavigation('RecoverWithPhrase'); @@ -33,8 +33,8 @@ const AccountRecoveryChoiceScreen: React.FC< const onRestoreFromCloudPress = useCallback(async () => { setRestoring(true); try { - const restoredPrivKey = await download(); - await restoreAccountFromPrivateKey(restoredPrivKey); + const mnemonic = await download(); + await restoreAccountFromMnemonic(mnemonic.phrase); if (!cloudBackupEnabled) { toggleCloudBackupEnabled(); } @@ -47,7 +47,7 @@ const AccountRecoveryChoiceScreen: React.FC< }, [ cloudBackupEnabled, download, - restoreAccountFromPrivateKey, + restoreAccountFromMnemonic, onRestoreFromCloudNext, ]); @@ -77,7 +77,8 @@ const AccountRecoveryChoiceScreen: React.FC< onPress={onRestoreFromCloudPress} disabled={restoring || !biometricsAvailable} > - Restore from {STORAGE_NAME} + {restoring ? 'Restoring' : 'Restore'} from {STORAGE_NAME} + {restoring ? '…' : ''} diff --git a/app/src/screens/AccountFlow/RecoverWithPhraseScreen.tsx b/app/src/screens/AccountFlow/RecoverWithPhraseScreen.tsx index 6a743c0cb..8c9eea4fc 100644 --- a/app/src/screens/AccountFlow/RecoverWithPhraseScreen.tsx +++ b/app/src/screens/AccountFlow/RecoverWithPhraseScreen.tsx @@ -45,6 +45,7 @@ const RecoverWithPhraseScreen: React.FC< return; } const result = await restoreAccountFromMnemonic(slimMnemonic); + if (!result) { console.warn('Failed to restore account'); // TODO SOMETHING ELSE? @@ -52,7 +53,7 @@ const RecoverWithPhraseScreen: React.FC< return; } setRestoring(false); - navigation.navigate('Home'); + navigation.navigate('AccountVerifiedSuccess'); }, [mnemonic, restoreAccountFromMnemonic]); return ( diff --git a/app/src/screens/AccountFlow/SaveRecoveryPhraseScreen.tsx b/app/src/screens/AccountFlow/SaveRecoveryPhraseScreen.tsx index 4ac83d1a1..3bb0677e4 100644 --- a/app/src/screens/AccountFlow/SaveRecoveryPhraseScreen.tsx +++ b/app/src/screens/AccountFlow/SaveRecoveryPhraseScreen.tsx @@ -1,7 +1,4 @@ import React, { useCallback, useState } from 'react'; -import { findBestLanguageTag } from 'react-native-localize'; - -import { ethers } from 'ethers'; import Mnemonic from '../../components/Mnemonic'; import { PrimaryButton } from '../../components/buttons/PrimaryButton'; @@ -10,8 +7,8 @@ import { Caption } from '../../components/typography/Caption'; import Description from '../../components/typography/Description'; import { Title } from '../../components/typography/Title'; import useHapticNavigation from '../../hooks/useHapticNavigation'; +import useMnemonic from '../../hooks/useMnemonic'; import { ExpandableBottomLayout } from '../../layouts/ExpandableBottomLayout'; -import { useAuth } from '../../stores/authProvider'; import { STORAGE_NAME } from '../../utils/cloudBackup'; import { black, slate400, white } from '../../utils/colors'; @@ -20,33 +17,14 @@ interface SaveRecoveryPhraseScreenProps {} const SaveRecoveryPhraseScreen: React.FC< SaveRecoveryPhraseScreenProps > = ({}) => { - const { getOrCreatePrivateKey } = useAuth(); - const [mnemonic, setMnemonic] = useState(); const [userHasSeenMnemonic, setUserHasSeenMnemonic] = useState(false); + const { mnemonic, loadMnemonic } = useMnemonic(); const onRevealWords = useCallback(async () => { await loadMnemonic(); setUserHasSeenMnemonic(true); }, []); - const loadMnemonic = useCallback(async () => { - const privKey = await getOrCreatePrivateKey(); - if (!privKey) { - return; - } - - const { languageTag } = findBestLanguageTag( - Object.keys(ethers.wordlists), - ) || { languageTag: 'en' }; - - const words = ethers.Mnemonic.entropyToPhrase( - privKey.data, - ethers.wordlists[languageTag], - ); - - setMnemonic(words.trim().split(' ')); - }, []); - const onCloudBackupPress = useHapticNavigation('CloudBackupSettings', { params: { nextScreen: 'SaveRecoveryPhrase' }, }); diff --git a/app/src/screens/Settings/CloudBackupScreen.tsx b/app/src/screens/Settings/CloudBackupScreen.tsx index 3039ba1a5..84fbc4106 100644 --- a/app/src/screens/Settings/CloudBackupScreen.tsx +++ b/app/src/screens/Settings/CloudBackupScreen.tsx @@ -15,7 +15,7 @@ import Cloud from '../../images/icons/logo_cloud_backup.svg'; import { ExpandableBottomLayout } from '../../layouts/ExpandableBottomLayout'; import { useAuth } from '../../stores/authProvider'; import { useSettingStore } from '../../stores/settingStore'; -import { STORAGE_NAME, useBackupPrivateKey } from '../../utils/cloudBackup'; +import { STORAGE_NAME, useBackupMnemonic } from '../../utils/cloudBackup'; import { black, white } from '../../utils/colors'; import { buttonTap, confirmTap } from '../../utils/haptic'; @@ -32,10 +32,10 @@ interface CloudBackupScreenProps const CloudBackupScreen: React.FC = ({ route: { params }, }) => { - const { getOrCreatePrivateKey, loginWithBiometrics } = useAuth(); + const { getOrCreateMnemonic, loginWithBiometrics } = useAuth(); const { cloudBackupEnabled, toggleCloudBackupEnabled, biometricsAvailable } = useSettingStore(); - const { upload, disableBackup } = useBackupPrivateKey(); + const { upload, disableBackup } = useBackupMnemonic(); const [pending, setPending] = useState(false); const { showModal } = useModal( @@ -70,17 +70,17 @@ const CloudBackupScreen: React.FC = ({ setPending(true); - const privKey = await getOrCreatePrivateKey(); - if (!privKey) { + const storedMnemonic = await getOrCreateMnemonic(); + if (!storedMnemonic) { setPending(false); return; } - await upload(privKey.data); + await upload(storedMnemonic.data); toggleCloudBackupEnabled(); setPending(false); }, [ cloudBackupEnabled, - getOrCreatePrivateKey, + getOrCreateMnemonic, upload, toggleCloudBackupEnabled, ]); diff --git a/app/src/screens/Settings/DevSettingsScreen.tsx b/app/src/screens/Settings/DevSettingsScreen.tsx index 56b06fe18..f68d21091 100644 --- a/app/src/screens/Settings/DevSettingsScreen.tsx +++ b/app/src/screens/Settings/DevSettingsScreen.tsx @@ -1,4 +1,5 @@ -import React from 'react'; +import React, { PropsWithChildren, useEffect, useState } from 'react'; +import { Platform, TextInput } from 'react-native'; import { useNavigation } from '@react-navigation/native'; import { @@ -8,10 +9,23 @@ import { IterationCw, VenetianMask, } from '@tamagui/lucide-icons'; -import { Adapt, Button, Fieldset, Label, Select, Sheet, YStack } from 'tamagui'; +import { + Adapt, + Button, + Fieldset, + Label, + Select, + Sheet, + Text, + YStack, +} from 'tamagui'; import { genMockPassportData } from '../../../../common/src/utils/passports/genMockPassportData'; import { RootStackParamList } from '../../Navigation'; +import { + unsafe_clearSecrets, + unsafe_getPrivateKey, +} from '../../stores/authProvider'; import { storePassportData, usePassport, @@ -20,6 +34,22 @@ import { borderColor, textBlack } from '../../utils/colors'; interface DevSettingsScreenProps {} +function SelectableText({ children, ...props }: PropsWithChildren) { + if (Platform.OS === 'ios') { + return ( + + {children} + + ); + } else { + return ( + + {children} + + ); + } +} + const items = [ 'DevSettings', 'Splash', @@ -103,14 +133,20 @@ const ScreenSelector = ({}) => { const DevSettingsScreen: React.FC = ({}) => { const { clearPassportData } = usePassport(); + const [privateKey, setPrivateKey] = useState('Loading private key…'); const nav = useNavigation(); - function handleRestart() { - clearPassportData(); + async function handleRestart() { + await clearPassportData(); nav.navigate('Launch'); } + async function deleteEverything() { + await unsafe_clearSecrets(); + await handleRestart(); + } + function handleGenerateMockPassportData() { const passportData = genMockPassportData( 'sha256', @@ -123,15 +159,14 @@ const DevSettingsScreen: React.FC = ({}) => { storePassportData(passportData); } + useEffect(() => { + unsafe_getPrivateKey().then(setPrivateKey); + }, []); + return ( - -
-