@avasapp/react-native-otp-autofill
v1.1.0
Published
Automatic SMS Verification with the SMS Retriever API
Readme
React Native OTP Autofill
An Expo module for automatic SMS verification using Android SMS Retriever API.
Requirements
- Android API 23+ (24+ on Expo SDK 54+)
- Google Play Services
- Expo SDK 50+
Features
- ✅ Automatic SMS verification using Android SMS Retriever API
- ✅ App signature hash generation for SMS verification
- ✅ OTP extraction from SMS messages
- ✅ Event-based listener system
- ✅ TypeScript support
- ✅ Expo modules API
Installation
This module is available as an npm package. To use it in your Expo/React Native app:
npm
npm install @avasapp/react-native-otp-autofillbun
bun add @avasapp/react-native-otp-autofillyarn
yarn add @avasapp/react-native-otp-autofillQuick Start
After installation, you can use the React hooks for the simplest integration:
import { useGetHash, useOtpListener } from '@avasapp/react-native-otp-autofill'
// In your component
const { hash } = useGetHash()
const { startListener, receivedOtp } = useOtpListener()Usage
React Hooks API (Recommended)
The module provides React hooks for easy integration with modern React apps:
useGetHash Hook
import React from 'react'
import { Button, Text, View } from 'react-native'
import { useGetHash } from '@avasapp/react-native-otp-autofill'
const AppHashComponent = () => {
const { hash, loading, error, refetch } = useGetHash({
onSuccess: (hash) => {
console.log('App hash loaded:', hash)
},
onError: (error) => {
console.error('Failed to get app hash:', error)
},
})
if (loading) return <Text>Loading app hash...</Text>
if (error) return <Text>Error: {error.message}</Text>
return (
<View>
<Text>App Hash: {hash}</Text>
<Button title="Refresh Hash" onPress={refetch} />
</View>
)
}useOtpListener Hook
import React from 'react'
import { Button, Text, View } from 'react-native'
import { useOtpListener } from '@avasapp/react-native-otp-autofill'
const SmsVerificationComponent = () => {
const {
isListening,
loading,
receivedOtp,
receivedMessage,
error,
startListener,
stopListener,
} = useOtpListener({
onOtpReceived: (otp, message) => {
console.log('OTP received:', otp)
console.log('Full message:', message)
// Process the OTP
},
onTimeout: (message) => {
console.log('SMS timeout:', message)
},
onError: (error, code) => {
console.error('SMS error:', error, 'Code:', code)
},
})
return (
<View>
<Button
title={
loading
? 'Starting...'
: isListening
? 'Listening...'
: 'Start SMS Listener'
}
onPress={startListener}
disabled={isListening || loading}
/>
<Button title="Stop Listener" onPress={stopListener} disabled={!isListening} />
{receivedOtp ? <Text>OTP: {receivedOtp}</Text> : null}
{error ? <Text>Error: {error}</Text> : null}
</View>
)
}Complete Example with Both Hooks
import React, { useState } from 'react'
import { Button, Text, TextInput, View } from 'react-native'
import { useGetHash, useOtpListener } from '@avasapp/react-native-otp-autofill'
const SmsVerificationFlow = () => {
const [step, setStep] = useState<'hash' | 'sms' | 'complete'>('hash')
const [phoneNumber, setPhoneNumber] = useState('')
// Get app hash first
const {
hash,
loading: hashLoading,
error: hashError,
} = useGetHash({
onSuccess: (hash) => {
console.log('Ready to send SMS with hash:', hash)
setStep('sms')
},
})
// Listen for SMS
const { isListening, receivedOtp, startListener, stopListener } =
useOtpListener({
onOtpReceived: (otp) => {
console.log('Verification complete:', otp)
setStep('complete')
stopListener()
},
})
const sendSms = async () => {
if (!hash) return
// Send SMS with your backend API
await fetch('/api/send-sms', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
phoneNumber,
appHash: hash,
}),
})
// Start listening for SMS
startListener()
}
if (step === 'hash') {
return (
<View>
<Text>Preparing SMS verification...</Text>
{hashLoading ? <Text>Loading...</Text> : null}
{hashError ? <Text>Error: {hashError.message}</Text> : null}
{hash ? <Text>Ready! Hash: {hash}</Text> : null}
</View>
)
}
if (step === 'sms') {
return (
<View>
<TextInput
keyboardType="phone-pad"
placeholder="Phone number"
value={phoneNumber}
onChangeText={setPhoneNumber}
/>
<Button title="Send SMS" onPress={sendSms} disabled={!phoneNumber} />
{isListening ? <Text>Waiting for SMS...</Text> : null}
</View>
)
}
return (
<View>
<Text>✅ Verification complete!</Text>
<Text>OTP: {receivedOtp}</Text>
</View>
)
}Manual API (no hooks)
If you prefer direct control without hooks, use the module methods and event listeners:
import { AvasOtpAutofillModule } from '@avasapp/react-native-otp-autofill'
// Get app signature hash (use the first one)
const getAppHash = async (): Promise<string | undefined> => {
try {
const hashes = await AvasOtpAutofillModule.getHash()
console.log('App signature hashes:', hashes)
return hashes[0]
} catch (error) {
console.error('Error getting app hash:', error)
}
}
// Start listening for SMS and wire up events
const startSmsListener = async () => {
const subs = [] as import('expo-modules-core').EventSubscription[]
subs.push(
AvasOtpAutofillModule.addListener('onSmsReceived', ({ otp, message }) => {
console.log('OTP received:', otp)
console.log('Full message:', message)
// ...verify with your backend
}),
)
subs.push(
AvasOtpAutofillModule.addListener('onTimeout', ({ message }) => {
console.log('SMS timeout:', message)
}),
)
subs.push(
AvasOtpAutofillModule.addListener('onError', ({ message, code }) => {
console.error('SMS error:', message, 'code:', code)
}),
)
await AvasOtpAutofillModule.startOtpListener()
// Return a cleanup function
return () => {
subs.forEach((s) => s.remove())
AvasOtpAutofillModule.stopSmsRetriever()
}
}Manual example (React component)
import React, { useRef, useState } from 'react'
import { Button, Text, View } from 'react-native'
import { AvasOtpAutofillModule } from '@avasapp/react-native-otp-autofill'
export const ManualSmsVerification = () => {
const [otp, setOtp] = useState<string | null>(null)
const [message, setMessage] = useState<string | null>(null)
const [isListening, setIsListening] = useState(false)
const cleanupRef = useRef<null | (() => void)>(null)
const start = async () => {
if (isListening) return
setIsListening(true)
setOtp(null)
setMessage(null)
// Register listeners first
const subs = [
AvasOtpAutofillModule.addListener('onSmsReceived', ({ otp, message }) => {
setOtp(otp ?? null)
setMessage(message)
setIsListening(false)
cleanup()
}),
AvasOtpAutofillModule.addListener('onTimeout', () => {
setIsListening(false)
cleanup()
}),
AvasOtpAutofillModule.addListener('onError', () => {
setIsListening(false)
cleanup()
}),
]
const cleanup = () => {
subs.forEach((s) => s.remove())
AvasOtpAutofillModule.stopSmsRetriever()
cleanupRef.current = null
}
cleanupRef.current = cleanup
await AvasOtpAutofillModule.startOtpListener()
}
const stop = () => {
cleanupRef.current?.()
setIsListening(false)
}
return (
<View>
<Button title={isListening ? 'Listening…' : 'Start SMS Listener'} onPress={start} disabled={isListening} />
{isListening ? <Button title="Stop" onPress={stop} /> : null}
{otp ? <Text>OTP: {otp}</Text> : null}
{message ? <Text>Message: {message}</Text> : null}
</View>
)
}API Reference
React Hooks
useGetHash(options?: UseGetHashOptions): UseGetHashReturn
A React hook for managing app signature hash retrieval with automatic loading states and error handling.
Options:
interface UseGetHashOptions {
onSuccess?: (value: string) => void
onError?: (error: Error) => void
}Returns:
interface UseGetHashReturn {
hash: string | null // The app signature hash
loading: boolean // Whether hash is being fetched
error: Error | null // Any error that occurred
refetch: () => Promise<void> // Function to refetch the hash
}Example:
const { hash, loading, error, refetch } = useGetHash({
onSuccess: (hash) => console.log('Hash:', hash),
onError: (error) => console.error('Error:', error),
})useOtpListener(options?: UseOtpListenerOptions): UseOtpListenerReturn
A React hook for managing SMS OTP listening with automatic cleanup and state management.
Options:
interface UseOtpListenerOptions {
onOtpReceived?: (otp: string, message: string) => void
onTimeout?: (message: string) => void
onError?: (error: string, code: number) => void
}Returns:
interface UseOtpListenerReturn {
isListening: boolean // Whether actively listening for SMS
loading: boolean // Whether starting/stopping listener
receivedOtp: string | null // Last received OTP
receivedMessage: string | null // Full SMS message
error: string | null // Any error message
startListener: () => Promise<void> // Start listening for SMS
stopListener: () => void // Stop listening and cleanup
}Example:
const { isListening, receivedOtp, startListener, stopListener } =
useOtpListener({
onOtpReceived: (otp, message) => {
console.log('OTP:', otp, 'Message:', message)
},
})Manual Methods
getHash(): Promise<string[]>
Returns the app signature hashes needed for SMS verification.
const hashes = await AvasOtpAutofillModule.getHash()startOtpListener(): Promise<boolean>
Starts listening for SMS using the Android SMS Retriever API.
await AvasOtpAutofillModule.startOtpListener()addListener(eventName, listener): EventSubscription
Adds a listener for module events. Call before startOtpListener().
const sub = AvasOtpAutofillModule.addListener('onSmsReceived', ({ otp, message }) => {
console.log('OTP:', otp, 'Message:', message)
})Cleanup
Use the returned subscription to remove listeners, and stop the retriever when done.
sub.remove()
await AvasOtpAutofillModule.stopSmsRetriever()Events
The module emits the following events:
onSmsReceived: When an SMS is received with OTPonTimeout: When SMS retriever times out (after 5 minutes)onError: When an error occurs
Event payloads:
type SmsReceivedEventPayload = {
message: string
otp: string | null
}
type TimeoutEventPayload = {
message: string
}
type ErrorEventPayload = {
message: string
code: number
}SMS Format Requirements
For the SMS Retriever API to work, the SMS message must:
- Contain a verification code (4-6 digits)
- Include your app's signature hash
- Be no longer than 140 bytes
- Contain a one-time code that the user has never seen before
Example SMS Format
Your verification code is: 123456
FA+9qCX9VSuWhere FA+9qCX9VSu is your app's signature hash.
Note on the app hash: The hash is an 11-character standard base64 string (alphabet
A-Za-z0-9+/, matching the provider's^[A-Za-z0-9+/=]{11}$pattern). It is derived from your app's signing certificate, so it differs per build variant. For builds distributed through Play App Signing, the hash must be derived from the "App signing key certificate" in the Play Console — this differs from your upload/debug keystore, so a hash computed locally will not match production SMS.useGetHashreads the runtime hash for whichever build variant is currently running, so prefer it over hardcoding a value.
Troubleshooting
Common Issues
- SMS not received: Ensure your SMS includes the correct app signature hash
- Module not found: Make sure the module is properly installed and linked
- Timeout errors: SMS Retriever has a 5-minute timeout limit
- Hooks not updating: Make sure you're using the hooks inside React components
- Multiple listeners: Use
stopListener()before starting a new listener
Debug Mode
Using Hooks for Debugging
import { Text, View } from 'react-native'
import { useGetHash, useOtpListener } from '@avasapp/react-native-otp-autofill'
const DebugComponent = () => {
const { hash, loading, error } = useGetHash({
onSuccess: (hash) => console.log('✅ Hash loaded:', hash),
onError: (error) => console.error('❌ Hash error:', error),
})
const {
isListening,
receivedOtp,
receivedMessage,
error: smsError,
} = useOtpListener({
onOtpReceived: (otp, message) => {
console.log('📱 SMS received:', { otp, message })
},
onTimeout: (message) => {
console.log('⏰ SMS timeout:', message)
},
onError: (error, code) => {
console.error('❌ SMS error:', { error, code })
},
})
return (
<View>
<Text>Hash: {hash || 'Loading...'}</Text>
<Text>Listening: {isListening ? 'Yes' : 'No'}</Text>
<Text>OTP: {receivedOtp || 'None'}</Text>
{error ? <Text>Hash Error: {error.message}</Text> : null}
{smsError ? <Text>SMS Error: {smsError}</Text> : null}
</View>
)
}Manual Event Debugging
import { AvasOtpAutofillModule } from '@avasapp/react-native-otp-autofill'
// Listen to all events for debugging
AvasOtpAutofillModule.addListener('onSmsReceived', (event) => {
console.log('📱 SMS received:', event)
})
AvasOtpAutofillModule.addListener('onTimeout', (event) => {
console.log('⏰ SMS timeout:', event)
})
AvasOtpAutofillModule.addListener('onError', (event) => {
console.log('❌ SMS error:', event)
})Performance Tips
- Use hooks at component level: Don't call hooks conditionally or in loops
- Clean up listeners: Always call
stopListener()when component unmounts - Avoid multiple hash fetches: Use
refetch()fromuseGetHashinstead of creating new instances - Handle loading states: Show loading indicators to improve user experience
Development
Building the Module
# Install dependencies
bun install
# Build the module
bun run build
# Clean build artifacts
bun run clean
# Run linting
bun run lint
# Run tests
bun run testPublishing
This package is published to the npm registry. To publish a new version:
- Update the version in
package.json - Build and publish:
bun run build npm publish --access public
Local Development
For local development and testing:
# Link the package locally
bun link
# In your test project
bun link @avasapp/react-native-otp-autofillLicense
MIT
