Mastering OTP Verification in React Native: A Step-by-Step Guide

Photo by Dan Nelson on Unsplash

Mastering OTP Verification in React Native: A Step-by-Step Guide

In the dynamic landscape of mobile app development, user authentication is a pivotal aspect, and nothing quite beats the effectiveness of One-Time Password (OTP) verification. In this comprehensive guide, we'll explore the intricacies of building a robust OTP screen in React Native, complete with resend functionality and a countdown timer.

Understanding the Components

Let's dissect the provided React Native code to understand its core components.

OTP Input Setup

const [otp, setOtp] = useState(['', '', '', '']);
const otpInputs = useRef([]);

The otp state manages the entered OTP digits, while otpInputs is a useRef array to keep track of individual TextInput components.

Resending OTP

const handleResendUpPress = () => {
  handleSubmitting();
  setCountdown(59); 
};

handleResendUpPress triggers the submission for the new OTP and resets the countdown timer. The actual resend action would typically involve sending a new OTP.

Fetching New OTP

const handleSubmitting = () => {
  // Fetch a new OTP using the 'mobileNumber'
  fetch('https://yoururl.com/', {
    method: 'POST',
    body: JSON.stringify({
      mobileNumber: mobileNumber,
      // ... (existing code)
    }),
    headers: {
      'Content-Type': 'application/json; charset=UTF-8',
    },
  })
    .then(response => response.json())
    .then(data => {
      const newOtpDigits = data.user.otp.toString().split('');
      setINewOtp(newOtpDigits);
      // ... (existing code)
    })
    .catch(error => {
      // ... (existing code)
    });
};

handleSubmitting fetches a new OTP using the provided mobile number and updates the state accordingly.

Countdown Timer

useEffect(() => {
  let interval;
  setResendVisible(true);

  if (countdown > 0) {
    interval = setInterval(() => {
      setCountdown((prevCountdown) => {
        if (prevCountdown === 0) {
          clearInterval(interval);
          setResendVisible(true);
          return 0;
        } else {
          return prevCountdown - 1;
        }
      });
    }, 1000);
  }

  return () => clearInterval(interval);
}, [countdown]);

The useEffect hook manages the countdown timer, updating every second and making the resend button visible when the countdown reaches zero.

Handling OTP Changes

const handleChangeOtp = (index, value) => {
  setErrorMessage('');

  if (!isNaN(value) || value === '') {
    const newOtp = [...otp];
    newOtp[index] = value;
    setOtp(newOtp);

    if (value === '' && index > 0) {
      otpInputs.current[index - 1].focus();
    } else if (value !== '' && index < 3) {
      otpInputs.current[index + 1].focus();
    }
  }
};

handleChangeOtp manages the OTP input changes, allowing only numeric values and handling auto-focus based on user input.

Handling OTP Submission

const handleSubmit = () => {
  const enteredOtp = otp.join('');
  if (enteredOtp.length === 4 && (enteredOtp === route.params.jotp || enteredOtp === inewOtp.join(''))) {
    navigation.navigate('Success');
  } else {
    setErrorMessage(enteredOtp.length === 4 ? 'Incorrect OTP' : 'Please enter a 4-digit OTP');
    setOtp(['', '', '', '']);
    setTimeout(() => {
      setErrorMessage('');
    }, 5000);
  }
};

handleSubmit validates the entered OTP and navigates to the 'Success' screen if the OTP matches. Otherwise, it displays an error message.

Complete Code:

import React, { useState, useRef, useEffect } from 'react';
import { View, Text, TextInput, StyleSheet, ScrollView, TouchableOpacity } from 'react-native';
import { useNavigation } from '@react-navigation/native';

export default function OtpScreen({ navigation, route }) {
  const [otp, setOtp] = useState(['', '', '', '']);
  const [inewOtp, setINewOtp] = useState([]);
  const otpInputs = useRef([]);
  const [errorMessage, setErrorMessage] = useState('');
  const [resendVisible, setResendVisible] = useState(true);
  const [countdown, setCountdown] = useState(59);

  const { mobileNumber } = route.params;

  const handleResendUpPress = () => {
    // Trigger the handleSubmit action for the new OTP
    handleSubmitting();

    // Perform the resend action here (e.g., send a new OTP)
    // For demonstration purposes, let's just reset the countdown
    setCountdown(59);
  };

  const handleSubmitting = () => {
    // Use the 'mobileNumber' received from the route parameters for the resend OTP request



    fetch('yoururl.com', {
      method: 'POST',
      body: JSON.stringify({
        mobileNumber: mobileNumber, // Use the mobile number for the resend request
        // ... (existing code)
      }),
      headers: {
        'Content-Type': 'application/json; charset=UTF-8',
      },
    })
      .then(response => response.json())
      .then(data => {
        // ... (existing code)
        const newOtpe = data.user.otp.toString().split('');
        setINewOtp(newOtpe);
        const user = {
          mobileNumber: mobileNumber,
          uniqueId: Math.random().toString(36).substring(2, 14) + Math.random().toString(36).substring(2, 14),

        };
        console.log('mobileNumber:', user.mobileNumber);
        console.log('New OTP:', data.user.otp);
      })
      .catch(error => {
        // ... (existing code)
      });
  };

  useEffect(() => {
    let interval;

    // Always set resendVisible to true when the component mounts
    setResendVisible(true);

    if (countdown > 0) {
      // Start the countdown only if it's greater than zero
      interval = setInterval(() => {
        setCountdown((prevCountdown) => {
          if (prevCountdown === 0) {
            // Countdown has elapsed, show the resend button
            clearInterval(interval);
            setResendVisible(true);
            return 0;
          } else {
            return prevCountdown - 1;
          }
        });
      }, 1000);
    }

    return () => clearInterval(interval); // Cleanup the interval on component unmount
  }, [countdown]);

  const handleChangeOtp = (index, value) => {
    // Reset error message when the user starts typing a new OTP
    setErrorMessage('');

    if (!isNaN(value) || value === '') {
      const newOtp = [...otp];
      newOtp[index] = value;
      setOtp(newOtp);

      // Auto focus to the next input or previous if backspace
      if (value === '' && index > 0) {
        otpInputs.current[index - 1].focus();
      } else if (value !== '' && index < 3) {
        otpInputs.current[index + 1].focus();
      }
    }
  };

  const handleSubmit = () => {
    const enteredOtp = otp.join('');
    if (enteredOtp.length === 4 && (enteredOtp === route.params.jotp || enteredOtp === inewOtp.join(''))) {
      // OTP matches, navigate to the reset screen and pass the necessary data
      navigation.navigate('Success');
    } else {
      // OTP does not match or no input, display an error message
      setErrorMessage(enteredOtp.length === 4 ? 'Incorrect OTP' : 'Please enter a 4-digit OTP');
      setOtp(['', '', '', '']); // Clear the input fields

      setTimeout(() => {
        setErrorMessage('');
      }, 5000);
    }
  };

  useEffect(() => {
    const enteredOtp = otp.join('');
    if (enteredOtp.length === 4 && (enteredOtp === route.params.jotp || enteredOtp === inewOtp.join(''))) {
      handleSubmit();
    }
  }, [otp, inewOtp]);

  return (
    <ScrollView contentContainerStyle={styles.scrollContainer} vertical={true}>
      <Text style={styles.firsttext}>Verification</Text>
      <Text style={styles.secondtext}>We sent an OTP to your phone number xxxxxxxx714</Text>

      <View style={styles.otpContainer}>
        {otp.map((value, index) => (
          <TextInput
            key={index}
            style={styles.otpInput}
            value={value}
            onChangeText={(text) => handleChangeOtp(index, text)}
            ref={(input) => (otpInputs.current[index] = input)}
            keyboardType="numeric"
            maxLength={1}
          />
        ))}
      </View>

      {countdown > 0 ? (
        <View style={styles.lasttext}>
          <Text>Resending OTP in {countdown} seconds</Text>
        </View>
      ) : (
        <View style={styles.lasttext}>
          <Text>
            Didn't get an OTP?{' '}
            <TouchableOpacity onPress={() => handleResendUpPress()}>
              <Text style={styles.linkText}>Resend</Text>
            </TouchableOpacity>
          </Text>
        </View>
      )}

      <TouchableOpacity style={styles.loginbutton1} onPress={handleSubmit}>
        <Text style={styles.logintext}>Verify</Text>
      </TouchableOpacity>

      {errorMessage !== '' && (
        <View style={styles.errorMessage}>
          <Text style={styles.errorMessageText}>{errorMessage}</Text>
        </View>
      )}
    </ScrollView>
  );
}

const styles = StyleSheet.create({

  scrollContainer: {
    flexGrow: 1, 
    justifyContent: 'center',
    padding: 16,
    gap: 8
  },
  firsttext:{
    fontSize: 26,
    fontWeight: '700'
  },
  secondtext:{
    fontSize: 14,
    fontWeight: '300'
  },

  loginbutton1: {
    backgroundColor: '#162d09',

    borderRadius: 10,
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center',
    padding: 14,

  },
  logintext: {
    color: 'white',
    fontSize: 16,
    fontWeight: '300',
  },
  orContainer: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'center',
    marginVertical: 20
  },
  line: {
    flex: 1,
    height: 1,
    backgroundColor: 'gray',
  },
  orText: {
    marginHorizontal: 10,
    fontSize: 16,
  },
  lasttext: {
    textAlign: 'center',
    fontSize: 14,
    fontWeight: '300',
    flexDirection: 'row', // Ensure the text is in a horizontal row
    alignItems: 'baseline', // Align the text elements at their baselines
    justifyContent: 'center', // Center the content horizontally
    marginBottom: 10
  },

  linkText: {
    fontSize: 14,
    fontWeight: '300',
    color: 'teal',
  },

  otpContainer: {
    flexDirection: 'row',
    justifyContent: 'center',
    marginVertical: 20,
  },
  otpInput: {
    flex: 1,
    borderWidth: 1,
    borderColor: 'gray',
    borderRadius: 10,
    padding: 10,
    textAlign: 'center',
    marginHorizontal: 5,
    fontSize: 20,
  },
  errorMessage: {
    marginTop: 10,
    alignSelf: 'center',
  },
  errorMessageText: {
    color: 'red',
  },
});

In Action: A User's Journey

Now, let's envision a user's journey through the OTP verification process:

  1. User Initiates OTP Verification:

    • User receives an OTP on their mobile number.
  2. Countdown Timer Starts:

    • Countdown timer begins, providing real-time feedback on the next OTP availability.
  3. User Requests Resend:

    • User triggers a resend, prompting the generation of a new OTP.
  4. New OTP Sent:

    • The application fetches a new OTP and updates the screen.
  5. User Enters OTP:

    • User enters the OTP, and the application navigates to success if the OTP is correct.

Conclusion

In this guide, we've dissected a React Native OTP verification component, unraveling its complexity and understanding the interplay between components. Feel free to incorporate and adapt this code into your React Native projects, providing users with a secure and seamless OTP verification experience.