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 <agent-pm@self.xyz>
This commit is contained in:
turnoffthiscomputer
2026-04-17 22:36:07 +02:00
committed by GitHub
parent e2daa97151
commit 043b0558fa

View File

@@ -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<RecoveryError, string> = {
invalid_mnemonic:
'That doesnt look like a valid recovery phrase. Make sure all 24 words are correct and in the right order.',
restore_failed:
'We couldnt restore your account with this phrase. Please double-check and try again.',
not_registered:
'This recovery phrase doesnt 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<NativeStackNavigationProp<RootStackParamList>>();
@@ -47,20 +64,31 @@ const RecoverWithPhraseScreen: React.FC = () => {
const { trackEvent } = useSelfClient();
const [mnemonic, setMnemonic] = useState<string>();
const [restoring, setRestoring] = useState(false);
const [error, setError] = useState<RecoveryError | null>(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 = () => {
</Description>
<View width="100%" position="relative">
<TextArea
borderColor={slate600}
borderColor={error ? red500 : slate600}
backgroundColor={slate700}
color={slate400}
borderWidth="$1"
@@ -174,23 +205,22 @@ const RecoverWithPhraseScreen: React.FC = () => {
onKeyPress={key =>
key.nativeEvent.key === 'Enter' && mnemonic && Keyboard.dismiss()
}
onChangeText={setMnemonic}
onChangeText={onChangeText}
/>
<XStack
gap="$2"
position="absolute"
bottom={0}
width="100%"
alignItems="flex-end"
justifyContent="center"
paddingBottom="$4"
<Pressable
onPress={onPaste}
style={styles.pasteButton}
hitSlop={{ top: 10, bottom: 10, left: 20, right: 20 }}
>
<Paste color={white} height={20} width={20} />
<Text style={styles.pasteText}>{recoveryCopy.phrase.paste}</Text>
</XStack>
<XStack gap="$2" alignItems="center" justifyContent="center">
<Paste color={white} height={20} width={20} />
<Text style={styles.pasteText}>{recoveryCopy.phrase.paste}</Text>
</XStack>
</Pressable>
</View>
{error && <Text style={styles.errorText}>{ERROR_MESSAGES[error]}</Text>}
<SecondaryButton
disabled={!mnemonic || restoring}
onPress={restoreAccount}
@@ -211,9 +241,22 @@ const styles = StyleSheet.create({
backgroundColor: black,
height: '100%',
},
pasteButton: {
position: 'absolute',
bottom: 0,
width: '100%',
alignItems: 'center',
paddingBottom: 16,
},
pasteText: {
lineHeight: 20,
fontSize: 15,
color: white,
},
errorText: {
color: red500,
fontSize: 14,
textAlign: 'center',
paddingHorizontal: 20,
},
});