A powerful, customizable, and performance-optimized OTP (One-Time Password) timer component for React Native applications. Perfect for implementing secure authentication flows with automatic resend functionality and background time tracking.
- ⏱️ Accurate Timing: Precise countdown with background time calculation
- 🔄 Auto Resend: Configurable resend attempts with loading states
- 📱 Background Aware: Continues timing when app is backgrounded
- 🎨 Fully Customizable: Extensive styling and text formatting options
- ⚡ Performance Optimized: Built with React.memo and stable references
- 🔧 TypeScript Support: Fully typed with comprehensive interfaces
- 🧪 Testing Friendly: Debug mode and testing utilities included
- 🎯 Flexible API: Use as component or hook only
- 🚀 Zero Dependencies: No external dependencies except React Native
npm install react-native-otp-timer
# or
yarn add react-native-otp-timer
import React from 'react';
import { View, Alert } from 'react-native';
import OtpTimer from 'react-native-otp-timer';
const App = () => {
const handleResend = async (attemptNumber: number) => {
// Your OTP resend logic here
console.log(`Resending OTP - attempt ${attemptNumber}`);
await fetch('/api/resend-otp', { method: 'POST' });
};
const handleTimeout = () => {
Alert.alert('Timeout', 'OTP has expired');
};
return (
<View style={{ flex: 1, justifyContent: 'center', padding: 20 }}>
<OtpTimer
initialTimer={300} // 5 minutes
resendLimit={3}
onResend={handleResend}
onTimeout={handleTimeout}
/>
</View>
);
};
export default App;
import React, { useCallback } from 'react';
import { StyleSheet } from 'react-native';
import OtpTimer from 'react-native-otp-timer';
const CustomOtpTimer = () => {
const handleResend = useCallback(async (count: number) => {
try {
const response = await api.resendOtp({ attempt: count });
showSuccess('OTP sent successfully');
} catch (error) {
showError('Failed to send OTP');
throw error; // Prevent timer reset on failure
}
}, []);
const formatTime = useCallback((seconds: number) => {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins}m ${secs}s remaining`;
}, []);
return (
<OtpTimer
initialTimer={600} // 10 minutes
resendLimit={5}
onResend={handleResend}
onTimeout={() => navigation.goBack()}
onLimitExceeded={() => showContactSupport()}
containerStyle={styles.container}
textStyle={styles.text}
linkStyle={styles.link}
formatText={formatTime}
formatResendText={(attempt, max) => `Resend (${attempt}/${max})`}
showAttemptCounter={true}
debug={__DEV__}
/>
);
};
const styles = StyleSheet.create({
container: {
backgroundColor: '#f8f9fa',
padding: 16,
borderRadius: 8,
},
text: {
fontSize: 16,
color: '#495057',
},
link: {
fontSize: 16,
color: '#007bff',
fontWeight: '600',
},
});
import { useOtpTimer } from 'react-native-otp-timer';
const CustomTimerComponent = () => {
const { timer, resetTimer, isExpired, pauseTimer, resumeTimer } = useOtpTimer({
initialTimer: 300,
onTimeout: () => console.log('Timer expired'),
onTick: (currentTime) => {
if (currentTime === 60) {
showWarning('1 minute remaining!');
}
},
debug: true,
});
// Your custom UI implementation
return (
<View>
<Text>Time: {timer}s</Text>
<Button title="Reset" onPress={resetTimer} />
</View>
);
};
Prop | Type | Default | Description |
---|---|---|---|
initialTimer |
number |
Required | Initial countdown time in seconds |
resendLimit |
number |
Required | Maximum number of resend attempts |
onResend |
(count: number) => Promise<void> |
Required | Callback for handling OTP resend |
onTimeout |
() => void |
undefined |
Called when timer reaches zero |
onLimitExceeded |
() => void |
undefined |
Called when resend limit is exceeded |
onTimerStart |
(seconds: number) => void |
undefined |
Called when timer starts/resets |
containerStyle |
ViewStyle |
undefined |
Custom container styling |
textStyle |
TextStyle |
undefined |
Custom text styling |
linkStyle |
TextStyle |
undefined |
Custom link/button styling |
limitExceededStyle |
TextStyle |
undefined |
Custom style for limit exceeded message |
formatText |
(seconds: number) => string |
"MM:SS" |
Custom timer display formatter |
formatResendText |
(attempt: number, max: number) => string |
"Resend" |
Custom resend button text |
limitExceededMessage |
string |
"Limit exceeded..." |
Custom limit exceeded message |
enabled |
boolean |
true |
Enable/disable timer functionality |
showAttemptCounter |
boolean |
false |
Show attempt counter in UI |
debug |
boolean |
false |
Enable debug logging |
interface UseOtpTimerProps {
initialTimer: number;
onTimeout: () => void;
onTick?: (currentTime: number) => void;
debug?: boolean;
}
interface UseOtpTimerReturn {
timer: number; // Current timer value in seconds
resetTimer: () => void; // Reset timer to initial value
isActive: boolean; // Whether timer is currently running
isExpired: boolean; // Whether timer has reached zero
pauseTimer: () => void; // Pause the timer
resumeTimer: () => void; // Resume paused timer
}
Always use useCallback
for your event handlers:
const handleResend = useCallback(async (count: number) => {
// Your resend logic
}, []);
const handleTimeout = useCallback(() => {
// Your timeout logic
}, []);
Avoid creating objects in render:
// ❌ Bad - creates new object on every render
<OtpTimer
containerStyle={{ padding: 16 }}
onResend={(count) => api.resend(count)}
/>
// ✅ Good - stable references
const containerStyle = { padding: 16 };
const handleResend = useCallback((count) => api.resend(count), []);
<OtpTimer
containerStyle={containerStyle}
onResend={handleResend}
/>
The OtpTimer component is already memoized, but ensure parent components don't cause unnecessary re-renders:
const ParentComponent = React.memo(() => {
// Component implementation
});
The timer automatically handles app state changes:
- Background: Records timestamp when app goes to background
- Foreground: Calculates time spent in background and updates timer accordingly
- Accuracy: Maintains precise timing regardless of background duration
The component provides several ways to handle errors:
const handleResend = async (count: number) => {
try {
await api.resendOtp();
// Success - timer will reset automatically
} catch (error) {
showError('Failed to send OTP');
throw error; // Re-throw to prevent timer reset
}
};
Enable debug mode for development:
<OtpTimer
debug={__DEV__}
// ... other props
/>
Use shorter timers and higher limits for testing:
const TIMER_CONFIG = __DEV__
? { initialTimer: 10, resendLimit: 10 }
: { initialTimer: 300, resendLimit: 3 };
<OtpTimer
{...TIMER_CONFIG}
// ... other props
/>
If you're migrating from a basic countdown timer:
- Replace timer prop with
initialTimer
- Add required
onResend
callback - Update styling props (most are compatible)
- Add error handling to resend function
- Callbacks are automatically memoized internally
- Component uses React.memo for render optimization
- Background timing prevents unnecessary re-renders
- All intervals and listeners are properly cleaned up
Check out the examples directory for complete implementation examples including:
- Basic usage
- Custom styling
- Redux integration
- Error handling
- Testing setups
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature
) - Commit your changes (
git commit -m 'Add amazing feature'
) - Push to the branch (
git push origin feature/amazing-feature
) - Open a Pull Request
MIT License - see the LICENSE file for details.
Made with ❤️ for the React Native community
👤 Hamza Gulraiz