Skip to content

Commit 0f1a9db

Browse files
committed
fix: import srp seed grid and focus
1 parent 4ff917a commit 0f1a9db

File tree

1 file changed

+146
-95
lines changed
  • app/components/Views/ImportFromSecretRecoveryPhrase

1 file changed

+146
-95
lines changed

app/components/Views/ImportFromSecretRecoveryPhrase/index.js

Lines changed: 146 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import {
1414
Platform,
1515
FlatList,
1616
TouchableOpacity,
17-
Keyboard,
1817
} from 'react-native';
1918
import { connect } from 'react-redux';
2019
import StorageWrapper from '../../../store/storage-wrapper';
@@ -96,6 +95,7 @@ const PASSCODE_NOT_SET_ERROR = 'Error: Passcode not set.';
9695
const IOS_REJECTED_BIOMETRICS_ERROR =
9796
'Error: The user name or passphrase you entered is not correct.';
9897

98+
const SPACE_CHAR = ' ';
9999
/**
100100
* View where users can set restore their account
101101
* using a secret recovery phrase (SRP)
@@ -131,7 +131,9 @@ const ImportFromSecretRecoveryPhrase = ({
131131
const [hideSeedPhraseInput, setHideSeedPhraseInput] = useState(true);
132132
const [seedPhrase, setSeedPhrase] = useState([]);
133133
const [seedPhraseInputFocusedIndex, setSeedPhraseInputFocusedIndex] =
134-
useState(-1);
134+
useState(0);
135+
const [nextSeedPhraseInputFocusedIndex, setNextSeedPhraseInputFocusedIndex] =
136+
useState(0);
135137
const [showAllSeedPhrase, setShowAllSeedPhrase] = useState(false);
136138
const [currentStep, setCurrentStep] = useState(0);
137139
const [learnMore, setLearnMore] = useState(false);
@@ -160,6 +162,66 @@ const ImportFromSecretRecoveryPhrase = ({
160162
trackOnboarding(eventBuilder.build());
161163
};
162164

165+
const checkValidSeedWord = useCallback((text) => wordlist.includes(text), []);
166+
167+
const [isAnyWordError, setIsAnyNewWordError] = useState(false);
168+
169+
const handleClear = useCallback(() => {
170+
setSeedPhrase([]);
171+
setShowAllSeedPhrase(false);
172+
setError('');
173+
}, []);
174+
175+
const handleSeedPhraseChange = useCallback(
176+
(text, index) => {
177+
if (error) setError('');
178+
179+
if (text.includes(SPACE_CHAR)) {
180+
const isEndWithSpace = text.at(-1) === SPACE_CHAR;
181+
// handle use pasting multiple words / whole seed phrase separated by spaces
182+
const splitArray = text.trim().split(' ');
183+
184+
if (splitArray.length > 1) {
185+
const isAllValid = splitArray.reduce(
186+
(acc, item) => acc && checkValidSeedWord(item),
187+
true,
188+
);
189+
190+
if (!isAllValid) {
191+
setIsAnyNewWordError(true);
192+
}
193+
}
194+
195+
let totalLength = seedPhrase.length + splitArray.length;
196+
setSeedPhrase((prev) => {
197+
const endSlices = prev.slice(index + 1);
198+
if (endSlices.length === 0 && isEndWithSpace) {
199+
endSlices.push('');
200+
totalLength++;
201+
}
202+
return [...prev.slice(0, index), ...splitArray, ...endSlices];
203+
// input the array into the correct index
204+
});
205+
206+
setNextSeedPhraseInputFocusedIndex(totalLength - 1);
207+
} else {
208+
setSeedPhrase((prev) => {
209+
// update the word at the correct index
210+
const newSeedPhrase = [...prev];
211+
newSeedPhrase[index] = text.trim();
212+
return newSeedPhrase;
213+
});
214+
}
215+
},
216+
[
217+
error,
218+
seedPhrase,
219+
setSeedPhrase,
220+
setNextSeedPhraseInputFocusedIndex,
221+
checkValidSeedWord,
222+
],
223+
);
224+
163225
const onQrCodePress = useCallback(() => {
164226
let shouldHideSRP = true;
165227
if (!hideSeedPhraseInput) {
@@ -172,7 +234,8 @@ const ImportFromSecretRecoveryPhrase = ({
172234
disableTabber: true,
173235
onScanSuccess: ({ seed = undefined }) => {
174236
if (seed) {
175-
setSeedPhrase(seed);
237+
handleClear();
238+
handleSeedPhraseChange(seed, 0);
176239
} else {
177240
Alert.alert(
178241
strings('import_from_seed.invalid_qr_code_title'),
@@ -185,7 +248,7 @@ const ImportFromSecretRecoveryPhrase = ({
185248
setHideSeedPhraseInput(shouldHideSRP);
186249
},
187250
});
188-
}, [hideSeedPhraseInput, navigation]);
251+
}, [hideSeedPhraseInput, navigation, handleClear, handleSeedPhraseChange]);
189252

190253
const onBackPress = () => {
191254
if (currentStep === 0) {
@@ -359,88 +422,32 @@ const ImportFromSecretRecoveryPhrase = ({
359422

360423
const passwordStrengthWord = getPasswordStrengthWord(passwordStrength);
361424

362-
const handleSeedPhraseChange = (text, index) => {
363-
setError('');
364-
if (text.includes(' ')) {
365-
setSeedPhrase((prev) => {
366-
// handle use pasting multiple words / whole seed phrase separated by spaces
367-
const splitArray = text.trim().split(/\s+/); // split by any spaces
368-
return [
369-
...prev.slice(0, index),
370-
...splitArray,
371-
...prev.slice(index + 1),
372-
]; // input the array into the correct index
373-
});
374-
} else {
375-
setSeedPhrase((prev) => {
376-
// update the word at the correct index
377-
const newSeedPhrase = [...prev];
378-
newSeedPhrase[index] = text.trim();
379-
return newSeedPhrase;
380-
});
381-
}
382-
};
383-
384425
const handleKeyPress = (e, index, enterPressed = false) => {
385-
setError('');
386426
const { key } = e.nativeEvent;
387427
if (key === 'Backspace') {
388-
if (index === 0 && seedPhrase.length === 1) {
389-
setSeedPhrase(['']);
390-
} else if (seedPhrase[index] === '') {
428+
if (seedPhrase[index] === '') {
391429
const newData = seedPhrase.filter((_, idx) => idx !== index);
392430
setSeedPhrase(newData);
393-
seedPhraseInputRefs.current[index - 1]?.focus();
394-
} else {
395-
const newData = [...seedPhrase];
396-
newData[index] = '';
397-
setSeedPhrase(newData);
398-
seedPhraseInputRefs.current[index]?.focus();
431+
setNextSeedPhraseInputFocusedIndex(newData.length - 1);
399432
}
400433
return;
401434
}
402-
if (
403-
(key === ' ' || key === 'Enter' || key === 'return' || enterPressed) &&
404-
index === seedPhrase.length - 1 &&
405-
seedPhrase[index] !== ''
406-
) {
407-
setSeedPhrase([...seedPhrase, '']);
408-
seedPhraseInputRefs.current[index + 1]?.focus();
409-
return;
410-
}
411-
if (
412-
(key === ' ' || key === 'Enter' || key === 'return' || enterPressed) &&
413-
seedPhrase[index] !== ''
414-
) {
415-
const firstList = seedPhrase.slice(0, index + 1);
416-
const secondList = seedPhrase.slice(index + 1);
417-
setSeedPhrase([...firstList, ' ', ...secondList]);
418-
seedPhraseInputRefs.current[index + 1]?.focus();
419-
return;
420-
}
421435
};
422436

423-
const handlePaste = async () => {
424-
setError('');
425-
const text = await Clipboard.getString(); // Get copied text
426-
if (text.trim() !== '') {
427-
const pastedData = text.split(' '); // Split by spaces
428-
setSeedPhrase([...pastedData].filter((item) => item !== ''));
429-
Keyboard.dismiss();
430-
seedPhraseInputRefs.current[seedPhrase.length]?.focus();
431-
}
432-
};
437+
const handlePaste = useCallback(
438+
async (focusedIndex) => {
439+
const text = await Clipboard.getString(); // Get copied text
440+
if (text.trim() !== '') {
441+
handleSeedPhraseChange(text, focusedIndex);
442+
}
443+
},
444+
[handleSeedPhraseChange],
445+
);
433446

434447
const toggleShowAllSeedPhrase = () => {
435448
setShowAllSeedPhrase((prev) => !prev);
436449
};
437450

438-
const handleClear = () => {
439-
setSeedPhrase([]);
440-
setShowAllSeedPhrase(false);
441-
setError('');
442-
};
443-
444451
const validateSeedPhrase = () => {
445452
const phrase = seedPhrase.filter((item) => item !== '').join(' ');
446453
const seedPhraseLength = seedPhrase.length;
@@ -610,14 +617,6 @@ const ImportFromSecretRecoveryPhrase = ({
610617
const isError =
611618
password !== '' && confirmPassword !== '' && password !== confirmPassword;
612619

613-
const isValidSeed = (text) => {
614-
const isValid = wordlist.includes(text);
615-
if (!isValid) {
616-
setError(strings('import_from_seed.spellcheck_error'));
617-
}
618-
return isValid;
619-
};
620-
621620
const showWhatIsSeedPhrase = () => {
622621
navigation.navigate(Routes.MODAL.ROOT_MODAL_FLOW, {
623622
screen: Routes.SHEET.SEEDPHRASE_MODAL,
@@ -634,6 +633,57 @@ const ImportFromSecretRecoveryPhrase = ({
634633
});
635634
};
636635

636+
const getSecureWord = useCallback(
637+
(word, index, focusedIndex) => {
638+
const isFocusedWord =
639+
focusedIndex === index &&
640+
focusedIndex !== nextSeedPhraseInputFocusedIndex;
641+
const isValidSeedWord = checkValidSeedWord(word);
642+
643+
return word &&
644+
(showAllSeedPhrase ? false : seedPhraseInputFocusedIndex !== index) &&
645+
isValidSeedWord
646+
? '***'
647+
: word;
648+
},
649+
[
650+
seedPhraseInputFocusedIndex,
651+
nextSeedPhraseInputFocusedIndex,
652+
showAllSeedPhrase,
653+
checkValidSeedWord,
654+
],
655+
);
656+
657+
useEffect(() => {
658+
seedPhraseInputRefs.current[nextSeedPhraseInputFocusedIndex]?.focus();
659+
}, [nextSeedPhraseInputFocusedIndex]);
660+
661+
const handleOnFocus = useCallback(
662+
(index) => {
663+
if (seedPhraseInputFocusedIndex !== index) {
664+
setError('');
665+
const focusOutWord = seedPhrase[seedPhraseInputFocusedIndex];
666+
667+
if (
668+
isAnyWordError ||
669+
(focusOutWord && !checkValidSeedWord(focusOutWord))
670+
) {
671+
setIsAnyNewWordError(false);
672+
setError(strings('import_from_seed.spellcheck_error'));
673+
}
674+
}
675+
setSeedPhraseInputFocusedIndex(index);
676+
},
677+
[
678+
seedPhrase,
679+
seedPhraseInputFocusedIndex,
680+
setError,
681+
isAnyWordError,
682+
checkValidSeedWord,
683+
setSeedPhraseInputFocusedIndex,
684+
],
685+
);
686+
637687
return (
638688
<SafeAreaView style={styles.root}>
639689
<KeyboardAwareScrollView
@@ -679,6 +729,9 @@ const ImportFromSecretRecoveryPhrase = ({
679729
<View style={styles.seedPhraseInnerContainer}>
680730
{seedPhrase.length <= 1 ? (
681731
<TextInput
732+
ref={(ref) => {
733+
seedPhraseInputRefs.current[0] = ref;
734+
}}
682735
textAlignVertical="top"
683736
label={strings('import_from_seed.srp')}
684737
placeholder={strings(
@@ -719,6 +772,9 @@ const ImportFromSecretRecoveryPhrase = ({
719772
]}
720773
>
721774
<TextField
775+
ref={(ref) => {
776+
seedPhraseInputRefs.current[index] = ref;
777+
}}
722778
startAccessory={
723779
<Text
724780
variant={TextVariant.BodyMD}
@@ -728,25 +784,20 @@ const ImportFromSecretRecoveryPhrase = ({
728784
{index + 1}.
729785
</Text>
730786
}
731-
value={
732-
item &&
733-
(showAllSeedPhrase
734-
? false
735-
: seedPhraseInputFocusedIndex !==
736-
index) &&
737-
isValidSeed(item)
738-
? '***'
739-
: item
740-
}
787+
value={getSecureWord(
788+
item,
789+
index,
790+
seedPhraseInputFocusedIndex,
791+
)}
741792
secureTextEntry={
742-
isValidSeed(item) &&
793+
checkValidSeedWord(item) &&
743794
(showAllSeedPhrase
744795
? false
745796
: seedPhraseInputFocusedIndex !== index)
746797
}
747-
onFocus={() =>
748-
setSeedPhraseInputFocusedIndex(index)
749-
}
798+
onFocus={() => {
799+
handleOnFocus(index);
800+
}}
750801
onChangeText={(text) =>
751802
handleSeedPhraseChange(text, index)
752803
}
@@ -766,7 +817,7 @@ const ImportFromSecretRecoveryPhrase = ({
766817
textAlignVertical="center"
767818
showSoftInputOnFocus
768819
blurOnSubmit={false}
769-
isError={!isValidSeed(item)}
820+
isError={!checkValidSeedWord(item)}
770821
autoCapitalize="none"
771822
numberOfLines={1}
772823
/>
@@ -800,7 +851,7 @@ const ImportFromSecretRecoveryPhrase = ({
800851
if (seedPhrase.length > 1) {
801852
handleClear();
802853
} else {
803-
handlePaste();
854+
handlePaste(seedPhraseInputFocusedIndex);
804855
}
805856
}}
806857
width={ButtonWidthTypes.Full}

0 commit comments

Comments
 (0)