expo-sms-listener
v1.0.10
Published
Listen to incoming SMS messages on Android – works when the app is active, backgrounded, or fully closed (deep sleep). Built with native Kotlin + Expo Modules API.
Downloads
1,182
Maintainers
Readme
expo-sms-listener
A comprehensive Expo library for Android that listens to incoming SMS messages in all app states — active, background, and fully closed (deep sleep) — with built-in OTP auto-detection and a React hook API.
Features
| Feature | Description |
|---|---|
| 📩 Foreground SMS | Receive SMS instantly while the app is open |
| 🔄 Background SMS | Receive SMS with screen off via Foreground Service |
| 💤 Deep-sleep SMS | Receive SMS when app is fully closed via HeadlessJS |
| 🔑 OTP Auto-detect | Extract OTP codes from any SMS with smart regex |
| ⏱️ OTP Hook | useOtpListener() with auto-clear timer |
| 🔍 SMS Filter Hook | useSmsFilter() — filter by sender or keyword |
| ⚙️ Config Plugin | Auto-merges Android permissions on expo prebuild |
Installation
npx expo install expo-sms-listenerAdd the Config Plugin
In your app.json / app.config.js:
{
"expo": {
"plugins": ["expo-sms-listener"]
}
}Then rebuild your native project:
npx expo prebuild --clean
npx expo run:androidRegister the HeadlessJS Task (Required for closed-app support)
In your app's entry file (index.js or index.ts) — before registerRootComponent:
import { AppRegistry } from 'react-native';
import { registerRootComponent } from 'expo';
import App from './App';
// Handles SMS when the app is FULLY CLOSED
AppRegistry.registerHeadlessTask(
'ExpoSmsListenerBackground',
() => async (data) => {
// data: { originatingAddress, body, timestamp }
console.log('SMS received while closed:', data.body);
// Save to AsyncStorage, send a local notification, etc.
}
);
registerRootComponent(App);Quick Start
import {
requestSmsPermissionAsync,
startSmsListenerServiceAsync,
useOtpListener,
useSmsListener,
} from 'expo-sms-listener';
export default function App() {
// Auto-detect OTP from any incoming SMS
const { otp, clear } = useOtpListener({ length: 6, timeout: 30_000 });
// Or listen to all incoming SMS
useSmsListener((msg) => {
console.log(msg.originatingAddress, msg.body);
});
useEffect(() => {
(async () => {
const { granted } = await requestSmsPermissionAsync();
if (granted) await startSmsListenerServiceAsync();
})();
}, []);
return <OtpInput value={otp ?? ''} />;
}API Reference
Permission
requestSmsPermissionAsync()
Shows the Android permission dialog for RECEIVE_SMS and READ_SMS.
Returns Promise<{ granted: boolean }>.
const { granted } = await requestSmsPermissionAsync();checkSmsPermissionAsync()
Checks current permission status without prompting.
Returns Promise<{ granted: boolean }>.
const { granted } = await checkSmsPermissionAsync();Foreground Service
startSmsListenerServiceAsync()
Starts the persistent foreground service that keeps the JS engine alive in background and deep sleep. Shows a persistent notification (required by Android 8+).
await startSmsListenerServiceAsync();stopSmsListenerServiceAsync()
Stops the foreground service and removes the notification.
await stopSmsListenerServiceAsync();Core SMS Listener
useSmsListener(callback)
React hook. Fires callback on every incoming SMS while the JS engine is alive (foreground + background). Automatically unsubscribes on unmount.
useSmsListener((msg: SmsMessage) => {
console.log(msg.originatingAddress); // "+1234567890"
console.log(msg.body); // "Your OTP is 123456"
console.log(msg.timestamp); // 1710000000000 (ms)
});addSmsListener(callback)
Imperative version. Returns an EventSubscription — call .remove() to unsubscribe.
const sub = addSmsListener((msg) => console.log(msg.body));
// later...
sub.remove();SMS Filter Hook
useSmsFilter(filter, callback)
Fires callback only for messages matching the filter. Case-insensitive partial match.
useSmsFilter(
{ sender: 'VM-MYBANK', keyword: 'transaction' },
(msg) => saveTransaction(msg.body)
);Filter options:
| Property | Type | Description |
|---|---|---|
| sender | string | Partial match on originatingAddress |
| keyword | string | Partial match on body text |
OTP Utilities
extractOtp(body, options?)
Extract a numeric OTP / verification code from an SMS body string. Uses 4 cascading regex patterns — keyword-adjacent codes take priority over plain digit sequences.
extractOtp('Your code is 123456. Do not share.') // → '123456'
extractOtp('OTP: 4829', { length: 4 }) // → '4829'
extractOtp('Verify: 88 91 23', { length: [4, 6] }) // → '889123' (first 6-digit run)
extractOtp('Call us at 1800-123') // → null (7 digits, out of default range)Options:
| Property | Type | Default | Description |
|---|---|---|---|
| length | number \| [min, max] | [4, 8] | Accepted OTP digit length or range |
useOtpListener(options?)
React hook. Watches all incoming SMS, auto-extracts an OTP, and returns it with an expiry timer.
const { otp, clear } = useOtpListener({
length: 6, // accept only 6-digit OTPs
timeout: 60_000, // auto-clear after 60 s (default: 30 s, 0 = never)
senderFilter: 'VM-BANK', // optional: only from this sender
bodyFilter: 'verification', // optional: only SMS containing this word
});
// Use the OTP
<TextInput value={otp ?? ''} placeholder="Waiting for OTP…" />Return value:
| Property | Type | Description |
|---|---|---|
| otp | string \| null | Extracted OTP or null |
| clear | () => void | Manually clear OTP and cancel the timer |
Options:
| Property | Type | Default | Description |
|---|---|---|---|
| length | number \| [min, max] | [4, 8] | OTP length filter |
| timeout | number (ms) | 30000 | Auto-clear delay. 0 = disabled |
| senderFilter | string | — | Only match this sender |
| bodyFilter | string | — | Only match SMS containing this word |
How It Works
SMS arrives on device
│
├─── App OPEN (foreground) ──────────────────────────────────────────┐
│ SmsForegroundReceiver fires │
│ → ExpoSmsListenerModule emits 'onSmsReceived' event │
│ → useSmsListener / useOtpListener callbacks fire in JS │
│ │
├─── App BACKGROUNDED / SCREEN OFF ──────────────────────────────────┤
│ SmsListenerForegroundService keeps process alive │
│ SmsForegroundReceiver still fires │
│ → same JS event path as foreground │
│ │
└─── App FULLY CLOSED / DEEP SLEEP ──────────────────────────────────┘
Android wakes SmsBackgroundReceiver (static, always registered)
→ Starts SmsBackgroundHeadlessService
→ React Native boots Hermes in headless mode
→ Your registered 'ExpoSmsListenerBackground' task runs
→ SMS data available for processing (AsyncStorage, notifications…)Required Permissions
The Config Plugin automatically adds these to AndroidManifest.xml:
| Permission | Purpose |
|---|---|
| RECEIVE_SMS | Listen to incoming SMS |
| READ_SMS | Read SMS content |
| WAKE_LOCK | Keep CPU awake when processing background SMS |
| FOREGROUND_SERVICE | Run foreground service |
| FOREGROUND_SERVICE_DATA_SYNC | Required on Android 14+ for typed foreground services |
Types
type SmsMessage = {
originatingAddress: string; // sender phone number / short code
body: string; // raw SMS text
timestamp: number; // Unix ms
};
type RequestPermissionResult = { granted: boolean };
type OtpExtractOptions = {
length?: number | [min: number, max: number];
};
type OtpListenerOptions = OtpExtractOptions & {
timeout?: number; // ms, default 30 000
senderFilter?: string;
bodyFilter?: string;
};
type OtpListenerResult = {
otp: string | null;
clear: () => void;
};
type SmsFilterOptions = {
sender?: string;
keyword?: string;
};Complete Example
import { useEffect } from 'react';
import { AppRegistry } from 'react-native';
import {
requestSmsPermissionAsync,
startSmsListenerServiceAsync,
useSmsListener,
useSmsFilter,
useOtpListener,
extractOtp,
SmsMessage,
} from 'expo-sms-listener';
// ── Register HeadlessJS task in index.ts (entry file) ──────────────
AppRegistry.registerHeadlessTask(
'ExpoSmsListenerBackground',
() => async (data: SmsMessage) => {
const otp = extractOtp(data.body);
if (otp) await saveOtpToStorage(otp);
}
);
// ── In your screen / component ──────────────────────────────────────
export default function LoginScreen() {
// Auto-fill OTP
const { otp, clear } = useOtpListener({ length: 6, timeout: 60_000 });
// Filter to only bank SMS
useSmsFilter({ sender: 'VM-MYBANK' }, (msg) => {
console.log('Bank SMS:', msg.body);
});
// All SMS
useSmsListener((msg) => {
console.log('Any SMS:', msg.body);
});
useEffect(() => {
(async () => {
const { granted } = await requestSmsPermissionAsync();
if (granted) {
await startSmsListenerServiceAsync();
}
})();
}, []);
return (
<TextInput
value={otp ?? ''}
placeholder="OTP auto-fills here"
onChangeText={clear}
/>
);
}Platform Support
| Platform | Supported | |---|---| | Android | ✅ Full support | | iOS | ❌ Not supported (iOS does not allow SMS access) | | Web | ❌ Not supported |
License
MIT © MULERx
