Files
info-target-mobile/screens/auth/register-confirm/ConfirmForm.tsx
Samandar Turgunboyev 124798419b fitst commit
2026-01-28 18:26:50 +05:00

240 lines
6.9 KiB
TypeScript

import { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
ActivityIndicator,
Animated,
Keyboard,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from 'react-native';
interface OtpFormProps {
phone?: string;
initialCode?: string;
onSubmit?: (otp: string) => void;
isLoading?: boolean;
error?: string;
onResendPress?: () => void;
resendTimer: number;
}
const RegisterConfirmForm = ({
initialCode,
onSubmit,
isLoading = false,
error,
onResendPress,
resendTimer,
}: OtpFormProps) => {
const [otp, setOtp] = useState<string[]>(Array(4).fill(''));
const [focusedIndex, setFocusedIndex] = useState<number>(0);
const inputRefs = useRef<TextInput[]>([]);
const shakeAnimation = useRef(new Animated.Value(0)).current;
const { t } = useTranslation();
useEffect(() => {
if (error) {
Animated.sequence([
Animated.timing(shakeAnimation, { toValue: 10, duration: 50, useNativeDriver: true }),
Animated.timing(shakeAnimation, { toValue: -10, duration: 50, useNativeDriver: true }),
Animated.timing(shakeAnimation, { toValue: 10, duration: 50, useNativeDriver: true }),
Animated.timing(shakeAnimation, { toValue: 0, duration: 50, useNativeDriver: true }),
]).start();
}
}, [error]);
useEffect(() => {
if (initialCode && initialCode.length === 4 && /^\d{4}$/.test(initialCode)) {
setOtp(initialCode.split(''));
}
}, [initialCode]);
// Raqam kiritilganda yoki o'chirilganda
const handleChange = (value: string, index: number) => {
const cleanValue = value.replace(/[^0-9]/g, '');
const newOtp = [...otp];
// Agar qiymat bo'sh bo'lsa (o'chirilgan bo'lsa)
if (value === '') {
newOtp[index] = '';
setOtp(newOtp);
return;
}
// Faqat oxirgi kiritilgan raqamni olish
newOtp[index] = cleanValue.slice(-1);
setOtp(newOtp);
// Keyingi katakka o'tish
if (newOtp[index] !== '' && index < 3) {
inputRefs.current[index + 1]?.focus();
}
// Hamma raqam kiritilgan bo'lsa avtomat yuborish
const fullCode = newOtp.join('');
if (fullCode.length === 4) {
Keyboard.dismiss();
onSubmit?.(fullCode);
}
};
// Maxsus tugmalar (Backspace) uchun
const handleKeyPress = ({ nativeEvent }: any, index: number) => {
if (nativeEvent.key === 'Backspace') {
// Agar katakda raqam bo'lsa, uni o'chiradi
if (otp[index] !== '') {
const newOtp = [...otp];
newOtp[index] = '';
setOtp(newOtp);
}
// Agar katak bo'sh bo'lsa, oldingisiga o'tib uni ham o'chiradi
else if (index > 0) {
const newOtp = [...otp];
newOtp[index - 1] = '';
setOtp(newOtp);
inputRefs.current[index - 1]?.focus();
}
}
};
return (
<View style={styles.container}>
<Animated.View style={[styles.otpContainer, { transform: [{ translateX: shakeAnimation }] }]}>
{otp.map((digit, index) => (
<TouchableOpacity
key={index}
activeOpacity={1}
onPress={() => inputRefs.current[index]?.focus()}
style={[
styles.inputBox,
focusedIndex === index && styles.inputBoxFocused,
digit !== '' && styles.inputBoxFilled,
error ? styles.inputBoxError : null,
]}
>
<Text style={[styles.inputText, digit === '' && styles.placeholderText]}>
{digit || '•'}
</Text>
<TextInput
ref={(ref) => {
if (ref) inputRefs.current[index] = ref;
}}
value={digit}
onChangeText={(v) => handleChange(v, index)}
onKeyPress={(e) => handleKeyPress(e, index)}
onFocus={() => setFocusedIndex(index)}
onBlur={() => setFocusedIndex(-1)}
keyboardType="number-pad"
maxLength={1}
style={styles.hiddenInput}
caretHidden
selectTextOnFocus // Bu 2 marta bosish muammosini oldini olishga yordam beradi
/>
</TouchableOpacity>
))}
</Animated.View>
{error && <Text style={styles.errorText}>{error}</Text>}
<TouchableOpacity
onPress={() => onSubmit?.(otp.join(''))}
disabled={isLoading || otp.join('').length !== 4}
activeOpacity={0.8}
style={[
styles.submitButton,
(isLoading || otp.join('').length !== 4) && styles.submitButtonDisabled,
]}
>
{isLoading ? (
<ActivityIndicator color="#fff" size="small" />
) : (
<Text style={styles.submitText}>{t('Kodni tasdiqlash')}</Text>
)}
</TouchableOpacity>
<View style={styles.resendContainer}>
{resendTimer > 0 ? (
<Text style={styles.timerText}>
{t('Qayta yuborish vaqti')}:<Text style={styles.timerCount}>{resendTimer}s</Text>
</Text>
) : (
<TouchableOpacity onPress={onResendPress} style={styles.resendButton}>
<Text style={styles.resendText}>{t('Kodni qayta yuborish')}</Text>
</TouchableOpacity>
)}
</View>
</View>
);
};
const styles = StyleSheet.create({
container: { width: '100%', paddingVertical: 10 },
otpContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 20,
paddingHorizontal: 5,
},
inputBox: {
width: 60,
height: 65,
borderRadius: 16,
borderWidth: 1.5,
borderColor: '#E2E8F0',
backgroundColor: '#F8FAFC',
alignItems: 'center',
justifyContent: 'center',
elevation: 1,
},
inputBoxFocused: {
borderColor: '#3B82F6',
backgroundColor: '#FFFFFF',
borderWidth: 2,
elevation: 4,
},
inputBoxFilled: {
borderColor: '#3B82F6',
backgroundColor: '#FFFFFF',
},
inputBoxError: {
borderColor: '#EF4444',
backgroundColor: '#FFF1F2',
},
inputText: { fontSize: 26, fontWeight: '700', color: '#1E293B' },
placeholderText: { color: '#CBD5E1', fontSize: 18 },
hiddenInput: { position: 'absolute', width: '100%', height: '100%', opacity: 0 },
errorText: {
color: '#EF4444',
fontSize: 14,
textAlign: 'center',
marginBottom: 20,
fontWeight: '500',
},
submitButton: {
height: 56,
backgroundColor: '#3B82F6',
borderRadius: 16,
alignItems: 'center',
justifyContent: 'center',
elevation: 3,
},
submitButtonDisabled: { backgroundColor: '#94A3B8', elevation: 0 },
submitText: { color: '#FFFFFF', fontSize: 16, fontWeight: '600' },
resendContainer: { alignItems: 'center', marginTop: 25 },
timerText: { color: '#64748B', fontSize: 14 },
timerCount: { color: '#1E293B', fontWeight: '700' },
resendButton: { paddingVertical: 5 },
resendText: {
color: '#3B82F6',
fontSize: 15,
fontWeight: '600',
textDecorationLine: 'underline',
},
});
export default RegisterConfirmForm;