import React, { useState, useRef, useEffect, useCallback } from 'react'; import { View, Text, StyleSheet, KeyboardAvoidingView, Platform, ScrollView, TouchableOpacity, TextInput, ActivityIndicator, Keyboard, } from 'react-native'; import { router, useLocalSearchParams } from 'expo-router'; import { Ionicons } from '@expo/vector-icons'; import { useAuth } from '@/contexts/AuthContext'; import { Button } from '@/components/ui/Button'; import { ErrorMessage } from '@/components/ui/ErrorMessage'; import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme'; import { useNavigationFlow } from '@/hooks/useNavigationFlow'; import { api } from '@/services/api'; const CODE_LENGTH = 6; export default function VerifyOTPScreen() { // Params from previous screens const params = useLocalSearchParams<{ email: string; skipOtp: string; inviteCode: string; isNewUser: string; }>(); const email = params.email || ''; const skipOtp = params.skipOtp === '1'; const inviteCode = params.inviteCode || ''; const isNewUser = params.isNewUser === '1'; // Auth context const { verifyOtp, requestOtp, isLoading: authLoading, error: authError, clearError } = useAuth(); const nav = useNavigationFlow(); // Local state const [code, setCode] = useState(''); const [verifying, setVerifying] = useState(false); const [resending, setResending] = useState(false); const [localError, setLocalError] = useState(null); const [resendCooldown, setResendCooldown] = useState(0); // Refs const inputRef = useRef(null); const hasAutoLoggedIn = useRef(false); // Clear errors on mount useEffect(() => { clearError(); }, []); // Navigate after successful verification const navigateAfterSuccess = useCallback(async () => { try { // SIMPLIFIED FLOW: // - Login (existing user) → go straight to beneficiaries // - Registration (new user) → follow onboarding flow if (!isNewUser) { router.replace('/(tabs)'); return; } // New user registration flow const profileResponse = await api.getProfile(); if (!profileResponse.ok || !profileResponse.data) { throw new Error(profileResponse.error?.message || 'Failed to load profile'); } const beneficiariesResponse = await api.getAllBeneficiaries(); if (!beneficiariesResponse.ok) { throw new Error(beneficiariesResponse.error?.message || 'Failed to load beneficiaries'); } // /auth/me returns { user: {...}, beneficiaries: [...] } // We need to extract user data from the nested 'user' object const userData = profileResponse.data.user || profileResponse.data; const profile = { id: userData.id, email: userData.email, firstName: userData.firstName, lastName: userData.lastName, phone: userData.phone, }; const result = nav.controller.getRouteAfterLogin( profile, beneficiariesResponse.data || [] ); if ( result.path === nav.ROUTES.AUTH.ENTER_NAME || result.path === nav.ROUTES.AUTH.ADD_LOVED_ONE ) { result.params = { ...result.params, email, inviteCode }; } nav.navigate(result, true); } catch (error) { setLocalError(error instanceof Error ? error.message : 'Failed to load profile data'); setVerifying(false); } }, [email, inviteCode, nav, isNewUser]); // Auto-login for skipOtp (dev mode) useEffect(() => { if (!skipOtp || !email || hasAutoLoggedIn.current) return; hasAutoLoggedIn.current = true; const autoLogin = async () => { setVerifying(true); const success = await verifyOtp(email, '000000'); if (success) { await navigateAfterSuccess(); } else { setLocalError('Auto-login failed'); setVerifying(false); } }; autoLogin(); }, [skipOtp, email, verifyOtp, navigateAfterSuccess]); // Focus input on mount (if not skipping OTP) useEffect(() => { if (!skipOtp) { setTimeout(() => inputRef.current?.focus(), 100); } }, [skipOtp]); // Resend cooldown timer useEffect(() => { if (resendCooldown > 0) { const timer = setTimeout(() => setResendCooldown(resendCooldown - 1), 1000); return () => clearTimeout(timer); } }, [resendCooldown]); // Handle code input const handleCodeChange = (text: string) => { const digits = text.replace(/\D/g, '').slice(0, CODE_LENGTH); setCode(digits); setLocalError(null); // Auto-submit when complete if (digits.length === CODE_LENGTH) { handleVerify(digits); } }; // Verify OTP const handleVerify = useCallback(async (verifyCode?: string) => { const codeToVerify = verifyCode || code; if (codeToVerify.length !== CODE_LENGTH) { setLocalError('Please enter the 6-digit code'); return; } setVerifying(true); setLocalError(null); const success = await verifyOtp(email, codeToVerify); if (success) { // If user has invite code, try to accept it (silent - don't block flow) if (inviteCode) { await api.acceptInvitation(inviteCode); // Don't block - continue with registration flow regardless of result } await navigateAfterSuccess(); return; } setVerifying(false); setLocalError('Invalid verification code. Please try again.'); setCode(''); setTimeout(() => inputRef.current?.focus(), 100); }, [code, email, verifyOtp, navigateAfterSuccess]); // Resend OTP const handleResend = useCallback(async () => { if (resendCooldown > 0) return; setResending(true); setLocalError(null); const result = await requestOtp(email); setResending(false); if (result.success) { setResendCooldown(60); setCode(''); } else { setLocalError('Failed to resend code'); } }, [email, resendCooldown, requestOtp]); // Go back const handleBack = () => { router.back(); }; // Render code boxes const renderCodeBoxes = () => { return Array.from({ length: CODE_LENGTH }).map((_, i) => { const isActive = i === code.length; const isFilled = i < code.length; return ( {code[i] || ''} ); }); }; // Auto-login loading screen if (skipOtp) { return ( Signing in... ); } const displayError = localError || authError?.message; return ( Check your email We sent a verification code to {email} {displayError && ( { setLocalError(null); clearError(); }} /> )} {/* Code Input Boxes */} inputRef.current?.focus()} > {renderCodeBoxes()} {/* Hidden Input */}