298 lines
8.7 KiB
TypeScript
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',
|
|
},
|
|
});
|