mob_notify
Local and push notifications for apps built with Mob
— the device half of Mob.Notify, extracted from mob core as a plugin. Owns
scheduling, cancellation, and push registration; pairs with the server-side
mob_push package for sending.
Installation
# mix.exs
{:mob_notify, "~> 0.1"}
# mob.exs
config :mob, :plugins, [:mob_notify]
Requires the :notifications permission
(Mob.Permissions.request(socket, :notifications)). Android 13+'s
POST_NOTIFICATIONS and the firebase-messaging gradle dep are merged from
this plugin's manifest; iOS needs no plist key.
Usage
Local notifications:
MobNotify.schedule(socket,
id: "reminder_1",
title: "Time to check in",
body: "Open the app to see today's updates",
at: ~U[2026-04-16 09:00:00Z], # or delay_seconds: 60
data: %{screen: "reminders"}
)
MobNotify.cancel(socket, "reminder_1")
def handle_info({:notification, %{presentation: :tap, id: id, data: data, source: :local}}, socket), do: ...
Push registration (call once after :notifications is granted):
socket = MobNotify.register_push(socket)
def handle_info({:push_token, platform, token}, socket) do
# platform is :ios | :android — store both, send with MobPush.send/3 server-side
end
def handle_info({:notification, %{presentation: :tap, title: t, body: b, data: d, source: :push}}, socket), do: ...
What arrives
Delivery lives in mob core; this plugin is the API surface. Every
notification arrives as {:notification, notif} (mob > 0.9.7; see
Mob.Notification):
%{
presentation: :tap, # :foreground = arrived while the app was open
action: "default", # nil for :foreground
source: :local, # or :push
id: "reminder_1",
title: "Time to check in",
body: "Open the app to see today's updates",
data: %{screen: "reminders"} # atom keys at the top level
}
A :foreground arrival still shows the system banner; tapping it then
delivers :tap. A tap that launched the app from a killed state arrives at
the root screen once it has mounted, exactly once.
It goes to the process that last called register_push/1 (on iOS, also
schedule/2) while that process is alive, otherwise to the screen currently
showing.
On Android, this plugin ships the app's FCM receiver, MobFirebaseService,
and declares it (and the boot re-arm receiver) in the app's manifest at
native build time; the app needs no Kotlin of its own:
- Tap on a push the system tray showed (a message with a
notificationblock, sent while the app was in the background or killed): one:tap, on cold launch, warm launch, or from the shade while the app is open again.idis the FCM message id anddatathe message's data keys (FCM's owngoogle.*,fromandcollapse_keyare dropped);titleandbodyarenil, because Android doesn't pass the displayed text to the app. A push from mob_push carriesmob_notification_json, which the generatedMainActivitydelivers instead, with the title and body. Each tap is delivered once, also when Android re-creates the activity or relaunches it from Recents. - Push arriving while the app is in the foreground: one
:foreground. For a message with anotificationblock mob_notify also shows the banner (Android shows none for a foreground app); tapping it delivers:tap. - Data-only push while the app is in the background: not delivered
(it's a silent push, not a notification). One with a
mob_wake_idgoes to mob_wake in any state, when the app has it. - Token refresh:
{:push_token, :android, token}to the process that calledregister_push/1, or on its nextregister_push/1if none has. :foregroundarrivals of local notifications, and taps while nothing is registered, need theNotificationReceiverandMainActivitythat mob_new 0.6.3 generates. Apps generated earlier get no:foregroundarrival for a local notification and lose a warm tap when nothing is registered.
Android hands FCM to one service. An app that declares its own
FirebaseMessagingService (the MobFirebaseService mob_new 0.1.45–0.4.10
generated, or mob_wake's MobWakeFcmService) keeps receiving FCM through it
and mob_notify's stands by. Delete the app's declaration and class to switch
to mob_notify's, which also forwards mob_wake's messages and tokens. Until
then the app's own service must add "presentation": "foreground" to the JSON
it passes to MobBridge.nativeDeliverNotification (both the
mob_notification_json string and the object it builds), or mob reads every
push that arrives while the app is open as a tap.
Host app requirements
Manual steps the build can't automate (the FCM service and boot receiver are declared by the native build, mob_dev >= 0.6.19):
- Android — Firebase wiring: the host
build.gradleneeds thecom.google.gms.google-servicesplugin + agoogle-services.jsonfrom the Firebase console (buildscript classpath entries are host-level). mob_new doesn't generate this, so add it by hand; Android push needs it. - iOS — APNs token forwarding: the host AppDelegate must call
mob_send_push_token(hex)(exported by mob core) indidRegisterForRemoteNotificationsWithDeviceToken(the mob_new template does). - Android — display receiver: scheduled notifications display via a
<applicationId>.NotificationReceiverBroadcastReceiver declared inAndroidManifest.xml(the mob_new template ships it) — this plugin only arms the alarm. - iOS silent APNs: see the manifest's
host_requirements(Info.plist background mode, Apple Developer Portal push capability).
Limits
- Local scheduling is device-verified on both platforms. Android FCM receipt (foreground arrival, banner tap, warm and cold tray taps, token on first launch) is verified on an API 35 emulator; iOS remote push needs real APNs credentials plus the mob_push server side.
- An unauthorized iOS app drops scheduled notifications silently —
request
:notificationsbefore scheduling. - The wire contract with mob_push is pinned by shared fixtures
(
test/fixtures/push_contract.exs, vendored identically in both repos). - On-device check: from a host app that activates this plugin,
mix mob.selftestrunsMobNotify.SelfTest, which reads the app's notification authorization through the NIF (no prompt, nothing posted) to prove the native side is linked.
Development
Clone, then run once:
mix setup
That fetches deps and activates the repo's git hooks (.githooks/pre-push):
mix format --check, mix credo --strict (incl. ExSlop), and mix compile --warnings-as-errors run on every push, plus the full test
suite when mix.exs changes — the same gate CI enforces before publishing.
License
MIT