npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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

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.

npm version Platform License: MIT


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-listener

Add 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:android

Register 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