mob_sms
SMS composer for Mob apps. Opens the user's default messaging app pre-filled with recipient + body; the user taps Send to actually send. No dangerous permissions on either platform. Cross-platform surface (MobSms.compose/2), per-platform delivery asymmetry documented honestly.
Install
Requires mob 0.9.1 or newer (the OTP flow's iOS half needs the
text_content_type prop on <TextField>, added in that release).
def deps do
[
{:mob, "~> 0.9.1"},
{:mob_sms, "~> 0.2"}
]
end
In mob.exs, activate the plugin:
config :mob, :plugins, [:mob_sms]
Usage
MobSms.compose(socket,
to: "+15551234567",
body: "Join me on Sample App: https://example.com/i/ABC123"
)
Both :to and :body are optional. Omit :to to let the user pick a recipient from Contacts inside the composer.
Result arrives as a message to the calling process:
def handle_info({:sms, :sent}, socket), do: ... # iOS
def handle_info({:sms, :cancelled}, socket), do: ... # iOS
def handle_info({:sms, :composer_opened}, socket), do: ... # Android
def handle_info({:sms, :not_available}, socket), do: ... # both
Per-platform delivery — the asymmetry to know
-
iOS —
MFMessageComposeViewControlleris presented as a sheet inside your app. The system delegate reports:sent,:cancelled, or:failedreliably.:not_availablemeans the device cannot send SMS (simulator, no cellular / iMessage capability, no SIM). -
Android —
Intent.ACTION_SENDTOwithsmsto:opens the user's default SMS app externally. Android does not reliably tell the host app whether the user tapped Send or cancelled — most SMS apps do not set an activity result. The plugin delivers:composer_openedonce the composer is showing (or:not_availableif no SMS-capable app is installed) and stops there. If knowing what the user did afterwards matters to your flow, plan around this asymmetry rather than assuming a:sent.
This asymmetry is by design at the platform layer, not something the plugin can paper over. Android intentionally hands the user off to a separate app rather than embedding a sheet; iOS presents inline and can observe the outcome.
Common use cases
- Invite codes — "Join me on X, here's the link" — the canonical fit
- Share a link / share a file — user picks a friend inside the composer
- Tell a friend — referral flows without your app needing to know the friend's number
- Verification workaround — send a code to a manually-provided number without SMS gateway costs
One-time code (OTP) autofill
The 0.2.0 addition. Verification codes delivered by SMS flow into the app
without the user typing them — iOS via QuickType autofill above the keyboard,
Android via Google Play Services' SMS Retriever (no READ_SMS permission,
no Play Store review gate).
# On the screen owning the OTP field. Pass the SAME atom to `on_receive`
# that the text field uses for `on_change` — both platforms deliver the
# code as {:change, tag, value}, matching the field's normal keystroke
# shape.
def mount(_params, _session, socket) do
{:ok, MobSms.OneTimeCode.arm(socket, on_receive: :code)}
end
def render(assigns) do
~MOB"""
<TextField
value={@code}
on_change={:code}
keyboard={:number}
text_content_type={:one_time_code}
/>
"""
end
def handle_info({:change, :code, value}, socket) do
# Fires on BOTH platforms — iOS QuickType tap OR Android SMS Retriever.
# The plugin translates Android's raw delivery into the on_change shape.
{:noreply, Mob.Socket.assign(socket, code: value)}
end
def handle_info({:sms_otp, :timeout}, socket) do
# Optional. Android-only sad path: 5-min retriever timeout, GMS
# failure, or evicted by a subsequent arm/2. iOS never fires this.
{:noreply, socket}
end
The text_content_type: :one_time_code prop tells iOS Messages to
surface the code in QuickType. arm/2 starts the Android SMS Retriever
and spawns a tiny proxy that translates the delivery into the same
{:change, tag, code} shape iOS uses — one handler per screen.
Server-side SMS format for Android:
Your Sample App code is: 123456
<blank line>
FA+9qCX9VSu
The 11-char suffix is base64(sha256("<package_name> <signing_cert_sha256_hex>"))[:11]
— it's what Google Play Services uses to route the SMS to your specific
app. See MobSms.OneTimeCode's docs for the debug-vs-release cert
distinction and Google's AppSignatureHelper snippet for computing the
hash during development. iOS doesn't need the hash — its SMS format is
any natural-language "your code is 123456" pattern.
See the full contract, per-platform behaviour, and edge cases in
MobSms.OneTimeCode's module documentation.
What this plugin is NOT
Not silent-send. iOS has no public API for sending SMS without the user tapping Send; Apple treats that as a bright-line spam surface. Android can silent-send via SmsManager.sendTextMessage under the SEND_SMS runtime permission, but Google Play rejects that permission request for most non-SMS-centric apps (the "Permissions Declaration" gate).
If you have a real reason to bypass the sheet on Android — and can accept iOS being permanently in composer mode — a separate send/2 on this module can land in a later release. Talk to us before designing around it.
Related
- MOB epic MOB-257 — the mob_wake plugin arc, which covers the receive side of push notifications for apps that want the "wake me when something happens" flow that people often reach for background tasks to build.
License
MIT.