All notable changes to mob_sms are documented here.
Format: Keep a Changelog. Versioning: SemVer.
[0.2.0] - 2026-09-18
Added
MobSms.OneTimeCode.arm(socket, on_receive: tag)— cross-platform SMS-delivered OTP autofill. Both platforms deliver the code as{:change, tag, code}(same shape a text field'son_changefires), so onehandle_infoclause covers iOS QuickType autofill AND Android's SMS Retriever. Pass the SAME atom as your text field'son_changetag.- iOS: no-op at the Elixir layer. Autofill is driven by the new
text_content_type: :one_time_codeprop on<TextField>(added in mob 0.9.1). iOS Messages parses the incoming SMS, surfaces the code as a QuickType suggestion above the keyboard, user taps to fill. Code flows in via the field's normalon_change. No entitlement, no framework, no server-side hash. - Android: registers a scoped BroadcastReceiver (only Google Play Services can broadcast to it, via the
com.google.android.gms.auth.api.phone.permission.SENDgate) and callsSmsRetrieverClient.startSmsRetriever(). Delivery lands via a short-lived Elixir proxy process that translates the raw{:sms_otp, code}into{:change, tag, code}and forwards to the caller. NoREAD_SMSpermission, no Play Store review gate. - Sad path: on Android,
{:sms_otp, :timeout}fires on 5-min retriever timeout, GMS start failure, or eviction by a subsequentarm/2. Optional handler — a retype prompt is a fine default. - Server-side SMS format for Android: body + blank line + 11-char hash suffix. Full spec +
AppSignatureHelpersnippet for computing the hash inMobSms.OneTimeCode's @moduledoc.
- iOS: no-op at the Elixir layer. Autofill is driven by the new
- Demo screen now exercises the OTP flow alongside the composer.
Changed
- mob floor bumped to
~> 0.9.1. The OTP flow's iOS half needs thetext_content_typeprop, added in that release. Bump generated apps accordingly. - Plugin manifest declares
com.google.android.gms:play-services-auth-api-phone:18.0.2as agradle_depscontribution. Present on 99%+ of Play-installed devices; aftermarket ROMs without Google Play Services fall straight to{:sms_otp, :timeout}fromarm/2.
Fixed (0.2.0 review pass, before publish)
- Kotlin bridge: re-arm evicts the previous receiver AND delivers
{:sms_otp, :timeout}to the prior caller instead of leaving it hanging. Idempotency guard ononReceive(compare receiver identity, not just presence) prevents double-delivery on queued-post-unregister edge.startSmsRetrieverwrapped intry/catchfor the synchronous-throw path GMS-absent devices sometimes take. - Kotlin regex:
extractCodenow prefers the LAST 4–8-digit run in the SMS body proper (stripping the 11-char hash suffix first). Previous.find(body)behaviour returned the first digit run — "MyApp2024 code is 123456" would deliver "2024" instead of "123456". - Zig NIF:
sendSmsOtphandles theenif_make_new_binaryOOM path.
[0.1.2] - 2026-09-18
Fixed
- iOS: use-after-free crash on overlapping composes (adversarial review of 0.1.1 caught this).
MobSmsDelegate'srelease_activecalledremoveAllObjectson the retention array — if two composes overlapped, the first one to finish dropped every strong ref, including the still-active second delegate. UIKit then delivereddidFinishWithResult:on a dangling pointer. Now takes the finishing delegate and callsremoveObject:so overlapping composes each survive to their own callback. Reproduction: fireMobSms.compose/2twice from a screen without waiting for the first{:sms, _}message; the second sheet's Send/Cancel crashed the app. - iOS: top-VC walk now bails when the top VC is mid-transition. Presenting on a VC whose
isBeingDismissedorisBeingPresentedis set silently no-ops in UIKit and the delegate never fires, leaving the caller waiting forever for a{:sms, _}. The plugin now returns:not_availablein that state. - Android: JVM-teardown and never-registered-bridge paths delivered no result to the caller.
callBridgeCompose'sget_jenvand null-bridge-class branches previously returned:errorwithout sending{:sms, :not_available}— the caller waited forever. Both branches now honour the "one message per compose" contract.
Docs
- Comment on
sms_str_from_iolist(iOS NIF) explains the correct-but-double-negate!enif_inspect_binary && !enif_inspect_iolist_as_binaryguard so a future reader doesn't "clean it up" into the wrong semantics.
Tests
MobSms.compose/2's argument-coercion path is now split intoMobSms.normalize_opts/1, a testable helper. The previous tests wrapped the coercion insideassert_raise ErlangError, which fired on the missing NIF instead of on the coercion — deleting the|> to_string()calls did not fail the suite. The rewritten tests observenormalize_opts/1's output directly, so a regression to the coercion is caught. Also assertsProtocol.UndefinedErrorpropagates for values with noString.Charsimpl (rather than being swallowed).
[0.1.1] - 2026-09-18
Docs
- Rework the "not silent-send" section of
MobSms's @moduledoc and the same section in README.md so ex_doc's function-link resolver isn't asked to resolve a hypothetical futuresend/2on this module. Removes amix docswarning; the deferred-until-a-user-asks stance on silent send is unchanged.
[0.1.0] - 2026-09-18
Initial release. Cross-platform SMS composer plugin.
Added
MobSms.compose/2— opens the system SMS composer pre-filled with the given:toand:body. Async result delivered as{:sms, atom}to the calling process.iOS:
MFMessageComposeViewControllerpresented from the top VC, delegate reports:sent | :cancelled | :failed.- Android:
Intent.ACTION_SENDTOwithsmsto:hands off to the user's default SMS app, delivers:composer_opened(Android cannot observe the send outcome). - Both platforms deliver
:not_availablewhen the device cannot send SMS (simulator on iOS; no SMS-capable app on Android). MobSms.DemoScreen— a ready-to-run screen exercising the composer flow, auto-listed by the host's plugin home if it enumeratesMob.Plugins.screens/0.- Plugin manifest declares the MessageUI framework on iOS and no permissions on either platform.