Packages

Server-side push notifications for Mob apps — APNs (iOS) and FCM (Android)

Current section

Files

Jump to
mob_push lib mob_push.ex
Raw

lib/mob_push.ex

defmodule MobPush do
@moduledoc """
Server-side push notifications for Mob apps.
A thin server library that wraps:
- **APNs HTTP/2** (iOS) — token-based auth with a p8 key
- **FCM HTTP v1** (Android) — OAuth2 via Google service account
Token storage and fan-out are out of scope — bring your own persistence.
## Setup
Add to your `mix.exs`:
{:mob_push, "~> 0.1"}
### iOS (APNs)
In Apple Developer portal, create an APNs Auth Key (`.p8` file) and note
your Key ID and Team ID. Then configure:
config :mob_push, :apns,
key_id: "XXXXXXXXXX",
team_id: "XXXXXXXXXX",
key_file: "/run/secrets/apns_auth_key.p8",
bundle_id: "com.example.myapp",
env: :production # :sandbox | :production
### Android (FCM)
In Firebase console → Project Settings → Service Accounts → Generate new
private key. Download the JSON file, then configure:
config :mob_push, :fcm,
project_id: "my-firebase-project",
service_account_key: "/run/secrets/fcm_service_account.json"
### Getting device tokens from the app
The Mob library calls these NIF functions on the native side:
- **iOS**: `mob_send_push_token(hexToken)` from `didRegisterForRemoteNotificationsWithDeviceToken`
- **Android**: `NotificationReceiver` BroadcastReceiver — token arrives as `{:push_token, token}`
These send `{:push_token, token}` to your `Mob.Screen` process. Store it
however you like (ETS, database, etc.) and pass it to `MobPush.send/3`.
## Usage
# Store tokens from handle_info/2 in your screen
def handle_info({:push_token, token}, socket) do
MyApp.PushTokens.save(socket.assigns.user_id, token, :ios)
{:noreply, socket}
end
# Send from your server (not from the app process)
MobPush.send("device_token_hex", :ios, %{
title: "New message",
body: "You have a new message from Alice",
data: %{screen: "messages", thread_id: "42"}
})
MobPush.send("fcm_registration_token", :android, %{
title: "New message",
body: "You have a new message",
data: %{screen: "messages", thread_id: "42"}
})
## Payload options
| Key | Type | Description |
|----------------------|---------|--------------------------------------------------|
| `:title` | string | Notification title (required) |
| `:body` | string | Notification body text (required) |
| `:data` | map | Arbitrary key-value data passed to the app |
| `:badge` | integer | iOS badge count |
| `:sound` | string | Sound name (`"default"` for system default) |
| `:content_available` | boolean | iOS silent push (wakes app in background) |
| `:android` | map | Raw FCM `android` message config |
## Return values
- `:ok` — notification accepted by APNs / FCM
- `{:error, :device_token_expired}` — token is no longer valid (deregister it)
- `{:error, :device_token_not_found}` — token unknown to FCM (deregister it)
- `{:error, :auth_failed}` — credentials rejected; check your config
- `{:error, {:apns_error, reason}}` — APNs rejected with `reason` string
- `{:error, {:fcm_error, status, message}}` — FCM error response
"""
@doc """
Send a push notification to a device.
`platform` is `:ios` or `:android`.
`payload` must include `:title` and `:body`. Optional keys: `:data`,
`:badge`, `:sound`, `:content_available`, `:android`.
"""
@spec send(device_token :: String.t(), platform :: :ios | :android, payload :: map()) ::
:ok | {:error, term()}
def send(device_token, :ios, payload), do: MobPush.APNS.send(device_token, payload)
def send(device_token, :android, payload), do: MobPush.FCM.send(device_token, payload)
def send(_token, platform, _payload), do: {:error, {:unknown_platform, platform}}
@doc """
Like `send/3` but raises on error.
"""
@spec send!(String.t(), :ios | :android, map()) :: :ok
def send!(device_token, platform, payload) do
case send(device_token, platform, payload) do
:ok -> :ok
{:error, reason} -> raise "MobPush.send!/3 failed: #{inspect(reason)}"
end
end
end