208 lines
6.9 KiB
TypeScript

import { useState, useEffect, useRef } from 'react';
import {
View,
Text,
TextInput,
Pressable,
KeyboardAvoidingView,
Platform,
ActivityIndicator,
} from 'react-native';
import { useRouter, useLocalSearchParams } from 'expo-router';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useTranslation } from 'react-i18next';
import { useAuthStore } from '../../stores/auth';
const OTP_LENGTH = 6;
const OTP_INPUT_STYLE = {
fontSize: 20,
fontFamily: 'Nunito_700Bold',
color: '#0a0a0a',
textAlign: 'center' as const,
width: 48,
height: 56,
borderRadius: 12,
};
export default function ConfirmOtpScreen() {
const router = useRouter();
const { t } = useTranslation();
const params = useLocalSearchParams<{ email: string }>();
const email = decodeURIComponent(params.email ?? '');
const { verifyOtp, resendConfirmation } = useAuthStore();
const [digits, setDigits] = useState<string[]>(Array(OTP_LENGTH).fill(''));
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState(false);
const [loading, setLoading] = useState(false);
const [resendCooldown, setResendCooldown] = useState(0);
const inputs = useRef<Array<TextInput | null>>([]);
useEffect(() => {
if (!email) {
router.replace('/signup');
}
}, [email]);
useEffect(() => {
if (resendCooldown <= 0) return;
const t = setInterval(() => {
setResendCooldown((c) => {
if (c <= 1) { clearInterval(t); return 0; }
return c - 1;
});
}, 1000);
return () => clearInterval(t);
}, [resendCooldown]);
const otp = digits.join('');
const handleDigit = (value: string, index: number) => {
if (value.length === OTP_LENGTH && /^\d+$/.test(value)) {
const next = value.split('');
setDigits(next);
inputs.current[OTP_LENGTH - 1]?.focus();
return;
}
const digit = value.replace(/\D/g, '').slice(-1);
const next = [...digits];
next[index] = digit;
setDigits(next);
if (digit && index < OTP_LENGTH - 1) {
inputs.current[index + 1]?.focus();
}
};
const handleKeyPress = (key: string, index: number) => {
if (key === 'Backspace' && !digits[index] && index > 0) {
inputs.current[index - 1]?.focus();
}
};
const verify = async () => {
if (otp.length < OTP_LENGTH || loading || success) return;
setError(null);
setLoading(true);
const res = await verifyOtp(email, otp);
setLoading(false);
if (res.error) {
setError(res.error);
setDigits(Array(OTP_LENGTH).fill(''));
inputs.current[0]?.focus();
return;
}
setSuccess(true);
router.replace('/(app)');
};
const resend = async () => {
if (resendCooldown > 0) return;
setError(null);
const res = await resendConfirmation(email);
if (res.error) {
setError(res.error);
return;
}
setResendCooldown(60);
};
return (
<SafeAreaView className="flex-1 bg-white">
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
className="flex-1"
>
<View className="flex-1 px-6 justify-center">
{/* Header */}
<View className="items-center mb-8">
<View className="w-16 h-16 bg-rebreak-500/10 rounded-full items-center justify-center mb-4">
<Text className="text-3xl" style={{ fontFamily: 'Nunito_400Regular' }}>&#x2709;</Text>
</View>
<Text className="text-2xl text-neutral-900 text-center mb-2" style={{ fontFamily: 'Nunito_700Bold' }}>
{t('auth.confirmEmailTitle')}
</Text>
<Text className="text-sm text-neutral-500 text-center leading-5" style={{ fontFamily: 'Nunito_400Regular' }}>
{t('auth.confirmEmailLine1')}{'\n'}
<Text className="text-neutral-900" style={{ fontFamily: 'Nunito_600SemiBold' }}>{email}</Text>
{t('auth.confirmEmailLine2') ? `\n${t('auth.confirmEmailLine2')}` : ''}
</Text>
</View>
{/* OTP Input */}
<View className="flex-row justify-center gap-3 mb-6">
{digits.map((digit, index) => (
<TextInput
key={index}
ref={(ref) => { inputs.current[index] = ref; }}
style={[
OTP_INPUT_STYLE,
{
backgroundColor: '#f5f5f5',
borderWidth: 2,
borderColor: digit ? '#f59e0b' : '#e5e5e5',
},
]}
value={digit}
onChangeText={(val) => handleDigit(val, index)}
onKeyPress={({ nativeEvent }) => handleKeyPress(nativeEvent.key, index)}
keyboardType="number-pad"
maxLength={OTP_LENGTH}
editable={!loading && !success}
selectTextOnFocus
/>
))}
</View>
{error && (
<View className="bg-red-50 border border-red-200 rounded-xl px-4 py-3 mb-4">
<Text className="text-red-600 text-sm text-center" style={{ fontFamily: 'Nunito_400Regular' }}>{error}</Text>
</View>
)}
{success && (
<View className="bg-green-50 border border-green-200 rounded-xl px-4 py-3 mb-4">
<Text className="text-green-700 text-sm text-center" style={{ fontFamily: 'Nunito_400Regular' }}>{t('auth.confirmed')}</Text>
</View>
)}
<Pressable
onPress={verify}
disabled={otp.length < OTP_LENGTH || loading || success}
className="bg-rebreak-500 rounded-xl items-center mb-4 active:opacity-80 disabled:opacity-40"
style={{ paddingVertical: 16 }}
>
{loading ? (
<ActivityIndicator color="white" size="small" />
) : (
<Text className="text-white text-base" style={{ fontFamily: 'Nunito_600SemiBold' }}>{t('auth.confirmBtn')}</Text>
)}
</Pressable>
<Pressable
onPress={resend}
disabled={resendCooldown > 0 || loading}
className="py-3 items-center"
>
<Text className="text-neutral-500 text-sm" style={{ fontFamily: 'Nunito_400Regular' }}>
{t('auth.noCode')}{' '}
<Text className={resendCooldown > 0 ? 'text-neutral-400' : 'text-rebreak-500'} style={{ fontFamily: 'Nunito_600SemiBold' }}>
{resendCooldown > 0 ? t('auth.resendCooldown', { seconds: resendCooldown }) : t('auth.resend')}
</Text>
</Text>
</Pressable>
<Pressable
onPress={() => router.back()}
className="py-3 items-center mt-2"
>
<Text className="text-neutral-400 text-sm" style={{ fontFamily: 'Nunito_400Regular' }}>{t('auth.backToSignup')}</Text>
</Pressable>
</View>
</KeyboardAvoidingView>
</SafeAreaView>
);
}