129 lines
3.6 KiB
TypeScript
129 lines
3.6 KiB
TypeScript
import React, { useState } from 'react';
|
|
import {
|
|
ActivityIndicator,
|
|
KeyboardAvoidingView,
|
|
Platform,
|
|
Pressable,
|
|
StyleSheet,
|
|
Text,
|
|
View,
|
|
} from 'react-native';
|
|
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
|
|
import { authApi } from '../api/auth';
|
|
import type { RootStackParamList } from '../navigation/types';
|
|
import { useAuth } from '../context/AuthContext';
|
|
import { colors, radii, spacing, typography } from '../theme';
|
|
import { Card, GradientHeader, PrimaryButton, TextField } from '../components/ui';
|
|
|
|
type Props = NativeStackScreenProps<RootStackParamList, 'Login2FA'>;
|
|
|
|
export function Login2FAScreen({ navigation, route }: Props) {
|
|
const { refreshUser } = useAuth();
|
|
const { twoFactorToken, email } = route.params;
|
|
const [code, setCode] = useState('');
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
const submit = async () => {
|
|
setError(null);
|
|
if (!code.trim()) {
|
|
setError('Enter your authenticator code');
|
|
return;
|
|
}
|
|
setLoading(true);
|
|
const res = await authApi.login2fa(twoFactorToken, code.trim());
|
|
setLoading(false);
|
|
if (res.error) {
|
|
setError(res.error);
|
|
return;
|
|
}
|
|
await refreshUser();
|
|
navigation.replace('Events');
|
|
};
|
|
|
|
return (
|
|
<KeyboardAvoidingView
|
|
style={styles.wrap}
|
|
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
|
>
|
|
<View style={styles.inner}>
|
|
<GradientHeader
|
|
title="Two-factor"
|
|
subtitle={`Enter the code for ${email}`}
|
|
style={styles.hero}
|
|
/>
|
|
|
|
<Card style={styles.sheet} padded={false}>
|
|
<View style={styles.sheetInner}>
|
|
{error ? (
|
|
<View style={styles.errorBox}>
|
|
<Text style={styles.errorText}>{error}</Text>
|
|
</View>
|
|
) : null}
|
|
|
|
<TextField
|
|
label="Authenticator code"
|
|
value={code}
|
|
onChangeText={setCode}
|
|
keyboardType="number-pad"
|
|
autoComplete="one-time-code"
|
|
placeholder="6-digit code"
|
|
style={styles.codeInput}
|
|
/>
|
|
|
|
<View style={styles.actions}>
|
|
<PrimaryButton title={loading ? 'Verifying…' : 'Verify'} loading={loading} onPress={submit} />
|
|
<Pressable onPress={() => navigation.goBack()} style={styles.back}>
|
|
<Text style={styles.backText}>Back to login</Text>
|
|
</Pressable>
|
|
</View>
|
|
</View>
|
|
</Card>
|
|
</View>
|
|
</KeyboardAvoidingView>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
wrap: { flex: 1, backgroundColor: colors.background },
|
|
inner: { flex: 1 },
|
|
hero: {
|
|
paddingTop: 72,
|
|
paddingBottom: 44,
|
|
paddingHorizontal: spacing.xl,
|
|
},
|
|
sheet: {
|
|
flex: 1,
|
|
marginTop: -28,
|
|
borderTopLeftRadius: radii.xl,
|
|
borderTopRightRadius: radii.xl,
|
|
borderBottomLeftRadius: 0,
|
|
borderBottomRightRadius: 0,
|
|
borderWidth: 0,
|
|
},
|
|
sheetInner: {
|
|
flex: 1,
|
|
paddingHorizontal: spacing.xl,
|
|
paddingTop: spacing.xl,
|
|
paddingBottom: spacing.xl,
|
|
},
|
|
errorBox: {
|
|
backgroundColor: 'rgba(220,38,38,0.08)',
|
|
borderWidth: 1,
|
|
borderColor: 'rgba(220,38,38,0.18)',
|
|
borderRadius: radii.md,
|
|
paddingHorizontal: 14,
|
|
paddingVertical: 12,
|
|
marginBottom: 14,
|
|
},
|
|
errorText: { ...typography.body, color: colors.error },
|
|
codeInput: {
|
|
fontSize: 20,
|
|
letterSpacing: 4,
|
|
textAlign: 'center',
|
|
},
|
|
actions: { marginTop: 18, gap: 14 },
|
|
back: { alignItems: 'center' },
|
|
backText: { color: colors.primaryDark, ...typography.bodyEm },
|
|
});
|