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.

0
Subscribe to my newsletter

Read articles from Ogunuyo Ogheneruemu B directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ogunuyo Ogheneruemu B
Ogunuyo Ogheneruemu B

I'm Ogunuyo Ogheneruemu Brown, a senior software developer. I specialize in DApp apps, fintech solutions, nursing web apps, fitness platforms, and e-commerce systems. Throughout my career, I've delivered successful projects, showcasing strong technical skills and problem-solving abilities. I create secure and user-friendly fintech innovations. Outside work, I enjoy coding, swimming, and playing football. I'm an avid reader and fitness enthusiast. Music inspires me. I'm committed to continuous growth and creating impactful software solutions. Let's connect and collaborate to make a lasting impact in software development.