S Y P Y O K

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.

React Native Expo Hybrid .tsx OTP
                                        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