Skip to content

Commit

Permalink
add 2fa code boxes
Browse files Browse the repository at this point in the history
  • Loading branch information
pvicensSpacedev committed May 15, 2024
1 parent 5ae8213 commit 7e1c0fe
Show file tree
Hide file tree
Showing 2 changed files with 184 additions and 71 deletions.
108 changes: 108 additions & 0 deletions apps/mobile/src/screens/Login/Onboarding2FAInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import Clipboard from '@react-native-clipboard/clipboard';
import React, { useRef } from 'react';
import {
Alert,
Button,
NativeSyntheticEvent,
StyleSheet,
TextInput,
TextInputKeyPressEventData,
View,
} from 'react-native';

type Onboarding2FAInputProps = {
code: string[];
setCode: React.Dispatch<React.SetStateAction<string[]>>;
onCodeComplete: (code: string) => void;
};

const Onboarding2FAInput: React.FC<Onboarding2FAInputProps> = ({
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<TextInputKeyPressEventData>, 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 (
<View style={styles.container}>
<View style={styles.inputContainer}>
{code.map((digit, index) => (
<TextInput
key={index}
style={[styles.input, index === 3 && styles.middleInput]}
value={digit}
keyboardType="number-pad"
maxLength={1}
onChangeText={(text) => handleChange(text, index)}
ref={(ref) => (inputs.current[index] = ref)}
onKeyPress={(e) => handleKeyPress(e, index)}
/>
))}
</View>
<Button title="Paste Code" onPress={handlePaste} />
</View>
);
};

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;
147 changes: 76 additions & 71 deletions apps/mobile/src/screens/Login/Onboarding2FAScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string[]>(new Array(6).fill('')); // Lifted code state

const navigation = useNavigation<LoginStackNavigatorProp>();
const route = useRoute<RouteProp<LoginStackNavigatorParamList, 'Onboarding2FA'>>();
Expand Down Expand Up @@ -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<void> => {
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();
Expand Down Expand Up @@ -168,12 +173,10 @@ function InnerOnboardingEmailScreen() {
</View>

<View>
<OnboardingTextInput
autoFocus
placeholder="x x x x x x"
keyboardType="number-pad"
value={code}
onChange={(e) => setCode(e.nativeEvent.text)}
<Onboarding2FAInput
code={code}
setCode={setCode}
onCodeComplete={handleLoginOrCreateUser}
/>
</View>

Expand All @@ -184,7 +187,9 @@ function InnerOnboardingEmailScreen() {
eventContext={contexts.Onboarding}
className="w-full"
text="Next"
onPress={handleLoginOrCreateUser}
onPress={() => {
handleLoginOrCreateUser();
}}
loading={isLoggingIn}
disabled={isLoggingIn}
/>
Expand Down

0 comments on commit 7e1c0fe

Please sign in to comment.