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:
User Initiates OTP Verification:
- User receives an OTP on their mobile number.
Countdown Timer Starts:
- Countdown timer begins, providing real-time feedback on the next OTP availability.
User Requests Resend:
- User triggers a resend, prompting the generation of a new OTP.
New OTP Sent:
- The application fetches a new OTP and updates the screen.
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.
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.