mirror of
https://github.com/selfxyz/self.git
synced 2026-04-27 03:01:15 -04:00
feat: add a recovery phrase screen (#24)
This commit is contained in:
committed by
GitHub
parent
506ef1b25d
commit
7bb592d6f9
@@ -23,6 +23,7 @@
|
||||
"@openpassport/zk-kit-lean-imt": "^0.0.6",
|
||||
"@openpassport/zk-kit-smt": "^0.0.1",
|
||||
"@react-native-async-storage/async-storage": "^1.23.1",
|
||||
"@react-native-clipboard/clipboard": "^1.16.1",
|
||||
"@react-native-community/netinfo": "^11.3.1",
|
||||
"@react-navigation/elements": "^2.2.5",
|
||||
"@react-navigation/native": "^7.0.14",
|
||||
@@ -55,6 +56,7 @@
|
||||
"react-native-gesture-handler": "^2.22.1",
|
||||
"react-native-get-random-values": "^1.11.0",
|
||||
"react-native-keychain": "^8.2.0",
|
||||
"react-native-localize": "^3.4.1",
|
||||
"react-native-nfc-manager": "^3.15.1",
|
||||
"react-native-passport-reader": "^1.0.3",
|
||||
"react-native-safe-area-context": "^5.1.0",
|
||||
|
||||
@@ -25,6 +25,8 @@ import PassportOnboardingScreen from './screens/Onboarding/PassportOnboardingScr
|
||||
import ProveScreen from './screens/ProveFlow/ProveScreen';
|
||||
import ValidProofScreen from './screens/ProveFlow/ValidProofScreen';
|
||||
import QRCodeViewFinderScreen from './screens/ProveFlow/ViewFinder';
|
||||
import AccountRecoveryScreen from './screens/Settings/AccountRecoveryScreen';
|
||||
import ShowRecoveryPhraseScreen from './screens/Settings/ShowRecoveryPhraseScreen';
|
||||
import WrongProofScreen from './screens/ProveFlow/WrongProofScreen';
|
||||
import SettingsScreen from './screens/SettingsScreen';
|
||||
import SplashScreen from './screens/SplashScreen';
|
||||
@@ -209,6 +211,18 @@ const RootStack = createStackNavigator({
|
||||
screens: {},
|
||||
},
|
||||
},
|
||||
AccountRecovery: {
|
||||
screen: AccountRecoveryScreen,
|
||||
options: {
|
||||
headerShown: false,
|
||||
},
|
||||
},
|
||||
ShowRecoveryPhrase: {
|
||||
screen: ShowRecoveryPhraseScreen,
|
||||
options: {
|
||||
headerShown: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
105
app/src/components/Mnemonic.tsx
Normal file
105
app/src/components/Mnemonic.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
|
||||
import Clipboard from '@react-native-clipboard/clipboard';
|
||||
import { Button, Text, XStack, YStack } from 'tamagui';
|
||||
|
||||
import {
|
||||
black,
|
||||
slate50,
|
||||
slate200,
|
||||
slate300,
|
||||
slate500,
|
||||
teal500,
|
||||
white,
|
||||
} from '../utils/colors';
|
||||
|
||||
interface MnemonicProps {
|
||||
words?: string[];
|
||||
revealWords?: boolean;
|
||||
}
|
||||
interface WordPill {
|
||||
index: number;
|
||||
word: string;
|
||||
}
|
||||
const WordPill = ({ index, word }: WordPill) => {
|
||||
return (
|
||||
<XStack
|
||||
key={index}
|
||||
borderColor={slate300}
|
||||
backgroundColor={white}
|
||||
borderWidth="$0.5"
|
||||
borderRadius="$2"
|
||||
py="$1"
|
||||
px="$2"
|
||||
gap="$2"
|
||||
>
|
||||
<Text color={slate300} fontSize={14} fontWeight={500}>
|
||||
{index}
|
||||
</Text>
|
||||
<Text color={slate500} fontSize={14} fontWeight={500}>
|
||||
{word}
|
||||
</Text>
|
||||
</XStack>
|
||||
);
|
||||
};
|
||||
const REDACTED = new Array(24).fill(' '.repeat(4));
|
||||
const Mnemonic = ({ words = REDACTED }: MnemonicProps) => {
|
||||
const [revealWords, setRevealWords] = useState(false);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const copyToClipboardOrReveal = useCallback(() => {
|
||||
if (!revealWords) {
|
||||
return setRevealWords(previous => !previous);
|
||||
}
|
||||
Clipboard.setString(words.join(' '));
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2500);
|
||||
}, [words, revealWords]);
|
||||
|
||||
return (
|
||||
<YStack position="relative" alignItems="stretch" gap={0}>
|
||||
<XStack
|
||||
borderColor={slate200}
|
||||
backgroundColor={slate50}
|
||||
background="blue"
|
||||
borderWidth="$1"
|
||||
borderBottomWidth={0}
|
||||
borderTopLeftRadius="$5"
|
||||
borderTopRightRadius="$5"
|
||||
gap="$2.5"
|
||||
p="$4"
|
||||
flexWrap="wrap"
|
||||
>
|
||||
{(revealWords ? words : REDACTED).map((word, i) => (
|
||||
<WordPill key={i} word={word} index={i} />
|
||||
))}
|
||||
</XStack>
|
||||
<XStack
|
||||
borderTopColor={slate200}
|
||||
borderTopWidth="$1"
|
||||
justifyContent="center"
|
||||
alignItems="stretch"
|
||||
>
|
||||
<Button
|
||||
unstyled
|
||||
color={revealWords ? (copied ? black : white) : black}
|
||||
borderColor={revealWords ? (copied ? teal500 : black) : slate200}
|
||||
backgroundColor={revealWords ? (copied ? teal500 : black) : slate50}
|
||||
borderWidth="$1"
|
||||
borderTopWidth={0}
|
||||
borderBottomLeftRadius="$5"
|
||||
borderBottomRightRadius="$5"
|
||||
py="$2"
|
||||
onPress={copyToClipboardOrReveal}
|
||||
width="100%"
|
||||
textAlign="center"
|
||||
>
|
||||
{revealWords
|
||||
? `${copied ? 'COPIED' : 'COPY'} TO CLIPBOARD`
|
||||
: 'TAP TO REVEAL'}
|
||||
</Button>
|
||||
</XStack>
|
||||
</YStack>
|
||||
);
|
||||
};
|
||||
|
||||
export default Mnemonic;
|
||||
@@ -1,68 +0,0 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet, Text } from 'react-native';
|
||||
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { Image, YStack } from 'tamagui';
|
||||
|
||||
import { PrimaryButton } from '../components/buttons/PrimaryButton';
|
||||
import Logo from '../images/logo.png';
|
||||
import { ExpandableBottomLayout } from '../layouts/ExpandableBottomLayout';
|
||||
import { slate50, slate100, slate500, slate700 } from '../utils/colors';
|
||||
|
||||
interface AccountRecoveryScreenProps {}
|
||||
|
||||
const AccountRecoveryScreen: React.FC<AccountRecoveryScreenProps> = ({}) => {
|
||||
const navigation = useNavigation();
|
||||
|
||||
return (
|
||||
<ExpandableBottomLayout.Layout>
|
||||
<ExpandableBottomLayout.TopSection>
|
||||
<Image src={Logo} />
|
||||
</ExpandableBottomLayout.TopSection>
|
||||
<ExpandableBottomLayout.BottomSection>
|
||||
<YStack gap="$2.5">
|
||||
<Text style={styles.subheader}>
|
||||
By continuing, you certify that this passport belongs to you and is
|
||||
not stolen or forged.
|
||||
</Text>
|
||||
<PrimaryButton onPress={() => navigation.navigate('TODO: restore')}>
|
||||
Restore my account
|
||||
</PrimaryButton>
|
||||
<PrimaryButton
|
||||
onPress={() => navigation.navigate('PassportOnboarding')}
|
||||
>
|
||||
Create new account
|
||||
</PrimaryButton>
|
||||
</YStack>
|
||||
</ExpandableBottomLayout.BottomSection>
|
||||
</ExpandableBottomLayout.Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccountRecoveryScreen;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
subheader: {
|
||||
color: slate700,
|
||||
// fontWeight: '500',
|
||||
fontSize: 20,
|
||||
lineHeight: 26,
|
||||
textAlign: 'center',
|
||||
},
|
||||
link: {
|
||||
textDecorationLine: 'underline',
|
||||
},
|
||||
notice: {
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
paddingLeft: 20,
|
||||
paddingRight: 20,
|
||||
backgroundColor: slate50,
|
||||
borderColor: slate100,
|
||||
borderWidth: 1,
|
||||
borderStyle: 'solid',
|
||||
color: slate500,
|
||||
textAlign: 'center',
|
||||
lineHeight: 18,
|
||||
},
|
||||
});
|
||||
50
app/src/screens/Settings/AccountRecoveryScreen.tsx
Normal file
50
app/src/screens/Settings/AccountRecoveryScreen.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { View, YStack } from 'tamagui';
|
||||
|
||||
import { PrimaryButton } from '../../components/buttons/PrimaryButton';
|
||||
import { SecondaryButton } from '../../components/buttons/SecondaryButton';
|
||||
import Description from '../../components/typography/Description';
|
||||
import { Title } from '../../components/typography/Title';
|
||||
import RestoreAccountSvg from '../../images/icons/restore_account.svg';
|
||||
import { ExpandableBottomLayout } from '../../layouts/ExpandableBottomLayout';
|
||||
import { slate600, white } from '../../utils/colors';
|
||||
|
||||
interface AccountRecoveryScreenProps {}
|
||||
|
||||
const AccountRecoveryScreen: React.FC<AccountRecoveryScreenProps> = ({}) => {
|
||||
const navigation = useNavigation();
|
||||
|
||||
return (
|
||||
<ExpandableBottomLayout.Layout>
|
||||
<ExpandableBottomLayout.TopSection>
|
||||
<View borderColor={slate600} borderWidth="$1" borderRadius="$10" p="$5">
|
||||
<RestoreAccountSvg height={80} width={80} color={white} />
|
||||
</View>
|
||||
</ExpandableBottomLayout.TopSection>
|
||||
<ExpandableBottomLayout.BottomSection>
|
||||
<YStack alignItems="center" gap="$2.5" pb="$2.5">
|
||||
<Title>Restore your Self ID account</Title>
|
||||
<Description>
|
||||
By continuing, you certify that this passport belongs to you and is
|
||||
not stolen or forged.
|
||||
</Description>
|
||||
|
||||
<YStack gap="$2.5" width="100%" pt="$6">
|
||||
<PrimaryButton onPress={() => navigation.navigate('TODO: restore')}>
|
||||
Restore my account
|
||||
</PrimaryButton>
|
||||
<SecondaryButton
|
||||
onPress={() => navigation.navigate('ShowRecoveryPhrase')}
|
||||
>
|
||||
Create new account
|
||||
</SecondaryButton>
|
||||
</YStack>
|
||||
</YStack>
|
||||
</ExpandableBottomLayout.BottomSection>
|
||||
</ExpandableBottomLayout.Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccountRecoveryScreen;
|
||||
84
app/src/screens/Settings/ShowRecoveryPhraseScreen.tsx
Normal file
84
app/src/screens/Settings/ShowRecoveryPhraseScreen.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { findBestLanguageTag } from 'react-native-localize';
|
||||
|
||||
import { ethers } from 'ethers';
|
||||
import { YStack } from 'tamagui';
|
||||
|
||||
import Mnemonic from '../../components/Mnemonic';
|
||||
import { PrimaryButton } from '../../components/buttons/PrimaryButton';
|
||||
import { SecondaryButton } from '../../components/buttons/SecondaryButton';
|
||||
import { Caption } from '../../components/typography/Caption';
|
||||
import Description from '../../components/typography/Description';
|
||||
import { Title } from '../../components/typography/Title';
|
||||
import { ExpandableBottomLayout } from '../../layouts/ExpandableBottomLayout';
|
||||
import { slate400 } from '../../utils/colors';
|
||||
import { loadSecretOrCreateIt } from '../../utils/keychain';
|
||||
|
||||
interface ShowRecoveryPhraseScreenProps {}
|
||||
|
||||
const ShowRecoveryPhraseScreen: React.FC<
|
||||
ShowRecoveryPhraseScreenProps
|
||||
> = ({}) => {
|
||||
const [mnemonic, setMnemonic] = useState<string[]>();
|
||||
|
||||
const loadPassword = useCallback(async () => {
|
||||
const privKey = await loadSecretOrCreateIt();
|
||||
|
||||
const { languageTag } = findBestLanguageTag(
|
||||
Object.keys(ethers.wordlists),
|
||||
) || { languageTag: 'en' };
|
||||
|
||||
const words = ethers.Mnemonic.entropyToPhrase(
|
||||
privKey,
|
||||
ethers.wordlists[languageTag],
|
||||
);
|
||||
|
||||
setMnemonic(words.trim().split(' '));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadPassword();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ExpandableBottomLayout.Layout>
|
||||
<ExpandableBottomLayout.BottomSection>
|
||||
<YStack
|
||||
alignItems="center"
|
||||
gap="$2.5"
|
||||
pb="$2.5"
|
||||
height="100%"
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<Title>Save your recovery phrase</Title>
|
||||
<Description>
|
||||
This phrase is the only way to recover your account. Keep it secret,
|
||||
keep it safe.
|
||||
</Description>
|
||||
<Mnemonic words={mnemonic} revealWords={false} />
|
||||
<YStack gap="$2.5" width="100%" pt="$6" alignItems="center">
|
||||
<Caption color={slate400}>
|
||||
You can reveal your recovery phrase in settings.
|
||||
</Caption>
|
||||
<PrimaryButton
|
||||
onPress={
|
||||
() => undefined /* TODO: navigate to icloud backup screen */
|
||||
}
|
||||
>
|
||||
Enable iCloud Back up
|
||||
</PrimaryButton>
|
||||
<SecondaryButton
|
||||
onPress={
|
||||
() => undefined /* TODO: show alert to confirm then navigate */
|
||||
}
|
||||
>
|
||||
Skip making a back up
|
||||
</SecondaryButton>
|
||||
</YStack>
|
||||
</YStack>
|
||||
</ExpandableBottomLayout.BottomSection>
|
||||
</ExpandableBottomLayout.Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShowRecoveryPhraseScreen;
|
||||
@@ -16,7 +16,7 @@ const SplashScreen: React.FC<SplashScreenProps> = ({}) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (userLoaded) {
|
||||
if (passportData && passportData.dg2Hash) {
|
||||
if (passportData) {
|
||||
navigation.navigate('Home');
|
||||
} else {
|
||||
navigation.navigate('Launch');
|
||||
|
||||
@@ -15,6 +15,7 @@ export const slate800 = '#1E293B';
|
||||
export const sky500 = '#0EA5E9';
|
||||
export const green500 = '#22C55E';
|
||||
export const red500 = '#EF4444';
|
||||
export const teal300 = '#5EEAD4';
|
||||
export const teal500 = '#5EEAD4';
|
||||
export const neutral400 = '#A3A3A3';
|
||||
export const neutral700 = '#404040';
|
||||
|
||||
@@ -2509,6 +2509,23 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-native-clipboard/clipboard@npm:^1.16.1":
|
||||
version: 1.16.1
|
||||
resolution: "@react-native-clipboard/clipboard@npm:1.16.1"
|
||||
peerDependencies:
|
||||
react: ">= 16.9.0"
|
||||
react-native: ">= 0.61.5"
|
||||
react-native-macos: ">= 0.61.0"
|
||||
react-native-windows: ">= 0.61.0"
|
||||
peerDependenciesMeta:
|
||||
react-native-macos:
|
||||
optional: true
|
||||
react-native-windows:
|
||||
optional: true
|
||||
checksum: 10c0/765e1c140c34fbfe30a1e7e0657cc2a325a103a4fcaf757994dff3d8877beb8c4f0b915eac5542ada5910560ce2cfe624cd23081302dd6d6bc76354ff0d98d5e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-native-community/cli-clean@npm:14.1.0":
|
||||
version: 14.1.0
|
||||
resolution: "@react-native-community/cli-clean@npm:14.1.0"
|
||||
@@ -12177,6 +12194,7 @@ __metadata:
|
||||
"@openpassport/zk-kit-lean-imt": "npm:^0.0.6"
|
||||
"@openpassport/zk-kit-smt": "npm:^0.0.1"
|
||||
"@react-native-async-storage/async-storage": "npm:^1.23.1"
|
||||
"@react-native-clipboard/clipboard": "npm:^1.16.1"
|
||||
"@react-native-community/cli": "npm:^14.1.1"
|
||||
"@react-native-community/netinfo": "npm:^11.3.1"
|
||||
"@react-native/babel-preset": "npm:0.75.4"
|
||||
@@ -12233,6 +12251,7 @@ __metadata:
|
||||
react-native-gesture-handler: "npm:^2.22.1"
|
||||
react-native-get-random-values: "npm:^1.11.0"
|
||||
react-native-keychain: "npm:^8.2.0"
|
||||
react-native-localize: "npm:^3.4.1"
|
||||
react-native-nfc-manager: "npm:^3.15.1"
|
||||
react-native-passport-reader: "npm:^1.0.3"
|
||||
react-native-safe-area-context: "npm:^5.1.0"
|
||||
@@ -12813,6 +12832,20 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-native-localize@npm:^3.4.1":
|
||||
version: 3.4.1
|
||||
resolution: "react-native-localize@npm:3.4.1"
|
||||
peerDependencies:
|
||||
react: ">=18.1.0"
|
||||
react-native: ">=0.70.0"
|
||||
react-native-macos: ">=0.70.0"
|
||||
peerDependenciesMeta:
|
||||
react-native-macos:
|
||||
optional: true
|
||||
checksum: 10c0/5da5ab3e3cd882878ce4336773d5cadb0b636bab40e3c72cd2d41e461b771e8afd6a92bb517e2afe57b83c0d8476634b4d5cedfc3ed41c0713778f71c867697c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-native-nfc-manager@npm:^3.15.1":
|
||||
version: 3.16.1
|
||||
resolution: "react-native-nfc-manager@npm:3.16.1"
|
||||
|
||||
Reference in New Issue
Block a user