From 72bb03174810ee4a7b1b49b815a327fe79479ea4 Mon Sep 17 00:00:00 2001 From: Peter Piekarczyk Date: Tue, 27 Jun 2023 18:43:25 -0500 Subject: [PATCH] add/connect wallet flow (#4239) * add/connect wallet * advanced import wallet * mnemonic word adjustments * username input adjustments * updates * cleanup * fix * turn off pssword when biometrics eanbled * undo images --- packages/app-mobile/src/Images.ts | 6 + .../src/components/MnemonicInput.tsx | 131 ++++++ .../src/components/MnemonicInputFields.tsx | 23 +- .../src/components/StyledTextInput.tsx | 93 ++-- .../navigation/AccountSettingsNavigator.tsx | 54 ++- .../src/navigation/LockedNavigator.tsx | 2 +- .../src/navigation/OnboardingNavigator.tsx | 174 ++----- .../src/screens/ImportPrivateKeyScreen.tsx | 69 +-- .../Settings/AddConnectWalletScreen.tsx | 439 ++++++++++++------ .../Unlocked/Settings/PreferencesScreen.tsx | 19 +- .../Settings/components/SettingsMenuList.tsx | 10 +- .../screens/Unlocked/YourAccountScreen.tsx | 13 +- .../tamagui-core/src/components/ListItem.tsx | 11 +- 13 files changed, 657 insertions(+), 387 deletions(-) create mode 100644 packages/app-mobile/src/components/MnemonicInput.tsx diff --git a/packages/app-mobile/src/Images.ts b/packages/app-mobile/src/Images.ts index e2a6d9260..038b7ac09 100644 --- a/packages/app-mobile/src/Images.ts +++ b/packages/app-mobile/src/Images.ts @@ -1,6 +1,12 @@ const Images = { solanaLogo: require("../assets/blockchains/solana.png"), ethereumLogo: require("../assets/blockchains/ethereum.png"), + logoAvalanche: require("./images/logo-avalanche.png"), + logoBsc: require("./images/logo-bsc.png"), + logoCosmos: require("./images/logo-cosmos.png"), + logoEthereum: require("./images/logo-ethereum.png"), + logoPolygon: require("./images/logo-polygon.png"), + logoSolana: require("./images/logo-solana.png"), }; export default Images; diff --git a/packages/app-mobile/src/components/MnemonicInput.tsx b/packages/app-mobile/src/components/MnemonicInput.tsx new file mode 100644 index 000000000..020f322d0 --- /dev/null +++ b/packages/app-mobile/src/components/MnemonicInput.tsx @@ -0,0 +1,131 @@ +import { useCallback, useEffect, useState } from "react"; +import { Alert, Keyboard, Pressable } from "react-native"; + +import { + UI_RPC_METHOD_KEYRING_STORE_MNEMONIC_CREATE, + UI_RPC_METHOD_KEYRING_VALIDATE_MNEMONIC, +} from "@coral-xyz/common"; +import { useBackgroundClient } from "@coral-xyz/recoil"; +import { StyledText, YStack } from "@coral-xyz/tamagui"; + +import { maybeRender } from "~lib/index"; + +import { MnemonicInputFields } from "~src/components/MnemonicInputFields"; +import { CopyButton, PasteButton } from "~src/components/index"; + +type MnemonicInputProps = { + readOnly: boolean; + onComplete: ({ + mnemonic, + isValid, + }: { + mnemonic: string; + isValid: boolean; + }) => void; +}; +export function MnemonicInput({ readOnly, onComplete }: MnemonicInputProps) { + const background = useBackgroundClient(); + const [keyboardStatus, setKeyboardStatus] = useState(""); + + const [mnemonicWords, setMnemonicWords] = useState([ + ...Array(12).fill(""), + ]); + + const mnemonic = mnemonicWords.map((f) => f.trim()).join(" "); + + useEffect(() => { + const showSubscription = Keyboard.addListener("keyboardDidShow", () => { + setKeyboardStatus("shown"); + }); + const hideSubscription = Keyboard.addListener("keyboardDidHide", () => { + setKeyboardStatus("hidden"); + }); + + return () => { + showSubscription.remove(); + hideSubscription.remove(); + }; + }, []); + + const generateRandom = useCallback(() => { + background + .request({ + method: UI_RPC_METHOD_KEYRING_STORE_MNEMONIC_CREATE, + params: [mnemonicWords.length === 12 ? 128 : 256], + }) + .then((m: string) => { + const words = m.split(" "); + setMnemonicWords(words); + }); + }, []); // eslint-disable-line + + useEffect(() => { + if (readOnly) { + generateRandom(); + } + }, [readOnly, generateRandom]); + + const isValidAsync = (mnemonic: string) => { + return background.request({ + method: UI_RPC_METHOD_KEYRING_VALIDATE_MNEMONIC, + params: [mnemonic], + }); + }; + + const onChange = async (words: string[]) => { + setMnemonicWords(words); + if (readOnly) { + const mnemonic = mnemonicWords.map((f) => f.trim()).join(" "); + onComplete({ isValid: true, mnemonic }); + return; + } + + if (words.length > 11) { + const mnemonic = mnemonicWords.map((f) => f.trim()).join(" "); + const isValid = words.length > 11 ? await isValidAsync(mnemonic) : false; + onComplete({ isValid, mnemonic }); + } + }; + + return ( + + { + const isValid = await isValidAsync(mnemonic); + onComplete({ isValid, mnemonic }); + }} + /> + {readOnly ? ( + + ) : keyboardStatus === "shown" ? null : ( + { + const split = words.split(" "); + if ([12, 24].includes(split.length)) { + setMnemonicWords(words.split(" ")); + } else { + Alert.alert("Mnemonic should be either 12 or 24 words"); + } + }} + /> + )} + {maybeRender(!readOnly, () => ( + { + setMnemonicWords([ + ...Array(mnemonicWords.length === 12 ? 24 : 12).fill(""), + ]); + }} + > + + Use a {mnemonicWords.length === 12 ? "24" : "12"}-word recovery + mnemonic + + + ))} + + ); +} diff --git a/packages/app-mobile/src/components/MnemonicInputFields.tsx b/packages/app-mobile/src/components/MnemonicInputFields.tsx index 24f0e8863..f16ca4676 100644 --- a/packages/app-mobile/src/components/MnemonicInputFields.tsx +++ b/packages/app-mobile/src/components/MnemonicInputFields.tsx @@ -12,11 +12,19 @@ type MnemonicWordInputProps = { returnKeyType: "next" | "done"; onChangeText: (word: string) => void; onSubmitEditing: () => void; + onBlur: () => void; }; const _MnemonicWordInput = forwardRef( (props, ref) => { - const { word, index, returnKeyType, onChangeText, onSubmitEditing } = props; + const { + word, + index, + returnKeyType, + onChangeText, + onSubmitEditing, + onBlur, + } = props; const theme = useTheme(); return ( ( spellCheck={false} scrollEnabled={false} onSubmitEditing={onSubmitEditing} + onBlur={onBlur} maxLength={10} value={word} style={[ @@ -103,7 +112,7 @@ export function MnemonicInputFields({ if (next) { next.focus(); } else { - onComplete(); + onComplete?.(); } }, [onComplete] @@ -123,6 +132,11 @@ export function MnemonicInputFields({ index={index} returnKeyType={index === mnemonicWords.length - 1 ? "done" : "next"} onSubmitEditing={selectNextInput(index)} + onBlur={() => { + if (mnemonicWords.length > 11) { + onComplete?.(); + } + }} onChangeText={(word) => { if (onChange) { const newMnemonicWords = [...mnemonicWords]; @@ -133,7 +147,7 @@ export function MnemonicInputFields({ /> ); }, - [mnemonicWords, onChange, selectNextInput] + [mnemonicWords, onChange, selectNextInput, onComplete] ); return ( @@ -141,12 +155,13 @@ export function MnemonicInputFields({ data={mnemonicWords} numColumns={3} initialNumToRender={12} - scrollEnabled={false} + scrollEnabled={mnemonicWords.length > 12} keyExtractor={keyExtractor} contentContainerStyle={{ gap: ITEM_GAP }} columnWrapperStyle={{ gap: ITEM_GAP }} renderItem={renderItem} maxToRenderPerBatch={12} + style={{ maxHeight: 275 }} getItemLayout={(_data, index) => ({ length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, diff --git a/packages/app-mobile/src/components/StyledTextInput.tsx b/packages/app-mobile/src/components/StyledTextInput.tsx index 9e1b42124..9a7e81152 100644 --- a/packages/app-mobile/src/components/StyledTextInput.tsx +++ b/packages/app-mobile/src/components/StyledTextInput.tsx @@ -47,49 +47,64 @@ function Container({ ); } -type UsernameInputProps = { - username: string; - onChange: (username: string) => void; - onComplete: () => void; - showError?: boolean; - disabled?: boolean; -}; +type UsernameInputProps = TextInputProps & + UseControllerProps & { + username: string; + onChange: (username: string) => void; + onComplete: () => void; + showError?: boolean; + disabled?: boolean; + }; + export function UsernameInput({ + onSubmitEditing, + autoFocus, + control, showError, - username, - onChange, - onComplete, - disabled, }: UsernameInputProps): JSX.Element { - const [localError, setLocalError] = useState(false); return ( - - - @ - { - const username = text.toLowerCase().replace(/[^a-z0-9_]/g, ""); - if ( - username !== "" && - (username.length < 4 || username.length > 15) - ) { - setLocalError(true); - } else { - setLocalError(false); - } - onChange(username); - }} - /> - - + ( + + + @ + + + + )} + /> ); } diff --git a/packages/app-mobile/src/navigation/AccountSettingsNavigator.tsx b/packages/app-mobile/src/navigation/AccountSettingsNavigator.tsx index 695b47688..64988ee79 100644 --- a/packages/app-mobile/src/navigation/AccountSettingsNavigator.tsx +++ b/packages/app-mobile/src/navigation/AccountSettingsNavigator.tsx @@ -56,7 +56,13 @@ import { EditWalletDetailScreen } from "~screens/Unlocked/EditWalletDetailScreen import { EditWalletsScreen } from "~screens/Unlocked/EditWalletsScreen"; import { ForgotPasswordScreen } from "~screens/Unlocked/ForgotPasswordScreen"; import { RenameWalletScreen } from "~screens/Unlocked/RenameWalletScreen"; -import { AddConnectWalletScreen } from "~screens/Unlocked/Settings/AddConnectWalletScreen"; +import { + AddWalletPrivacyDisclaimer, + AddWalletSelectBlockchain, + AddWalletCreateOrImportScreen, + AddWalletAdvancedImportScreen, + ImportFromMnemonicScreen, +} from "~screens/Unlocked/Settings/AddConnectWalletScreen"; import { ChangePasswordScreen } from "~screens/Unlocked/Settings/ChangePasswordScreen"; import { PreferencesScreen } from "~screens/Unlocked/Settings/PreferencesScreen"; import { PreferencesTrustedSitesScreen } from "~screens/Unlocked/Settings/PreferencesTrustedSitesScreen"; @@ -97,7 +103,14 @@ type AccountSettingsParamList = { PreferencesSolanaExplorer: undefined; PreferencesSolanaCustomRpcUrl: undefined; PreferencesTrustedSites: undefined; - "import-private-key": undefined; + ImportFromMnemonic: { + blockchain: Blockchain; + keyringExists: boolean; + inputMnemonic: boolean; + }; + ImportPrivateKey: { + blockchain: Blockchain; + }; "reset-warning": undefined; "show-secret-phrase-warning": undefined; "show-secret-phrase": { @@ -122,6 +135,13 @@ type AccountSettingsParamList = { "forgot-password": undefined; "logout-warning": undefined; UserAccountMenu: undefined; + AddWalletPrivacyDisclaimer: undefined; + AddWalletSelectBlockchain: undefined; + AddWalletCreateOrImport: undefined; + AddWalletAdvancedImport: { + publicKey: PublicKey; + blockchain: Blockchain; + }; }; export type EditWalletsScreenProps = StackScreenProps< @@ -238,7 +258,7 @@ export function AccountSettingsNavigator(): JSX.Element { /> ( { - navigation.push("add-wallet"); + navigation.push("AddWalletPrivacyDisclaimer"); }} > + + + + (); + const { control, handleSubmit, setError } = useForm(); const onSubmit = async ({ password }: FormData) => { await maybeUnlock({ password }); diff --git a/packages/app-mobile/src/navigation/OnboardingNavigator.tsx b/packages/app-mobile/src/navigation/OnboardingNavigator.tsx index aecbb6ae5..9bceda3a9 100644 --- a/packages/app-mobile/src/navigation/OnboardingNavigator.tsx +++ b/packages/app-mobile/src/navigation/OnboardingNavigator.tsx @@ -12,8 +12,6 @@ import { View, Platform, KeyboardAvoidingView, - Pressable, - Text, DevSettings, StyleProp, ViewStyle, @@ -33,8 +31,6 @@ import { DISCORD_INVITE_LINK, toTitleCase, TWITTER_LINK, - UI_RPC_METHOD_KEYRING_STORE_MNEMONIC_CREATE, - UI_RPC_METHOD_KEYRING_VALIDATE_MNEMONIC, XNFT_GG_LINK, PrivateKeyWalletDescriptor, } from "@coral-xyz/common"; @@ -78,7 +74,6 @@ import { FullScreenLoading, Header, Margin, - MnemonicInputFields, PrimaryButton, SecondaryButton, LinkButton, @@ -86,8 +81,6 @@ import { StyledText, SubtextParagraph, WelcomeLogoHeader, - CopyButton, - PasteButton, EmptyState, CallToAction, } from "~components/index"; @@ -96,6 +89,7 @@ import { useSession } from "~lib/SessionProvider"; import { maybeRender } from "~lib/index"; import * as Form from "~src/components/Form"; +import { MnemonicInput } from "~src/components/MnemonicInput"; import { BiometricAuthenticationStatus, BIOMETRIC_PASSWORD, @@ -540,22 +534,27 @@ function OnboardingPrivateKeyInputScreen({ ); } +type UsernameData = { + username: string; +}; + function CreateOrRecoverUsernameScreen({ navigation, }: StackScreenProps< OnboardingStackParamList, "CreateOrRecoverUsername" >): JSX.Element { - const [error, setError] = useState(""); + const { control, clearErrors, handleSubmit, setError, formState } = + useForm(); const [loading, setLoading] = useState(false); - const [username, setUsername] = useState(""); const { onboardingData, setOnboardingData } = useOnboarding(); const { action } = onboardingData; // create | recover const screenTitle = action === "create" ? "Claim your username" : "Username recovery"; - const handlePresContinue = async () => { + const onSubmit = async ({ username }: UsernameData) => { + clearErrors("username"); setLoading(true); if (action === "recover") { try { @@ -569,7 +568,7 @@ function CreateOrRecoverUsernameScreen({ navigation.push(RecoverAccountRoutes.KeyringTypeSelector); } catch (err: any) { Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error); - setError(err.message); + setError("username", { message: err.message }); } finally { setLoading(false); } @@ -585,7 +584,7 @@ function CreateOrRecoverUsernameScreen({ setOnboardingData({ username }); navigation.push(NewAccountRoutes.CreateOrImportWallet); } catch (err: any) { - setError(err.message); + setError("username", { message: err.message }); } finally { setLoading(false); } @@ -618,8 +617,6 @@ function CreateOrRecoverUsernameScreen({ ); - const isButtonDisabled = loading || username.length < 4; - return ( {text} - + { - if (!isButtonDisabled) { - handlePresContinue(); - } - }} + control={control} + errorMessage={Boolean(formState.errors?.username)} + onSubmitEditing={handleSubmit(onSubmit)} /> @@ -654,111 +646,40 @@ function CreateOrRecoverUsernameScreen({ function OnboardingMnemonicInputScreen({ navigation, }: StackScreenProps) { - const { onboardingData, setOnboardingData } = useOnboarding(); - const { action } = onboardingData; - const readOnly = action === "create"; - - const background = useBackgroundClient(); - const [mnemonicWords, setMnemonicWords] = useState([ - ...Array(12).fill(""), - ]); - const [error, setError] = useState(); const [checked, setChecked] = useState(false); + const [isValid, setIsValid] = useState(false); - const mnemonic = mnemonicWords.map((f) => f.trim()).join(" "); - // Only enable copy all fields populated - const copyEnabled = mnemonicWords.find((w) => w.length < 3) === undefined; - // Only allow next if checkbox is checked in read only and all fields are populated - const nextEnabled = (!readOnly || checked) && copyEnabled; + const { onboardingData, setOnboardingData } = useOnboarding(); + const { action } = onboardingData; + const readOnly = action === "create"; const subtitle = readOnly ? "This is the only way to recover your account if you lose your device. Write it down and store it in a safe place." : "Enter your 12 or 24-word secret recovery mnemonic to add an existing wallet."; - // - // Generate a random mnemonic and populate state. - // - const generateRandom = useCallback(() => { - background - .request({ - method: UI_RPC_METHOD_KEYRING_STORE_MNEMONIC_CREATE, - params: [mnemonicWords.length === 12 ? 128 : 256], - }) - .then((m: string) => { - const words = m.split(" "); - setMnemonicWords(words); - }); - }, []); // eslint-disable-line - - const next = () => { - background - .request({ - method: UI_RPC_METHOD_KEYRING_VALIDATE_MNEMONIC, - params: [mnemonic], - }) - .then((isValid: boolean) => { - setOnboardingData({ mnemonic }); - const route = - action === "recover" ? "MnemonicSearch" : "SelectBlockchain"; - return isValid - ? navigation.push(route) - : setError("Invalid secret recovery phrase"); - }); + const onComplete = ({ + isValid, + mnemonic, + }: { + isValid: boolean; + mnemonic: string; + }) => { + setIsValid(isValid); + if (isValid) { + setOnboardingData({ mnemonic }); + } }; - useEffect(() => { - if (readOnly) { - generateRandom(); - } - }, [readOnly, generateRandom]); + const isButtonDisabled = + (readOnly && !checked) || (!readOnly && !isValid && !checked); return ( - - - - - {readOnly ? ( - - ) : ( - { - const split = words.split(" "); - if ([12, 24].includes(split.length)) { - setMnemonicWords(words.split(" ")); - } else { - Alert.alert("Mnemonic should be either 12 or 24 words"); - } - }} - /> - )} - - - - - {maybeRender(!readOnly, () => ( - { - setMnemonicWords([ - ...Array(mnemonicWords.length === 12 ? 24 : 12).fill(""), - ]); - }} - > - - Use a {mnemonicWords.length === 12 ? "24" : "12"}-word recovery - mnemonic - - - ))} + + + + + {maybeRender(readOnly, () => ( @@ -767,6 +688,7 @@ function OnboardingMnemonicInputScreen({ value={checked} onPress={() => { setChecked(!checked); + setIsValid(readOnly && !checked); }} /> @@ -776,17 +698,19 @@ function OnboardingMnemonicInputScreen({ ))} - { - setMnemonicWords([...Array(12).fill("")]); + if (isValid) { + const route = + action === "recover" ? "MnemonicSearch" : "SelectBlockchain"; + navigation.push(route); + } else { + setError("Invalid secret recovery phrase"); + } }} /> - + ); } diff --git a/packages/app-mobile/src/screens/ImportPrivateKeyScreen.tsx b/packages/app-mobile/src/screens/ImportPrivateKeyScreen.tsx index 25d911d27..7f4a989ff 100644 --- a/packages/app-mobile/src/screens/ImportPrivateKeyScreen.tsx +++ b/packages/app-mobile/src/screens/ImportPrivateKeyScreen.tsx @@ -1,5 +1,11 @@ import { Alert, View } from "react-native"; +import { UI_RPC_METHOD_KEYRING_IMPORT_SECRET_KEY } from "@coral-xyz/common"; +import { useBackgroundClient, useWalletPublicKeys } from "@coral-xyz/recoil"; +import { Controller, useForm } from "react-hook-form"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; + +import { InputField } from "~components/Form"; import { Header, Margin, @@ -8,11 +14,6 @@ import { StyledTextInput, SubtextParagraph, } from "~components/index"; -import { UI_RPC_METHOD_KEYRING_IMPORT_SECRET_KEY } from "@coral-xyz/common"; -import { useBackgroundClient, useWalletPublicKeys } from "@coral-xyz/recoil"; -import { Controller, useForm } from "react-hook-form"; - -import { InputField } from "~components/Form"; import { validateSecretKey } from "~lib/validateSecretKey"; type PrivateKeyInput = { @@ -22,6 +23,7 @@ type PrivateKeyInput = { export function ImportPrivateKeyScreen({ route }) { const { blockchain } = route.params; + const insets = useSafeAreaInsets(); const background = useBackgroundClient(); const existingPublicKeys = useWalletPublicKeys(); @@ -60,7 +62,7 @@ export function ImportPrivateKeyScreen({ route }) { }; return ( - +
@@ -69,44 +71,23 @@ export function ImportPrivateKeyScreen({ route }) { device. - - ( - - )} - /> - - - ( - - )} - /> - + ( + + )} + /> (null); - const snapPoints = useMemo(() => ["25%"], []); - const modalHeight = 240; - - const handleOpenModal = () => bottomSheetModalRef.current?.present(); - const renderBackdrop = useCallback( - (props: BottomSheetBackdropProps) => ( - - ), - [] - ); +export function AddWalletPrivacyDisclaimer({ navigation }): JSX.Element { + const user = useUser(); + const insets = useSafeAreaInsets(); return ( - - -
- Add new wallets to Backpack - - - {hasMnemonic ? ( - - - } - text="Create a new wallet" - onPress={async () => { - const newPubkey = await background.request({ - method: UI_RPC_METHOD_KEYRING_DERIVE_WALLET, - params: [blockchain], - }); - - await setActiveWallet({ blockchain, publicKey: newPubkey }); - - setNewPublicKey(newPubkey); - handleOpenModal(); - }} - /> - - ) : null} - - - } - text="Import a private key" - onPress={() => - navigation.push("import-private-key", { blockchain }) - } - /> - - - - - } - text="Import from hardware wallet" + + + + + Your new wallet will be associated with @{user.username} + + + This connection will be public, so if you'd prefer to create a + separate identity, create a new account. + + + + { - // openConnectHardware(blockchain); + navigation.push("AddWalletSelectBlockchain"); }} /> - - - - + { + Alert.alert("Create a new account"); + }} + /> + ); } -export const ConfirmCreateWallet = ({ - blockchain, - publicKey, -}: { - blockchain: Blockchain; - publicKey: string; -}): JSX.Element => { - const theme = useTheme(); - const walletName = useWalletName(publicKey); +export function AddWalletSelectBlockchain({ navigation }): JSX.Element { + const menuItems = { + Solana: { + icon: , + onPress: () => { + navigation.push("AddWalletCreateOrImport", { + blockchain: Blockchain.SOLANA, + }); + }, + }, + Ethereum: { + icon: , + onPress: () => { + navigation.push("AddWalletCreateOrImport", { + blockchain: Blockchain.ETHEREUM, + }); + }, + }, + }; return ( - - - - Wallet Created - - - - - - - - - + + + + ); +} + +function CreateNewWalletButton({ blockchain }: { blockchain: Blockchain }) { + const background = useBackgroundClient(); + const { signMessageForWallet } = useRpcRequests(); + const publicKeys = useWalletPublicKeys(); + const keyringExists = publicKeys[blockchain]; + + // If the keyring or if we don't have any public keys of the type we are + // adding then additional logic is required to select the account index of + // the first derivation path added + const hasHdPublicKeys = + publicKeys?.[blockchain]?.["hdPublicKeys"]?.length > 0; + + const [newPublicKey, setNewPublicKey] = useState(""); + const [openDrawer, setOpenDrawer] = useState(false); + const [loading, setLoading] = useState(false); + + // Copied from extension/AddConnectWallet/index + const createNewWithPhrase = async () => { + // Mnemonic based keyring. This is the simple case because we don't + // need to prompt for the user to open their Ledger app to get the + // required public key. We also don't need a signature to prove + // ownership of the public key because that can't be done + // transparently by the backend. + if (loading) { + return; + } + + setOpenDrawer(true); + setLoading(true); + + let newPublicKey; + if (!keyringExists || !hasHdPublicKeys) { + // No keyring or no existing mnemonic public keys so can't derive next + const walletDescriptor = await background.request({ + method: UI_RPC_METHOD_FIND_WALLET_DESCRIPTOR, + params: [blockchain, 0], + }); + + const signature = await signMessageForWallet( + blockchain, + walletDescriptor.publicKey, + getAddMessage(walletDescriptor.publicKey), + { + mnemonic: true, + signedWalletDescriptors: [ + { + ...walletDescriptor, + signature: "", + }, + ], + } + ); + + const signedWalletDescriptor = { ...walletDescriptor, signature }; + if (!keyringExists) { + // Keyring doesn't exist, create it + await background.request({ + method: UI_RPC_METHOD_BLOCKCHAIN_KEYRINGS_ADD, + params: [ + { + mnemonic: true, // Use the existing mnemonic + signedWalletDescriptors: [signedWalletDescriptor], + }, + ], + }); + } else { + // Keyring exists but the hd keyring is not initialised, import + await background.request({ + method: UI_RPC_METHOD_KEYRING_IMPORT_WALLET, + params: [signedWalletDescriptor], + }); + } + + newPublicKey = walletDescriptor.publicKey; + } else { + newPublicKey = await background.request({ + method: UI_RPC_METHOD_KEYRING_DERIVE_WALLET, + params: [blockchain], + }); + } + + setNewPublicKey(newPublicKey); + setLoading(false); + }; + + return ( + <_ListItemOneLine + loading={loading} + title="Create a new wallet" + icon={getIcon("add-circle")} + iconAfter={} + onPress={createNewWithPhrase} + /> + ); +} + +// {openDrawer ? ( +// +// ) : null} + +export function AddWalletCreateOrImportScreen({ + navigation, + route, +}): JSX.Element { + const { blockchain } = route.params; + + const menuItems = { + Advanced: { + label: "Advanced wallet import", + icon: getIcon("arrow-circle-up"), + onPress: () => { + navigation.push("AddWalletAdvancedImport", { + blockchain, + }); + }, + }, + }; + + return ( + + + + + + ); +} + +export function AddWalletAdvancedImportScreen({ navigation, route }) { + const { blockchain, publicKey } = route.params; + const enabledBlockchains = useEnabledBlockchains(); + const keyringExists = enabledBlockchains.includes(blockchain); + + const menuItems = { + "Backpack recovery phrase": { + onPress: () => + navigation.push("ImportFromMnemonic", { + blockchain, + keyringExists, + inputMnemonic: false, + }), + }, + "Other recovery phrase": { + onPress: () => + navigation.push("ImportFromMnemonic", { + blockchain, + keyringExists, + inputMnemonic: true, + }), + }, + "Private key": { + onPress: () => + navigation.push("ImportPrivateKey", { + blockchain, + }), + }, + }; + + return ( + + + ); -}; +} + +export function ImportFromMnemonicScreen({ navigation, route }): JSX.Element { + const insets = useSafeAreaInsets(); + const { blockchain, keyringExists, inputMnemonic } = route.params; + const [isValid, setIsValid] = useState(false); + + const onComplete = ({ + isValid, + mnemonic, + }: { + isValid: boolean; + mnemonic: string; + }) => { + setIsValid(isValid); + if (isValid) { + console.log(mnemonic); + } + }; + + return ( + + + +
+ + Enter your 12 or 24-word secret recovery mnemonic to add an existing + wallet. + + + + + + { + // if (isValid) { + // const route = + // action === "recover" ? "MnemonicSearch" : "SelectBlockchain"; + // navigation.push(route); + // } else { + // setError("Invalid secret recovery phrase"); + // } + }} + /> + + ); +} diff --git a/packages/app-mobile/src/screens/Unlocked/Settings/PreferencesScreen.tsx b/packages/app-mobile/src/screens/Unlocked/Settings/PreferencesScreen.tsx index 2c90f7e12..54c2b96e6 100644 --- a/packages/app-mobile/src/screens/Unlocked/Settings/PreferencesScreen.tsx +++ b/packages/app-mobile/src/screens/Unlocked/Settings/PreferencesScreen.tsx @@ -31,6 +31,7 @@ function SettingsBiometricsMode() { const { biometricName } = useDeviceSupportsBiometricAuth(); const isSupported = useDeviceSupportsBiometricAuth(); const isEnabled = useOsBiometricAuthEnabled(); + if (!isSupported) { return null; } @@ -66,12 +67,14 @@ function SettingsBiometricsMode() { }; return ( - + + + ); } @@ -96,9 +99,7 @@ function Container({ navigation }) { return ( - - - + ; textStyle?: StyleProp; borderColor?: string; + children?: React.ReactNode; menuItems: { [key: string]: { onPress: () => void; @@ -20,7 +19,9 @@ export function SettingsList({ label?: string; }; }; -}) { +}; + +export function SettingsList({ menuItems, children }: SettingsListProps) { return ( } > + {children} {Object.entries(menuItems).map( ([key, { onPress, detail, icon, label }]) => ( diff --git a/packages/app-mobile/src/screens/Unlocked/YourAccountScreen.tsx b/packages/app-mobile/src/screens/Unlocked/YourAccountScreen.tsx index 88c9c16da..06a6176de 100644 --- a/packages/app-mobile/src/screens/Unlocked/YourAccountScreen.tsx +++ b/packages/app-mobile/src/screens/Unlocked/YourAccountScreen.tsx @@ -3,13 +3,20 @@ import { useKeyringHasMnemonic } from "@coral-xyz/recoil"; import { Screen } from "~components/index"; import { SettingsList } from "~screens/Unlocked/Settings/components/SettingsMenuList"; +import { useOsBiometricAuthEnabled } from "~src/features/biometrics/hooks"; + export function YourAccountScreen({ navigation }): JSX.Element { const hasMnemonic = useKeyringHasMnemonic(); + const isBiometricsEnabled = useOsBiometricAuthEnabled(); const menuItems = { - "Change Password": { - onPress: () => navigation.push("change-password"), - }, + ...(!isBiometricsEnabled + ? { + "Change Password": { + onPress: () => navigation.push("change-password"), + }, + } + : {}), ...(hasMnemonic ? { "Show Secret Recovery Phrase": { diff --git a/packages/tamagui-core/src/components/ListItem.tsx b/packages/tamagui-core/src/components/ListItem.tsx index f7cd7c9ab..1ce1a8100 100644 --- a/packages/tamagui-core/src/components/ListItem.tsx +++ b/packages/tamagui-core/src/components/ListItem.tsx @@ -1,4 +1,5 @@ import { + ActivityIndicator, FlatList, Image, Pressable, @@ -531,12 +532,16 @@ export function ListItemFriendRequest({ } export function _ListItemOneLine({ + loading, + disabled, icon, title, rightText, iconAfter, onPress, }: { + disabled?: boolean; + loading?: boolean; icon: JSX.Element | null; title: string; rightText?: string; @@ -546,7 +551,7 @@ export function _ListItemOneLine({ return ( {icon ? ( - + {icon} ) : null} @@ -566,7 +571,7 @@ export function _ListItemOneLine({ {rightText ? {rightText} : null} - {iconAfter} + {loading ? : iconAfter}