From 63ab8b982e938f7c9ebf57f617311a956d52e322 Mon Sep 17 00:00:00 2001 From: pvicens <112896251+pvicensSpacedev@users.noreply.github.com> Date: Thu, 6 Jun 2024 15:23:57 -0300 Subject: [PATCH] 2fa code boxes input (#2468) * add 2fa code boxes * change paste code button style --- .../src/screens/Login/Onboarding2FAInput.tsx | 122 +++++++++++++++ .../src/screens/Login/Onboarding2FAScreen.tsx | 147 +++++++++--------- 2 files changed, 198 insertions(+), 71 deletions(-) create mode 100644 apps/mobile/src/screens/Login/Onboarding2FAInput.tsx diff --git a/apps/mobile/src/screens/Login/Onboarding2FAInput.tsx b/apps/mobile/src/screens/Login/Onboarding2FAInput.tsx new file mode 100644 index 000000000..6b091d723 --- /dev/null +++ b/apps/mobile/src/screens/Login/Onboarding2FAInput.tsx @@ -0,0 +1,122 @@ +import Clipboard from '@react-native-clipboard/clipboard'; +import React, { useRef } from 'react'; +import { + Alert, + NativeSyntheticEvent, + StyleSheet, + TextInput, + TextInputKeyPressEventData, + View, +} from 'react-native'; + +import { GalleryTouchableOpacity } from '~/components/GalleryTouchableOpacity'; +import { Typography } from '~/components/Typography'; + +type Onboarding2FAInputProps = { + code: string[]; + setCode: React.Dispatch>; + onCodeComplete: (code: string) => void; +}; + +const Onboarding2FAInput: React.FC = ({ + code, + setCode, + onCodeComplete, +}) => { + const inputs = useRef<(TextInput | null)[]>([]); + + const handleChange = (text: string, index: number) => { + const newCode = [...code]; + newCode[index] = text; + setCode(newCode); + + if (text && index < 5) { + inputs.current[index + 1]?.focus(); + } + + if (newCode.join('').length === 6) { + onCodeComplete(newCode.join('')); + } + }; + + const handleKeyPress = (e: NativeSyntheticEvent, index: number) => { + if (e.nativeEvent.key === 'Backspace' && index > 0 && !code[index]) { + inputs.current[index - 1]?.focus(); + } + }; + + const handlePaste = async () => { + const clipboardContent = await Clipboard.getString(); + if (clipboardContent.length === 6) { + const newCode = clipboardContent.split(''); + setCode(newCode); + + if (inputs.current[5]) { + inputs.current[5]?.focus(); + } + + onCodeComplete(newCode.join('')); + } else { + Alert.alert('Invalid Code', 'Clipboard content is not a 6-digit code.'); + } + }; + + return ( + + + {code.map((digit, index) => ( + handleChange(text, index)} + ref={(ref) => (inputs.current[index] = ref)} + onKeyPress={(e) => handleKeyPress(e, index)} + /> + ))} + + + + Paste Code + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + }, + inputContainer: { + flexDirection: 'row', + justifyContent: 'space-between', + }, + input: { + borderWidth: 1, + borderColor: '#F2F2F2', + margin: 5, + padding: 10, + width: 36, + height: 36, + textAlign: 'center', + fontSize: 18, + }, + middleInput: { + marginLeft: 24, + }, +}); + +export default Onboarding2FAInput; diff --git a/apps/mobile/src/screens/Login/Onboarding2FAScreen.tsx b/apps/mobile/src/screens/Login/Onboarding2FAScreen.tsx index b258b0219..89b3a789b 100644 --- a/apps/mobile/src/screens/Login/Onboarding2FAScreen.tsx +++ b/apps/mobile/src/screens/Login/Onboarding2FAScreen.tsx @@ -11,19 +11,19 @@ import { useLogin } from 'src/hooks/useLogin'; import { BackButton } from '~/components/BackButton'; import { OnboardingProgressBar } from '~/components/Onboarding/OnboardingProgressBar'; -import { OnboardingTextInput } from '~/components/Onboarding/OnboardingTextInput'; import { Typography } from '~/components/Typography'; import { LoginStackNavigatorParamList, LoginStackNavigatorProp } from '~/navigation/types'; import { Button } from '../../components/Button'; import { navigateToNotificationUpsellOrHomeScreen } from './navigateToNotificationUpsellOrHomeScreen'; +import Onboarding2FAInput from './Onboarding2FAInput'; const FALLBACK_ERROR_MESSAGE = `Something unexpected went wrong while logging in. We've been notified and are looking into it`; function InnerOnboardingEmailScreen() { - const [code, setCode] = useState(''); const [isLoggingIn, setIsLoggingIn] = useState(false); const [error, setError] = useState(''); + const [code, setCode] = useState(new Array(6).fill('')); // Lifted code state const navigation = useNavigation(); const route = useRoute>(); @@ -54,73 +54,78 @@ function InnerOnboardingEmailScreen() { [reportError, track] ); - const handleLoginOrCreateUser = useCallback(async () => { - try { - setIsLoggingIn(true); - const privyUser = await loginWithCode({ code, email }); - if (!privyUser) { - throw new Error('No email found; code may be invalid. Restart and try again.'); - } + const handleLoginOrCreateUser = useCallback( + async (inputCode?: string): Promise => { + const codeString = inputCode || code.join(''); + if (codeString.length !== 6) return; + try { + setIsLoggingIn(true); + const privyUser = await loginWithCode({ code: codeString, email }); + if (!privyUser) { + throw new Error('No email found; code may be invalid. Restart and try again.'); + } - const token = await getAccessToken(); - if (!token) { - throw new Error('no access token for privy user'); - } - const result = await login({ privy: { token } }); - - // If user not found, create a new user - if (result.kind !== 'success') { - if (authMethod === 'Privy') { - // create an embedded wallet for users signing up through privy - await embeddedWallet?.create?.({ recoveryMethod: 'privy' }); - - navigation.navigate('OnboardingUsername', { - authMethod: 'Privy', - authMechanism: { - authMechanismType: 'privy', - privyToken: token, - }, - email, - }); + const token = await getAccessToken(); + if (!token) { + throw new Error('no access token for privy user'); + } + const result = await login({ privy: { token } }); + + // If user not found, create a new user + if (result.kind !== 'success') { + if (authMethod === 'Privy') { + // create an embedded wallet for users signing up through privy + await embeddedWallet?.create?.({ recoveryMethod: 'privy' }); + + navigation.navigate('OnboardingUsername', { + authMethod: 'Privy', + authMechanism: { + authMechanismType: 'privy', + privyToken: token, + }, + email, + }); + } + if (authMethod === 'Wallet' || authMethod === 'Farcaster') { + navigation.navigate('OnboardingUsername', { + authMethod, + authMechanism: { + ...authMechanism, + privyToken: token, + }, + email, + }); + } + } else { + // otherwise, log them in + track('Sign In Success', { 'Sign in method': 'Email' }); + await navigateToNotificationUpsellOrHomeScreen(navigation); } - if (authMethod === 'Wallet' || authMethod === 'Farcaster') { - navigation.navigate('OnboardingUsername', { - authMethod, - authMechanism: { - ...authMechanism, - privyToken: token, - }, - email, + } catch (error) { + if (error instanceof Error) { + handleLoginError({ + message: error.message || FALLBACK_ERROR_MESSAGE, + underlyingError: error as Error, }); } - } else { - // otherwise, log them in - track('Sign In Success', { 'Sign in method': 'Email' }); - await navigateToNotificationUpsellOrHomeScreen(navigation); - } - } catch (error) { - if (error instanceof Error) { - handleLoginError({ - message: error.message || FALLBACK_ERROR_MESSAGE, - underlyingError: error as Error, - }); + } finally { + setIsLoggingIn(false); } - } finally { - setIsLoggingIn(false); - } - }, [ - authMechanism, - authMethod, - code, - email, - embeddedWallet, - getAccessToken, - handleLoginError, - login, - loginWithCode, - navigation, - track, - ]); + }, + [ + authMechanism, + authMethod, + email, + embeddedWallet, + getAccessToken, + handleLoginError, + login, + loginWithCode, + navigation, + track, + code, + ] + ); const handleBack = useCallback(() => { navigation.goBack(); @@ -168,12 +173,10 @@ function InnerOnboardingEmailScreen() { - setCode(e.nativeEvent.text)} + @@ -184,7 +187,9 @@ function InnerOnboardingEmailScreen() { eventContext={contexts.Onboarding} className="w-full" text="Next" - onPress={handleLoginOrCreateUser} + onPress={() => { + handleLoginOrCreateUser(); + }} loading={isLoggingIn} disabled={isLoggingIn} />