SMS Signals

Matched messages, straight to your endpoint.

Android MDM / sideload only
$ composer require smssignals/nativephp-sms-signals

Receive incoming SMS in NativePHP Mobile apps and forward matched messages straight to an HTTPS signals endpoint. For sideloaded and enterprise (MDM) distribution only — RECEIVE_SMS is restricted on Google Play.

  • Pattern-matched forwarding to your HTTPS signals endpoint
  • Runtime permission flow handled for you
  • Enterprise (MDM) and sideload distribution only — not Play Store

Documentation

Synced from GitHub 0 seconds ago

SMS Signals for NativePHP Mobile

Receive incoming SMS inside a NativePHP Mobile (v4) app and forward matched messages straight to an HTTPS endpoint — evaluated and delivered entirely on-device, in the background, with an offline retry queue.

[!CAUTION] This plugin uses the Android RECEIVE_SMS permission, which Google Play heavily restricts. Apps that read SMS are rejected from the Play Store unless they are the user's default SMS handler (which this plugin is not). Ship this only through sideloading or enterprise / MDM distribution (e.g. a managed device fleet, an internal APK, or a private app catalogue). If your app must go on Google Play, do not use this plugin — use a server-side SMS provider webhook (Twilio, Vonage, etc.) instead. See docs/DESIGN.md for the full policy and threat discussion.

[!NOTE] Android only. iOS provides no public API to intercept SMS; there is no iOS implementation and none is possible.


What it does

  1. A SMS_RECEIVED BroadcastReceiver (system-delivered, no Activity required) picks up every incoming SMS.

  2. Optional on-device sender / body regular expressions decide whether the message is relevant. Non-matching messages are dropped on the phone and never leave it.

  3. Matched messages are POSTed directly to your configured HTTPS endpoint with a bearer token:

    { "type": "sms", "payload": { "from": "+15550001111", "body": "Your code is 123456", "received_at": "2026-07-23T10:04:00Z" } }
    
  4. If the device is offline (or the server errors), the message is persisted to an encrypted on-device retry queue and re-sent by a network-constrained WorkManager worker.

It pairs with any Laravel backend — you expose one authenticated POST route that ingests the envelope. Nothing about the plugin is tied to a particular app.


Requirements

  • NativePHP Mobile ^4.0 (works today against the 4.0.0-rc.1 as 3.99.99 alias — see below), or ^3.0.
  • Android minSdk 26+.
  • PHP 8.2+, Laravel 11 / 12.

Installation

composer require smssignals/nativephp-sms-signals

NativePHP plugins are fail-closed: a plugin is only compiled into your app if your app's allowlist names its service provider. Register it:

php artisan native:plugin:register smssignals/nativephp-sms-signals

That command adds the provider to app/Providers/NativeServiceProvider::plugins(). Confirm it looks like this:

// app/Providers/NativeServiceProvider.php
public function plugins(): array
{
    return [
        // ...your other plugins...
        \SmsSignals\Sms\SmsServiceProvider::class,
    ];
}

Then (re)build the native project:

php artisan native:install --force
php artisan native:run

Validate the plugin manifest at any time:

php artisan native:plugin:validate

Configuration

Publish the config if you want to tune timeouts / queue sizes (optional):

php artisan vendor:publish --tag=sms-signals-config

Key options (config/sms-signals.php): connect_timeout_ms, read_timeout_ms, max_queue_size, max_retry_attempts, show_status_notification, plus optional env-driven endpoint / token / sender_pattern / body_pattern defaults for single-tenant builds.

In practice you seed the endpoint and a per-user token at runtime (see below), because the token is user-scoped and secret. The plugin stores it in EncryptedSharedPreferences.


Usage

use SmsSignals\Sms\Facades\Sms;

// 1. Ask for the runtime RECEIVE_SMS permission (and POST_NOTIFICATIONS on Android 13+).
Sms::requestPermission();

// 2. Seed the on-device encrypted config store. Endpoint MUST be https://.
Sms::configure([
    'endpoint'       => 'https://api.example.com/mobile/signals',
    'token'          => $user->currentAccessToken()->plainTextToken, // per-user bearer
    'sender_pattern' => '^(\\+?1)?(12345|BANKCO)$', // java.util.regex, on-device
    'body_pattern'   => '\\b\\d{4,8}\\b',           // e.g. only messages containing an OTP
]);

// 3. Start forwarding.
Sms::start();

// Later…
Sms::status();     // ['enabled' => true, 'configured' => true, 'encrypted_store' => true, 'queued' => 0, 'permission' => 'granted']
Sms::stop();       // pause forwarding (receiver stays registered, messages dropped)
Sms::flushQueue(); // manually retry anything queued while offline

Listening for events (SuperNative)

use SmsSignals\Sms\Events\SmsReceived;
use SmsSignals\Sms\Events\SmsForwarded;
use SmsSignals\Sms\Events\SmsForwardFailed;
use Native\Mobile\Attributes\OnNative; // provided by nativephp/mobile

#[OnNative(SmsReceived::class)]
public function whenSmsArrives(SmsReceived $e): void
{
    // $e->from, $e->body, $e->receivedAt
}

Inbound events are best-effort while the app is foregrounded; the background forward to your endpoint happens regardless of whether the app is open. Events raised while the app is closed are queued and replayed on next launch.


Pairing with your Laravel signals endpoint (generic example)

The forward envelope is small and predictable, so the receiving route is a few lines. Authenticate the bearer token, validate the shape, then hand off to your own domain logic.

// routes/api.php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

Route::post('/mobile/signals', function (Request $request) {
    $data = $request->validate([
        'type'               => 'required|in:sms',
        'payload.from'       => 'required|string|max:64',
        'payload.body'       => 'required|string|max:2000',
        'payload.received_at'=> 'required|date',
    ]);

    // $request->user() resolves from the Bearer token (e.g. Sanctum).
    IncomingSignal::create([
        'user_id'     => $request->user()->id,
        'channel'     => 'sms',
        'sender'      => $data['payload']['from'],
        'body'        => $data['payload']['body'],
        'received_at' => $data['payload']['received_at'],
    ]);

    return response()->json(['ok' => true], 202);
})->middleware('auth:sanctum');

Return any 2xx to acknowledge; anything else makes the device queue and retry.


Web-view apps (Livewire / Inertia)

SuperNative apps use the Sms facade directly. If you build a web-view app, a JS wrapper is provided at resources/js/index.js (configure, start, stop, status, requestPermission, checkPermission, flushQueue, and an Events map), and an <x-sms-signals::init /> Blade component that checks the permission on load.


Security notes

  • The endpoint is enforced to be https:// in both PHP (config validation) and Kotlin (before the POST). Plaintext http:// is refused because SMS bodies frequently contain OTPs / PII.
  • The token and filters live in EncryptedSharedPreferences (AndroidKeyStore-backed). If the keystore is unavailable the plugin falls back to plaintext storage and flags it in status() (encrypted_store: false).
  • Keep your sender_pattern / body_pattern narrow. Every non-matching message is discarded on-device; only what matches is transmitted.

See docs/DESIGN.md for architecture decisions and what still requires on-device validation.

License

Proprietary placeholder — the repository owner decides the terms. See LICENSE.