ApplePushNotifications (apple_push_notifications v0.3.0)

Copy Markdown View Source

Elixir client for the Apple Push Notification service (APNs).

Provides a complete interface for sending push notifications to iOS, macOS, watchOS, and tvOS devices.

Features

  • HTTP/2 connection support (required by APNs)
  • JWT-based authentication with P8 keys
  • Automatic token caching with expiration buffer
  • Rich notification support (images, actions, custom UI)
  • VoIP push support
  • Comprehensive error handling with device token validation

Quick Start

# Configure in config/config.exs
config :apple_push_notifications,
  team_id: System.get_env("APNS_TEAM_ID"),
  key_id: System.get_env("APNS_KEY_ID"),
  bundle_id: System.get_env("APNS_BUNDLE_ID"),
  private_key: System.get_env("APNS_PRIVATE_KEY"),
  sandbox: true  # Set to false for production

# Send a simple push notification
ApplePushNotifications.push(
  "a1b2c3d4e5f6789...",
  alert: "Hello World!",
  badge: 1
)

# Send a rich notification with custom payload
ApplePushNotifications.push(
  "a1b2c3d4e5f6789...",
  alert: %{title: "Breaking News", body: "Something happened!"},
  sound: "news_alert.caf",
  custom: %{article_id: "12345", category: "sports"}
)

Configuration

Required configuration:

  • team_id: Your Apple Developer Team ID (10 characters)
  • key_id: Your APNs Auth Key ID (10 characters)
  • bundle_id: Your app's bundle identifier (e.g., "com.example.myapp")
  • private_key: The P8 private key content (or use private_key_path)
  • sandbox: true for development, false for production

Optional configuration:

  • base_url: Override the APNs endpoint (default: api.push.apple.com or api.sandbox.push.apple.com)
  • token_ttl_seconds: JWT expiration time (default: 1200 seconds / 20 minutes)

Per-Call Options

Every function accepts per-call opts that override the application config:

ApplePushNotifications.push(
  device_token,
  alert: "Hello",
  bundle_id: "com.example.differentapp"  # Override bundle_id for this call
)

Error Handling

APNs returns specific error codes for different failure scenarios:

  • BadDeviceToken: Invalid or malformed device token
  • Unregistered: Device token is no longer valid (app uninstalled or token expired)
  • PayloadTooLarge: Notification payload exceeds 4KB (4096 bytes)
  • TooManyRequests: Rate limit exceeded
  • InternalServerError: APNs server error, retry later

Use ApplePushNotifications.Error.invalid_token?/1 to check if an error indicates the device token should be removed from your database.

Background Notifications

Send silent background notifications:

ApplePushNotifications.background_push(
  device_token,
  content_available: true,
  custom: %{sync_data: true}
)

VoIP Push Notifications

Send VoIP pushes for CallKit integration:

ApplePushNotifications.voip_push(
  device_token,
  handle: "+1234567890",
  display_name: "Incoming Call"
)

Interruption Levels (iOS 15+)

Control notification urgency:

ApplePushNotifications.push(
  device_token,
  alert: "Important!",
  interruption_level: :time_sensitive,  # :passive, :active, :time_sensitive, :critical
  relevance_score: 0.75
)

Summary

Functions

Send a silent background notification.

Check if an error indicates an invalid/unregistered device token.

Send a push notification to a device.

Send a notification to multiple devices.

Return a cached APNs access token (after JWT generation).

Validate a device token format.

Send a VoIP push notification for CallKit.

Types

opts()

@type opts() :: keyword()

response()

@type response() :: {:ok, map()} | {:error, term()}

Functions

background_push(device_token, opts \\ [])

@spec background_push(String.t(), opts()) :: response()

Send a silent background notification.

Background notifications wake up the app to perform work without alerting the user.

Parameters

  • device_token: The device token
  • opts:
    • content_available: Must be true (default)
    • custom: Custom data for background processing
    • Per-call config overrides

Examples

ApplePushNotifications.background_push(token,
  custom: %{fetch_new_data: true, endpoint: "/api/sync"}
)

invalid_token_error?(error)

@spec invalid_token_error?(term()) :: boolean()

Check if an error indicates an invalid/unregistered device token.

Returns true if the error is one of:

  • BadDeviceToken: Token format is invalid
  • Unregistered: Token is no longer valid (app uninstalled, token rotated)

When this returns true, you should remove the device token from your database.

Examples

case ApplePushNotifications.push(token, alert: "Hello") do
  {:ok, _} -> :ok
  {:error, error} ->
    if ApplePushNotifications.invalid_token_error?(error) do
      # Remove token from database
      User.remove_device_token(user_id, token)
    end
end

push(device_token, opts \\ [])

@spec push(String.t(), opts()) :: response()

Send a push notification to a device.

Parameters

  • device_token: The 64-character hex device token
  • opts:
    • alert: String alert message or map with title/body/subtitle
    • badge: Integer badge count (nil to leave unchanged)
    • sound: Sound file name or "default"
    • custom: Map of custom payload data
    • interruption_level: One of :passive, :active, :time_sensitive, :critical
    • relevance_score: Float between 0 and 1 (iOS 15+)
    • Per-call config overrides (team_id, key_id, bundle_id, etc.)

Examples

# Simple text notification
ApplePushNotifications.push(token, alert: "Hello!")

# Rich notification with badge
ApplePushNotifications.push(token,
  alert: %{title: "News", body: "New article available"},
  badge: 5,
  sound: "news.caf"
)

# With custom data
ApplePushNotifications.push(token,
  alert: "Update available",
  custom: %{version: "2.0", force_update: false}
)

push_many(device_tokens, opts \\ [])

@spec push_many([String.t()], opts()) :: {:ok, map()} | {:error, term()}

Send a notification to multiple devices.

Note: Unlike Firebase Cloud Messaging, APNs does not support multicast in a single request. This function iterates over device tokens and sends individual requests.

Parameters

  • device_tokens: List of device tokens
  • opts: Same options as push/2

Returns

  • {:ok, results}: Map of device_token to :ok or {:error, reason}
  • {:error, reason}: Configuration error before sending

Examples

ApplePushNotifications.push_many([token1, token2, token3],
  alert: "Broadcast message",
  badge: 1
)

token(opts \\ [])

@spec token(opts()) :: {:ok, String.t()} | {:error, term()}

Return a cached APNs access token (after JWT generation).

valid_token?(token)

@spec valid_token?(String.t()) :: boolean()

Validate a device token format.

Device tokens should be exactly 64 hexadecimal characters.

Examples

ApplePushNotifications.valid_token?("a1b2c3d4e5f6...")  # true/false

voip_push(device_token, opts \\ [])

@spec voip_push(String.t(), opts()) :: response()

Send a VoIP push notification for CallKit.

VoIP pushes use a different topic (bundle ID + ".voip").

Parameters

  • device_token: The VoIP device token
  • opts:
    • handle: Phone number or handle for the call
    • display_name: Caller display name
    • custom: Additional CallKit data
    • Per-call config overrides (bundle_id will have ".voip" appended)

Examples

ApplePushNotifications.voip_push(token,
  handle: "+1-555-123-4567",
  display_name: "John Doe",
  custom: %{uuid: "...", session_id: "..."}
)