Packages

Local + push notifications for Mob apps — device half, pairs with mob_push (extracted from mob core)

Current section

Files

Jump to
mob_notify README.md
Raw

README.md

# mob_notify

Local and push notifications for apps built with [Mob](https://hexdocs.pm/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](https://hex.pm/packages/mob_push) package for sending.

## Installation

```elixir
# 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:

```elixir
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):

```elixir
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`](https://hexdocs.pm/mob/Mob.Notification.html)):

```elixir
%{
  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 `:tap` when the
  user taps it, provided it carries mob_push's `mob_notification_json` data
  field, which `MobPush` always adds. One sent without it (the FCM console,
  another sender) carries no mob payload and does not arrive.
- `:foreground` arrivals of local notifications, and taps while nothing is
  registered, need the `NotificationReceiver` and `MainActivity` that mob_new
  0.6.3 generates. Apps generated earlier get no `:foreground` arrival 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 to `MobBridge.nativeDeliverNotification` (both the
  `mob_notification_json` string 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:

1. **Android — FCM service**: `AndroidManifest.xml` must declare
   `<service android:name=".MobFirebaseService" android:exported="false">`
   with the `com.google.firebase.MESSAGING_EVENT` intent filter. The
   `MobFirebaseService.kt` class ships in the host app, not this plugin
   (`FirebaseMessagingService` subclasses must live in the host package).
2. **Android — Firebase wiring**: the host `build.gradle` needs the
   `com.google.gms.google-services` plugin + a `google-services.json` from
   the Firebase console (buildscript classpath entries are host-level).
3. **iOS — APNs token forwarding**: the host AppDelegate must call
   `mob_send_push_token(hex)` (exported by mob core) in
   `didRegisterForRemoteNotificationsWithDeviceToken`.
4. **Android — display receiver**: scheduled notifications display via a
   `<applicationId>.NotificationReceiver` BroadcastReceiver declared in
   `AndroidManifest.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 `:notifications` before 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:

```bash
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