From 043b0558faa788724270fe9c47978b3a2eae1eff Mon Sep 17 00:00:00 2001 From: turnoffthiscomputer <98749896+remicolin@users.noreply.github.com> Date: Fri, 17 Apr 2026 22:36:07 +0200 Subject: [PATCH] Fix recovery phrase screen wonkiness (SELF-2649) (#1983) * Fix recovery phrase screen wonkiness (SELF-2649) - Swap paste XStack to Pressable with hitSlop to fix multiple-tap issue - Add error state + user-facing messages for all failure modes - Dismiss keyboard on Continue press - Clear error on new input * Format RecoverWithPhraseScreen with Prettier * Clear error state at start of restoreAccount --------- Co-authored-by: Agent PM --- .../recovery/RecoverWithPhraseScreen.tsx | 71 +++++++++++++++---- 1 file changed, 57 insertions(+), 14 deletions(-) diff --git a/app/src/screens/account/recovery/RecoverWithPhraseScreen.tsx b/app/src/screens/account/recovery/RecoverWithPhraseScreen.tsx index 45760c2e1..0d399d5c3 100644 --- a/app/src/screens/account/recovery/RecoverWithPhraseScreen.tsx +++ b/app/src/screens/account/recovery/RecoverWithPhraseScreen.tsx @@ -4,7 +4,7 @@ import { ethers } from 'ethers'; import React, { useCallback, useState } from 'react'; -import { Keyboard, StyleSheet } from 'react-native'; +import { Keyboard, Pressable, StyleSheet } from 'react-native'; import { Text, TextArea, View, XStack, YStack } from 'tamagui'; import Clipboard from '@react-native-clipboard/clipboard'; import { useNavigation } from '@react-navigation/native'; @@ -22,6 +22,7 @@ import { import { BackupEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics'; import { black, + red500, slate300, slate400, slate600, @@ -38,6 +39,22 @@ import { } from '@/providers/passportDataProvider'; import { recoveryCopy } from '@/screens/account/recovery/recoveryCopy'; +type RecoveryError = + | 'invalid_mnemonic' + | 'restore_failed' + | 'not_registered' + | 'unexpected_error'; + +const ERROR_MESSAGES: Record = { + invalid_mnemonic: + 'That doesn’t look like a valid recovery phrase. Make sure all 24 words are correct and in the right order.', + restore_failed: + 'We couldn’t restore your account with this phrase. Please double-check and try again.', + not_registered: + 'This recovery phrase doesn’t match a registered ID. If you registered with a different phrase, try that one instead.', + unexpected_error: 'Something went wrong. Please try again.', +}; + const RecoverWithPhraseScreen: React.FC = () => { const navigation = useNavigation>(); @@ -47,20 +64,31 @@ const RecoverWithPhraseScreen: React.FC = () => { const { trackEvent } = useSelfClient(); const [mnemonic, setMnemonic] = useState(); const [restoring, setRestoring] = useState(false); + const [error, setError] = useState(null); + const onPaste = useCallback(async () => { const clipboard = (await Clipboard.getString()).trim(); // bugfix: perform a simple clipboard check; ethers.Mnemonic.isValidMnemonic doesn't work if (clipboard) { setMnemonic(clipboard); + setError(null); Keyboard.dismiss(); } }, []); + const onChangeText = useCallback((text: string) => { + setMnemonic(text); + setError(null); + }, []); + const restoreAccount = useCallback(async () => { + Keyboard.dismiss(); + setError(null); try { setRestoring(true); const slimMnemonic = mnemonic?.trim(); if (!slimMnemonic || !ethers.Mnemonic.isValidMnemonic(slimMnemonic)) { + setError('invalid_mnemonic'); setRestoring(false); return; } @@ -71,6 +99,7 @@ const RecoverWithPhraseScreen: React.FC = () => { trackEvent(BackupEvents.CLOUD_RESTORE_FAILED_AUTH, { mnemonicLength: slimMnemonic.split(' ').length, }); + setError('restore_failed'); setRestoring(false); return; } @@ -121,6 +150,7 @@ const RecoverWithPhraseScreen: React.FC = () => { reason: 'document_not_registered', hasCSCA: !!csca, }); + setError('not_registered'); setRestoring(false); return; } @@ -138,6 +168,7 @@ const RecoverWithPhraseScreen: React.FC = () => { reason: 'unexpected_error', error: error instanceof Error ? error.message : 'unknown', }); + setError('unexpected_error'); setRestoring(false); } }, [ @@ -161,7 +192,7 @@ const RecoverWithPhraseScreen: React.FC = () => {