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:
- A push the system tray shows for FCM on its own arrives as
:tapwhen the user taps it, provided it carries mob_push'smob_notification_jsondata field, whichMobPushalways adds. One sent without it (the FCM console, another sender) carries no mob payload and does not arrive. :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.- An app with its own
MobFirebaseService(generated by mob_new 0.1.45–0.4.10) must add"presentation": "foreground"to the JSON it passes toMobBridge.nativeDeliverNotification(both themob_notification_jsonstring and the object it builds). Without it mob reads every push that arrives while the app is open as a tap.
Host app requirements
Four manual steps the build can't automate. mob_new-generated apps already satisfy all of them via their templates; hand-rolled hosts must add:
- Android — FCM service:
AndroidManifest.xmlmust declare<service android:name=".MobFirebaseService" android:exported="false">with thecom.google.firebase.MESSAGING_EVENTintent filter. TheMobFirebaseService.ktclass ships in the host app, not this plugin (FirebaseMessagingServicesubclasses must live in the host package). - Android — Firebase wiring: the host
build.gradleneeds thecom.google.gms.google-servicesplugin + agoogle-services.jsonfrom the Firebase console (buildscript classpath entries are host-level). - iOS — APNs token forwarding: the host AppDelegate must call
mob_send_push_token(hex)(exported by mob core) indidRegisterForRemoteNotificationsWithDeviceToken. - Android — display receiver: scheduled notifications display via a
<applicationId>.NotificationReceiverBroadcastReceiver declared inAndroidManifest.xml— this plugin only arms the alarm.
Limits
- Local scheduling is device-verified on both platforms. Live remote push needs real APNs/FCM 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).
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