import React, { useState, useEffect, useRef } from 'react'; import { View, Text, StyleSheet, TextInput, Animated, Platform, KeyboardAvoidingView, ToastAndroid, TouchableOpacity, } from 'react-native'; import { useNavigation, useRoute, RouteProp, CommonActions } from '@react-navigation/native'; import { responsiveFontSize } from 'react-native-responsive-dimensions'; import * as Keychain from 'react-native-keychain'; import { RootStackParamList } from '../navigation/AppNavigator'; import { useTheme } from '../hooks/ui/useTheme'; import { useCreate } from '../hooks/core/useCreate'; import { useFetch } from '../hooks/core/useFetch'; import { LOGIN_URL, VARIFY_OTP_URL } from '../utils/ApiUrls'; import { getFcmToken } from '../utils/firebase/firebaseServices'; import { CustomButton } from '../components/common/Custom'; import AppView from '../components/AppView'; import FontsFamily from '../constents/FontsFamily'; const CELL_COUNT = 6; type OtpVerifyRouteProp = RouteProp; export default function OtpVerificationScreen() { const theme = useTheme(); const navigation = useNavigation(); const route = useRoute(); const { mobile } = route.params; const [code, setCode] = useState(Array(CELL_COUNT).fill('')); const [timer, setTimer] = useState(30); const inputs = useRef>([]); const fadeAnim = useRef(new Animated.Value(0)).current; const slideAnim = useRef(new Animated.Value(20)).current; useEffect(() => { Animated.parallel([ Animated.timing(fadeAnim, { toValue: 1, duration: 800, useNativeDriver: true, }), Animated.spring(slideAnim, { toValue: 0, tension: 20, useNativeDriver: true, }), ]).start(); const interval = setInterval(() => { setTimer((prev) => (prev > 0 ? prev - 1 : 0)); }, 1000); return () => clearInterval(interval); }, [fadeAnim, slideAnim]); const { mutate: verifyOtp, isPending: verifying } = useCreate(VARIFY_OTP_URL, { onSuccess: async (res) => { if (res?.statusCode === 200) { const { jwt, userId } = res?.data || {}; await Keychain.setGenericPassword('auth', jwt, { accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED, }); await getFcmToken(userId); ToastAndroid.show('Verified successfully', ToastAndroid.SHORT); navigation.dispatch( CommonActions.reset({ index: 0, routes: [{ name: 'Home' }], }) ); } else { ToastAndroid.show(res?.message || 'Invalid OTP', ToastAndroid.SHORT); } }, onError: () => { ToastAndroid.show('Verification failed. Please try again.', ToastAndroid.SHORT); }, }); const { refetch: resendOtp, isFetching: resending } = useFetch( LOGIN_URL + mobile, {}, { enabled: false, retry: false, } ); const handleResendOtp = async () => { try { const { data } = await resendOtp(); if (data?.statusCode === 200) { setTimer(30); setCode(Array(CELL_COUNT).fill('')); inputs.current[0]?.focus(); ToastAndroid.show('OTP Resent', ToastAndroid.SHORT); } else { ToastAndroid.show(data?.message || 'Failed to send OTP. Please try again.', ToastAndroid.SHORT); } } catch { ToastAndroid.show('Something went wrong. Please check your connection.', ToastAndroid.SHORT); } }; const handleTextChange = (text: string, index: number) => { // If text length > 1, it's likely a paste operation if (text.length > 1) { const pastedCode = text.trim().slice(0, CELL_COUNT).split(''); const newCode = [...code]; pastedCode.forEach((char, i) => { if (i < CELL_COUNT) { newCode[i] = char; } }); setCode(newCode); // Focus the last filled cell or the next empty one const focusIndex = Math.min(pastedCode.length, CELL_COUNT - 1); inputs.current[focusIndex]?.focus(); // Trigger verification if OTP complete if (newCode.every((cell) => cell !== '')) { verifyOtp({ mobile, otp: newCode.join('') }); } return; } const newCode = [...code]; newCode[index] = text.slice(-1); setCode(newCode); if (text && index < CELL_COUNT - 1) { inputs.current[index + 1]?.focus(); } if (newCode.every((cell) => cell !== '')) { verifyOtp({ mobile, otp: newCode.join('') }); } }; const handleKeyPress = (e: any, index: number) => { if (e.nativeEvent.key === 'Backspace' && !code[index] && index > 0) { inputs.current[index - 1]?.focus(); } }; return ( Verification Enter the 6-digit code sent to {'\n'} {mobile} {code.map((digit, index) => ( { inputs.current[index] = ref }} style={[ styles.otpInput, { backgroundColor: theme.colors.surface, color: theme.colors.textPrimary, borderColor: digit ? theme.colors.primary : theme.colors.border, }, ]} keyboardType="number-pad" maxLength={CELL_COUNT} value={digit} placeholder="0" placeholderTextColor={theme.colors.textMuted} onChangeText={(text) => handleTextChange(text, index)} onKeyPress={(e) => handleKeyPress(e, index)} selectionColor={theme.colors.primary} textContentType="oneTimeCode" /> ))} verifyOtp({ mobile, otp: code.join('') })} loading={verifying} style={styles.button} /> {timer > 0 ? ( Resend code in 00:{timer.toString().padStart(2, '0')} ) : ( {resending ? 'Sending...' : 'Resend Code'} )} ); } const styles = StyleSheet.create({ container: { flex: 1, }, keyboardView: { flex: 1, justifyContent: 'center', }, content: { paddingHorizontal: 12, }, headerSection: { alignItems: 'center', marginBottom: 48, }, title: { fontSize: responsiveFontSize(3.5), fontFamily: FontsFamily.nunitoBold, marginBottom: 12, }, subtitle: { fontSize: responsiveFontSize(1.6), fontFamily: FontsFamily.nunitoRegular, textAlign: 'center', lineHeight: 24, }, otpSection: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 40, }, otpInput: { width: 48, height: 56, borderRadius: 12, borderWidth: 1.5, textAlign: 'center', fontSize: responsiveFontSize(2.2), fontFamily: FontsFamily.nunitoBold, }, actionSection: { alignItems: 'center', }, button: { width: '100%', marginBottom: 24, }, resendContainer: { alignItems: 'center', }, timerText: { fontSize: responsiveFontSize(1.4), fontFamily: FontsFamily.nunitoRegular, }, resendText: { fontSize: responsiveFontSize(1.5), fontFamily: FontsFamily.nunitoBold, textDecorationLine: 'underline', }, emphasisText: { fontWeight: '700', }, });