An Expo module for automatic SMS verification using Android SMS Retriever API.
- Android API 23+ (24+ on Expo SDK 54+)
- Google Play Services
- Expo SDK 50+
- β 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
This module is available as an npm package. To use it in your Expo/React Native app:
npm install @avasapp/react-native-otp-autofillbun add @avasapp/react-native-otp-autofillyarn add @avasapp/react-native-otp-autofillAfter 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()The module provides React hooks for easy integration with modern React apps:
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>
)
}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>
)
}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>
)
}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()
}
}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>
)
}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),
})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)
},
})Returns the app signature hashes needed for SMS verification.
const hashes = await AvasOtpAutofillModule.getHash()Starts listening for SMS using the Android SMS Retriever API.
await AvasOtpAutofillModule.startOtpListener()Adds a listener for module events. Call before startOtpListener().
const sub = AvasOtpAutofillModule.addListener('onSmsReceived', ({ otp, message }) => {
console.log('OTP:', otp, 'Message:', message)
})Use the returned subscription to remove listeners, and stop the retriever when done.
sub.remove()
await AvasOtpAutofillModule.stopSmsRetriever()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
}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
Your verification code is: 123456
FA+9qCX9VSu
Where 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.
- 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
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>
)
}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)
})- 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
# 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 testThis 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
For local development and testing:
# Link the package locally
bun link
# In your test project
bun link @avasapp/react-native-otp-autofillMIT