MobPush (mob_push v0.2.0)

Copy Markdown View Source

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 intentionally out of scope — bring your own persistence.

Setup

Add to your mix.exs:

{:mob_push, "~> 0.1"}

Then run mix mob_push.install for interactive credential setup, or configure manually in config/runtime.exs:

# iOS
config :mob_push, :apns,
  key_id:    "XXXXXXXXXX",         # 10-char Key ID from Apple Developer portal
  team_id:   "XXXXXXXXXX",         # 10-char Team ID from Membership Details
  bundle_id: "com.example.myapp",
  key_file:  "/run/secrets/AuthKey_XXXXXXXXXX.p8",
  env:       :production           # :sandbox | :production

# Android
config :mob_push, :fcm,
  project_id:          "my-firebase-project",
  service_account_key: "/run/secrets/fcm_service_account.json"

See the README for the full credential walkthrough.

Receiving device tokens in the app

In your Mob.Screen, request permission and register for push:

def on_mount(socket) do
  socket = Mob.Permissions.request(socket, :notifications)
  {:ok, socket}
end

def handle_info({:permission, :notifications, :granted}, socket) do
  {:noreply, Mob.Notify.register_push(socket)}
end

def handle_info({:push_token, platform, token}, socket) do
  MyApp.PushTokens.upsert(socket.assigns.user_id, token, platform)
  {:noreply, socket}
end

Sending notifications

MobPush.send(token, :ios, %{
  title:    "New message",
  body:     "Alice: Hey, are you free tonight?",
  subtitle: "in #general",
  badge:    3,
  sound:    "default",
  data:     %{screen: "chat", thread_id: "42"}
})

MobPush.send(token, :android, %{
  title: "New message",
  body:  "Alice: Hey, are you free tonight?",
  data:  %{screen: "chat", thread_id: "42"},
  android: %{
    "notification" => %{
      "icon"       => "ic_notification",
      "color"      => "#FF6200EE",
      "channel_id" => "messages"
    }
  }
})

Handling received notifications in the app

All three delivery scenarios (foreground, background tap, killed-then-tapped) deliver the same {:notification, notif} message to your screen:

def handle_info({:notification, notif}, socket) do
  # notif has string keys: "title", "body", "data"
  case get_in(notif, ["data", "screen"]) do
    "chat"  -> {:noreply, Mob.Socket.push_screen(socket, MyApp.ChatScreen)}
    _       -> {:noreply, socket}
  end
end

Payload options

KeyPlatformsTypeDescription
:titlebothstringNotification title (required)
:bodybothstringNotification body text (required)
:subtitleiOSstringSecond line under the title
:databothmapArbitrary key-value pairs delivered to the app
:badgeiOSintegerBadge count on the app icon (0 to clear)
:soundiOSstring"default" or a filename bundled in the app
:content_availableiOSbooleanSilent push — wakes app in background, no alert
:androidAndroidmapRaw FCM AndroidConfig for appearance customization

Return values

  • :ok — accepted by APNs / FCM
  • {:error, :device_token_expired} — stale APNs token; delete it
  • {:error, :device_token_not_found} — FCM doesn't know this token; delete it
  • {:error, :auth_failed} — credentials rejected; check your config
  • {:error, {:apns_error, reason}} — APNs rejected with a reason string
  • {:error, {:fcm_error, status, message}} — FCM HTTP error
  • {:error, :missing_apns_key_config}:key_file / :key_pem not configured
  • {:error, {:apns_key_file_unreadable, path, reason}}.p8 file not readable
  • {:error, :missing_fcm_service_account_config} — service account not configured

Summary

Functions

Send a push notification to a device.

Functions

send(device_token, platform, payload)

@spec send(device_token :: String.t(), platform :: :ios | :android, payload :: map()) ::
  :ok | {:error, term()}

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.

send!(device_token, platform, payload)

@spec send!(String.t(), :ios | :android, map()) :: :ok

Like send/3 but raises on error.