Push notifications
APNs device token, push subscriptions, listeners, click tracking, and delivered notifications on iOS.
This document describes behavior implemented by the MotiSig SDK. Your app still needs the usual Xcode capabilities (Push Notifications, background modes if required) and provisioning with APNs.
What initialize installs
On the main queue, the SDK installs notification-center observation and AppDelegate method swizzling so it can receive the APNs device token and intercept notification delivery. It also starts permission monitoring and may request notification permission and register for remote notifications (unless disabled for tests).
See the MotiSig.initialize documentation in the SDK source for the exact statement of installed behavior.
Device token and push subscription
When an APNs device token arrives, it is stored. If a user id is already set, the SDK upserts a push subscription (POST …/push-subscriptions) with devicePlatform: ios, pushType: apns, the token, the current notification permission (granted / declined / unknown from UNAuthorizationStatus), and the customer enabled flag (see below). If setUser completes after the token exists, the SDK performs the same upsert at that point. If the token string changes, the SDK removes the old subscription then upserts the new one.
setNotificationEnabled(_:)
This is the customer-controlled server flag for whether this device’s subscription is enabled. It is persisted locally (survives logout()). Call it when your app UI lets the user turn MotiSig-delivered pushes on or off for this device; the SDK sends PATCH …/push-subscriptions with enabled. It does not change OS notification permission.
Permission vs enabled
When the user changes notification authorization in Settings and returns to the app, the SDK patches the push subscription permission field only (it does not infer enabled from permission). notDetermined maps to unknown until the user responds to the prompt.
Notification listeners
Register a listener that conforms to MotiSigNotificationListener:
let subscription = MotiSig.shared.addNotificationListener(self, order: 0)
// later:
subscription.remove()Behavior:
- Listeners are held weakly. Retain your listener object for as long as you need callbacks.
ordercontrols sort priority: lower values are notified first.nilbehaves like0. For the same order, registration order (FIFO) applies.- Callbacks are delivered on the main queue via
motiSig(didReceiveNotification:inForeground:). - If no listeners are registered when an event arrives, the SDK buffers a bounded set of events; when you add a listener, buffered events are replayed on the main queue before new live events.
MotiSigNotificationSubscription.remove()is idempotent and safe from any thread. Dropping the subscription object without callingremove()does not unregister the listener.logout()andremoveAllNotificationListeners()clear listeners and the buffer.
MotiSigNotification
Contains optional messageId, title, body, and userInfo (string-keyed copy of the APNs payload). When the event originates from UserNotifications (delegate delivery or fetchDeliveredNotifications), requestIdentifier is set to UNNotificationRequest.identifier when non-empty; synthetic or test payloads may leave it nil.
Fetching delivered notifications (explicit)
MotiSig.shared.fetchDeliveredNotifications() returns [MotiSigDeliveredNotification], each with requestIdentifier, date (system delivery time), and notification (MotiSigNotification) for every entry still present in the system Notification Center (UNUserNotificationCenter.getDeliveredNotifications).
Call this when your app wants to reconcile in-app state with the OS list—for example on scenePhase == .active or UIApplication.didBecomeActiveNotification—so pushes that arrived while the app was backgrounded appear even if the user opened the app from the home screen without tapping the notification.
This is not strictly “only while inactive”: the delivered list can overlap with listener callbacks (foreground willPresent or tap) until the user dismisses notifications from Notification Center. Deduplicate using requestIdentifier (and/or messageId) against rows you already recorded from MotiSigNotificationListener.
On watchOS the method returns an empty array.
Click tracking
The SDK sends POST /track/click when a payload contains messageId and a user is set (or becomes set before the queued click is sent). Clicks are enqueued through an internal ClickDispatcher rather than fired as a single fire-and-forget HTTP call.
Automatic tracking runs on these paths:
- Tap —
UNUserNotificationCenterdidReceiveresponse (and cold-start delivery; see below). - Foreground delivery —
willPresent(isForeground: trueon the track-click payload). Listener delivery still respectssuppressForegroundon the payload.
If messageId is missing, tracking is skipped (see SDK logs at .debug). If no user is set yet, the click stays in the queue until setUser completes, then drains.
You can also call MotiSig.shared.trackClick(messageId:isForeground:completion:) manually; it enqueues the same way.
Reliability
- Persistent FIFO — Pending clicks are stored on disk and survive app restarts.
- Deduplication — The same
messageIdis not sent twice (ClickDedupeStore). Cold-start delivery vialaunchOptions[.remoteNotification]and a laterdidReceivefor the same tap are deduped. - Retries — Transient failures (network, 408, 429, 5xx) are retried with exponential backoff until success or max attempts. Non-retryable 4xx responses are dropped with a log at
.debug/.error. logout()— Clears the pending click queue and dedupe store for that install.
Cold-start tap
Pass launchOptions from application(_:didFinishLaunchingWithOptions:) into MotiSig.initialize (see Getting started and Configuration). When the user launched the app by tapping a notification, launchOptions[.remoteNotification] is delivered during initialize() so click tracking and listener callbacks run before your UI appears.
Scene-based SwiftUI apps may also receive the same tap through the notification-center delegate. The SDK marks cold-start payloads so a duplicate didReceive for the same notification is ignored.
Rich notification images (Notification Service Extension)
Apple does not download remote image URLs for you. To show an image in the banner, the push needs mutable-content: 1 and your app needs a Notification Service Extension that calls MotiSigRichPushHandler.handle(...).
Full setup (Xcode steps, payload contract, foreground gotchas, troubleshooting): see Rich notification images.