OTP Screen with Auto Focus & Animation
Create a sleek and animated OTP input screen in React Native using Expo and TypeScript, featuring auto-focus, auto-submit, and resend timer.
import React, { useRef, useState, useEffect } from 'react';
import {
View,
Text,
TextInput,
TouchableOpacity,
StyleSheet,
Alert,
TextInput as RNTextInput,
} from 'react-native';
import * as Animatable from 'react-native-animatable';
export default function OtpScreen(): JSX.Element {
// State to store OTP input values
const [otp, setOtp] = useState<string[]>(['', '', '', '']);
const [error, setError] = useState(false); // Error state for invalid OTP
const [resendTimer, setResendTimer] = useState(30); // Timer for resend OTP
const [verified, setVerified] = useState(false); // State to track OTP verification
// Refs for managing input focus and animations
const inputs = useRef<Array<RNTextInput | null>>([]);
const animRefs = useRef<Array<Animatable.View | null>>([]);
// Countdown Timer for Resend OTP
useEffect(() => {
if (resendTimer === 0) return; // Stop timer when it reaches 0
const interval = setInterval(() => setResendTimer((t) => t - 1), 1000);
return () => clearInterval(interval); // Cleanup interval on unmount
}, [resendTimer]);
// Handle OTP input changes
const handleChange = (text: string, index: number) => {
if (/^\d$/.test(text)) {
const newOtp = [...otp];
newOtp[index] = text;
setOtp(newOtp);
// Move to the next input field if not the last digit
if (index < 3) {
inputs.current[index + 1]?.focus();
} else {
// Auto-submit OTP when all digits are entered
handleSubmit(newOtp.join(''));
}
} else if (text === '') {
// Clear the current input field
const newOtp = [...otp];
newOtp[index] = '';
setOtp(newOtp);
}
};
// Handle OTP submission
const handleSubmit = (code?: string) => {
const enteredCode = code || otp.join('');
if (enteredCode.length < 4 || enteredCode !== '1234') {
// Invalid OTP
setError(true);
animRefs.current.forEach((ref) => ref?.shake?.(500)); // Shake animation for error
setOtp(['', '', '', '']); // Clear OTP inputs
inputs.current[0]?.focus(); // Focus on the first input
} else {
// Valid OTP
setError(false);
setVerified(true);
Alert.alert('Success', 'OTP Verified 🎉');
}
};
// Handle Resend OTP
const handleResend = () => {
setOtp(['', '', '', '']); // Clear OTP inputs
inputs.current[0]?.focus(); // Focus on the first input
setResendTimer(30); // Reset timer
Alert.alert('OTP Resent', 'Check your phone.');
};
return (
<View style={styles.container}>
{/* Title */}
<Text style={styles.title}>Enter OTP</Text>
{/* OTP Input Fields */}
<View style={styles.otpContainer}>
{[0, 1, 2, 3].map((index) => (
<Animatable.View
key={index}
animation="bounceIn"
delay={index * 100}
ref={(ref) => (animRefs.current[index] = ref)}
>
<TextInput
style={[
styles.input,
error && { borderColor: 'red' }, // Red border for error
otp[index] !== '' && { borderColor: '#28a745' }, // Green border for valid input
]}
keyboardType="numeric"
maxLength={1}
value={otp[index]}
onChangeText={(text) => handleChange(text, index)}
onKeyPress={({ nativeEvent }) => {
// Handle backspace to move to the previous input
if (
nativeEvent.key === 'Backspace' &&
otp[index] === '' &&
index > 0
) {
inputs.current[index - 1]?.focus();
}
}}
ref={(ref) => (inputs.current[index] = ref)}
/>
</Animatable.View>
))}
</View>
{/* Verify Button */}
<TouchableOpacity style={styles.button} onPress={() => handleSubmit()}>
<Text style={styles.buttonText}>Verify</Text>
</TouchableOpacity>
{/* Resend OTP Button */}
<TouchableOpacity
onPress={resendTimer === 0 ? handleResend : undefined}
disabled={resendTimer !== 0} // Disable button while timer is running
>
<Text style={styles.resendText}>
{resendTimer === 0
? 'Resend OTP'
: `Resend in ${resendTimer}s`}
</Text>
</TouchableOpacity>
</View>
);
}
// Styles
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 20,
backgroundColor: '#fff',
},
title: {
fontSize: 24,
marginBottom: 40,
fontWeight: 'bold',
},
otpContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
},
input: {
borderWidth: 2,
borderColor: '#ccc',
borderRadius: 12,
width: 55,
height: 55,
textAlign: 'center',
fontSize: 22,
marginHorizontal: 6,
backgroundColor: '#f9f9f9',
},
button: {
marginTop: 30,
backgroundColor: '#007bff',
paddingHorizontal: 40,
paddingVertical: 12,
borderRadius: 10,
},
buttonText: {
color: '#fff',
fontSize: 18,
},
resendText: {
marginTop: 20,
fontSize: 16,
color: '#007bff',
},
});
See More