Files
circle-collect/src/screens/OtpVarificationScreen.tsx
T
2026-03-31 13:02:17 +05:30

298 lines
8.7 KiB
TypeScript

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<RootStackParamList, 'OtpVarify'>;
export default function OtpVerificationScreen() {
const theme = useTheme();
const navigation = useNavigation<any>();
const route = useRoute<OtpVerifyRouteProp>();
const { mobile } = route.params;
const [code, setCode] = useState<string[]>(Array(CELL_COUNT).fill(''));
const [timer, setTimer] = useState(30);
const inputs = useRef<Array<TextInput | null>>([]);
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<any, any>(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<any>(
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 (
<AppView keyboardSafe={false} style={styles.container}>
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.keyboardView}
>
<Animated.View
style={[
styles.content,
{
opacity: fadeAnim,
transform: [{ translateY: slideAnim }],
},
]}
>
<View style={styles.headerSection}>
<Text style={[styles.title, { color: theme.colors.textPrimary }]}>Verification</Text>
<Text style={[styles.subtitle, { color: theme.colors.textSecondary }]}>
Enter the 6-digit code sent to {'\n'}
<Text style={[styles.emphasisText, { color: theme.colors.textPrimary }]}>{mobile}</Text>
</Text>
</View>
<View style={styles.otpSection}>
{code.map((digit, index) => (
<TextInput
key={index}
ref={(ref) => { 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"
/>
))}
</View>
<View style={styles.actionSection}>
<CustomButton
title="Verify Code"
onPress={() => verifyOtp({ mobile, otp: code.join('') })}
loading={verifying}
style={styles.button}
/>
<View style={styles.resendContainer}>
{timer > 0 ? (
<Text style={[styles.timerText, { color: theme.colors.textMuted }]}>
Resend code in <Text style={styles.emphasisText}>00:{timer.toString().padStart(2, '0')}</Text>
</Text>
) : (
<TouchableOpacity onPress={handleResendOtp} disabled={resending}>
<Text style={[styles.resendText, { color: theme.colors.primary }]}>
{resending ? 'Sending...' : 'Resend Code'}
</Text>
</TouchableOpacity>
)}
</View>
</View>
</Animated.View>
</KeyboardAvoidingView>
</AppView>
);
}
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',
},
});