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.