Skip to main content
Supported on: iOS SDK 1.0.61+. The automatic notification proxy, its Info.plist keys, the forwarding API (AppDNA.pushModule.isAppDNAMessage / handleMessageData / handleNotificationTap) and AppDNA.installNotificationProxy() need iOS SDK 1.0.82+ (Flutter 1.0.20+, React Native 1.0.19+).
The AppDNA SDK provides a complete push notification module for registering device tokens, handling notification delivery, and tracking user interactions.

APNs Setup

Before using push notifications, configure Apple Push Notification service (APNs) for your app:
  1. In Xcode, enable the Push Notifications capability under your target’s Signing & Capabilities tab.
  2. Generate an APNs authentication key (.p8 file) in the Apple Developer Portal.
  3. In the AppDNA Console under Settings > Push, in the APNs (iOS) card, paste the contents of the .p8 file into P8 Auth Key, fill in Key ID, Team ID and Bundle ID, pick the Environment, and click Save.
Without the APNs capability enabled and the .p8 key saved in the Console, push notifications will not be delivered to your users.

Request Permission

Request push notification permission from the user. This presents the system permission dialog and registers for remote notifications:
The method returns a Bool indicating whether the user granted permission.

Set Token Manually

If you handle token registration yourself (e.g., in application(_:didRegisterForRemoteNotificationsWithDeviceToken:)), pass the raw token data to the SDK:
AppDNA sends iOS pushes directly over APNs, so pass the APNs device token. If your app also uses Firebase Messaging, never pass the FCM token (Messaging.messaging().fcmToken / getToken()) on iOS — every iOS push to that device then fails.

Set Permission Status

Manually update the SDK with the current push permission status:

How push works on iOS

Requires an AppDNA server from this release or later for delivered / tapped tracking (older servers send no appdna marker, push_id or delivery_id, so their pushes are not recognised as AppDNA pushes: no tracking, no callbacks, no tap routing).
The SDK installs a notification-centre delegate proxy automatically at app launch — no AppDelegate changes are needed. The install happens right after application(_:didFinishLaunchingWithOptions:) returns. It wraps whatever UNUserNotificationCenter delegate exists at that moment (your own, FlutterAppDelegate, or a push library’s), so a delegate you set in didFinishLaunching is wrapped. A delegate you assign later replaces the proxy, and it cannot be re-wrapped: the proxy is installed once per launch, and after that installNotificationProxy() does nothing. (If your build dropped the SDK’s launch hook, configure installs the proxy instead and logs a warning.) In that case either set your delegate in didFinishLaunching instead, or forward AppDNA pushes from it yourself — AppDNA.pushModule.handleMessageData in willPresent and handleNotificationTap in didReceive, as shown in Forwarding yourself. The proxy does not change how your own notifications are handled: every notification that is not an AppDNA push is forwarded to the previous delegate when it implements the method (otherwise it is completed with no presentation, the iOS default). The exception is the AppDNAForegroundPresentation trade-off below. AppDNA pushes (marked appdna: "1" in the payload) are handled by AppDNA and are not forwarded:
  • Tap (warm or cold start) — tracked once as push_tapped, onPushTapped fires, and the AppDNA action is routed (show_screen, deep link / URL). A push delivered or tapped before configure (for example a cold-start tap) is buffered — up to 8 — and handled once configure has set up push; beyond 8, further ones are dropped. No forwarding code is needed.
  • Foreground delivery — tracked as push_delivered and onPushReceived(…, inForeground: true) fires when AppDNA handles foreground presentation (below). Through the proxy, delivered tracking covers foreground delivery only: AppDNA pushes do not wake the app in the background, so background delivery is not tracked. A delivery you forward with handleMessageData is tracked too.
If a push library installs its own delegate later and does not chain to the previous one, AppDNA stops auto-tracking (AppDNA.diagnose() reports notificationDelegate: wrapped-or-replaced); forward with the AppDNA.pushModule API below.

Info.plist keys

Requires iOS SDK 1.0.82+ (Flutter 1.0.20+, React Native 1.0.19+). These are Info.plist keys, not AppDNAOptions — they are read at launch, before any runtime option could arrive. Foreground presentation and push libraries. When no other delegate and no push library (FlutterFire, React Native Firebase, notifee) is present, AppDNA pushes are shown in the foreground with the AppDNA options and tracked. When a push library is linked and ends up outside AppDNA, that library keeps deciding foreground presentation for every notification, AppDNA’s included, and AppDNA’s foreground delivered tracking does not run — unless the delegate AppDNA wrapped implements willPresent or you set AppDNAForegroundPresentation.
Trade-off of AppDNAForegroundPresentation: if AppDNA ends up innermost with no previous delegate under a push library (for example React Native Firebase), setting the key makes that library defer foreground presentation to AppDNA, and your own (FCM) pushes then get no foreground presentation. Set the key only if foreground tracking of AppDNA pushes matters more than foreground presentation of your own pushes.

Forwarding yourself (proxy disabled)

Requires iOS SDK 1.0.82+ (Flutter 1.0.20+, React Native 1.0.19+). With AppDNADisableNotificationProxy = YES, forward AppDNA pushes from your own delegate. Both calls are no-ops returning false for non-AppDNA payloads, and are deduplicated per push, so forwarding is also safe while the proxy is active.
A host with its own SceneDelegate may also forward the cold-start tap from connectionOptions; it is deduplicated against the proxy:

installNotificationProxy()

Requires iOS SDK 1.0.82+ (Flutter 1.0.20+, React Native 1.0.19+). AppDNA.installNotificationProxy() installs the proxy immediately (main thread) if it is not installed yet. You only need it if your linking setup drops the SDK’s launch-time install (for example aggressive dead-stripping) and you want the proxy in place before configure. It wraps the delegate that exists when you call it, so call it after you set your own UNUserNotificationCenter.current().delegate. Once the proxy is installed — by the launch hook, by configure, or by an earlier call — the call does nothing: it cannot re-wrap a delegate assigned after the install (see How push works on iOS). With AppDNADisableNotificationProxy = YES it does nothing.

Track Delivery and Taps (manual)

trackPushDelivered / trackPushTapped are only for pushes the SDK does not see (for example pushes shown by another system you forward from). They carry no delivery_id, so combining them with forwarding (AppDNA.pushModule) or the notification proxy double-counts — do not call them for AppDNA pushes.

Push Module

Access the push module directly for advanced usage:
On iOS the host-facing property is AppDNA.pushModule (Swift); Android, Flutter, and React Native expose the same surface as AppDNA.push. The behavior and methods are identical — only the namespace name differs. (On iOS there is also an internal AppDNA.push accessor for the lower-level PushTokenManager, but it is not part of the public API and hosts do not call it.)

PushModule Methods

Pass the marker as a string. AppDNA’s servers send appdna and push_id as strings ("1", "…"). FCM data values are always strings. If you build or rewrite a payload yourself, keep them strings: iOS reads only string values, so a numeric appdna: 1 is not an AppDNA push there. On Android the push APIs take a Map<String, String>, and handlePushTap(intent) drops every extra that is not a String, so a numeric appdna extra is not an AppDNA tap either.

AppDNAPushDelegate Protocol

All 3 methods on this delegate fire from the SDK’s push pipeline:
  • onPushTokenRegistered(token:) — fires whenever the SDK registers a new APNs token (or detects a change). Token registration with the AppDNA backend continues regardless of whether you implement this method.
  • onPushReceived(notification:, inForeground:) — fires when an AppDNA push is delivered: in the foreground through the proxy, or whenever you forward it with handleMessageData (then inForeground reports the app state). It does not fire for your own notifications.
  • onPushTapped(notification:, actionId:) — fires when the user taps an AppDNA push or one of its action buttons (through the proxy or handleNotificationTap).
Implement the AppDNAPushDelegate protocol to receive push notification lifecycle callbacks:

Example Implementation

Action buttons and custom sound

Buttons and a sound set on the Console’s push compose page reach the device:
  • Buttons. The push carries its buttons as an actions array and aps.category. Each distinct button set gets its own category id, appdna_ plus a hash of the set. A category set in the campaign (ios.category) is sent only when the push has no buttons; with buttons the generated category is always sent, so the SDK never registers AppDNA buttons over a category your app owns. iOS shows buttons only for a category that is registered before the notification is displayed. The SDK registers it in three places:
    1. In a Notification Service Extension (recommended). Add a Notification Service Extension target whose principal class subclasses NotificationService from the AppDNANotificationExtension library. Link that library to the extension, not AppDNASDK: AppDNASDK uses API that is unavailable in an app extension (UIApplication.shared) and installs its notification handler at launch, so it must not run inside the extension. AppDNANotificationExtension has no dependencies and builds with application-extension-only API. It registers the category on receipt, before display, and downloads the image. Every push with buttons is sent with mutable-content: 1, so the extension runs for it. This is the only way the first push with a new button set shows its buttons while the app is not running.
    2. When the app sees the push: foreground delivery, or a tap.
    3. At configure, from the AppDNA notifications still in Notification Centre.
    Without the extension, a new button set shows its buttons only after the app has registered it (points 2 and 3). That usually means from the second push with the same buttons.
    Add the library to the extension target only:
    • Swift Package Manager: the AppDNANotificationExtension product of the AppDNA iOS package.
    • CocoaPods: pod 'AppDNANotificationExtension' in the extension’s own target block (the app keeps pod 'AppDNASDK'). Make that block a top-level target, not one nested inside the app’s target block: a nested target inherits the app’s pods, so AppDNASDK and Firebase would be linked into the extension. If you keep it nested, add inherit! :none to it:
      The Podfile needs use_frameworks!, dynamic or :linkage => :static, as for any app that uses AppDNASDK through CocoaPods (Installation). With pods built as static libraries (no use_frameworks!), pod install stops: “The Swift pod FirebaseCoreInternal depends upon GoogleUtilities, which does not define modules”. Adding modular headers does not fix it: use_modular_headers! gets past pod install, but then gRPC-C++ does not compile (“module map file ’…/gRPC-Core.modulemap’ not found”); :modular_headers => true on GoogleUtilities and FirebaseFirestoreInternal only gets through the compile, but the app does not link (“ld: framework ‘FirebaseFirestoreInternal’ not found”), because FirebaseFirestore links FirebaseFirestoreInternal as a framework, which a static-library build does not produce.
    The app and the extension register categories through the same code, so a button’s title is the same whichever registered it. A button label’s {{…}} variables resolve to their fallback ({{user.name | there}} → there), or to nothing without one, in the registered button, because a category is shared by every push with the same buttons and the extension has no user data; the actions in PushPayload keep the app’s values. The SDK caps the AppDNA button categories it keeps registered at 16: when a new one would pass that, it keeps the new one, the categories of notifications still in Notification Centre (all of them, even past 16) and then the most recently registered ones up to 16. “Most recently registered” is per process: the extension and the app each keep their own list. Your own categories are never touched. The extension and the app register in separate processes, and each registration rewrites the whole category set, so one can overwrite a category the other has just registered. After writing, a registration reads the set back and, if its category is gone, adds it again (up to three times). A category that is still lost is registered again the next time the app sees a push with those buttons, or at configure while one of its notifications is in Notification Centre.
  • Button taps go through the same path as a body tap. The tap is tracked as push_tapped with the button’s id as action, and onPushTapped(notification:, actionId:) fires with the button’s id. The SDK then routes the button’s own action: deep_link and open_url (the Console’s URL type) go to your deep-link handler, and show_screen opens that screen. dismiss routes nowhere and is shown as a destructive button; it never opens the app, even if a payload marks it foreground. A button with Open app in foreground switched off is handled without bringing the app to the foreground.
  • Text reply. A text_reply button is a text-input action whose placeholder is the button’s value. The typed text reaches onPushTapped as notification.data["reply_text"]. If you forward taps yourself with handleNotificationTap, copy (response as? UNTextInputNotificationResponse)?.userText into the userInfo under appdna_reply_text first.
  • Custom sound is aps.sound: the file name exactly as authored (for example chime.wav). Bundle the file in the app’s main bundle or Library/Sounds. default plays the system sound.

PushPayload

The PushPayload struct contains the notification content:

PushAction

The PushAction struct defines the action associated with a notification tap:

Auto-Tracked Events

The SDK automatically tracks the following push-related events:
push_delivered / push_tapped are tracked by the notification proxy (or your AppDNA.pushModule forwarding) — see How push works on iOS. Do not also track AppDNA pushes with trackPushDelivered / trackPushTapped.

Full Example

A Notification Service Extension is unaffected by the proxy. Background delivery is not tracked on iOS. For how events are queued and delivered, see the Offline Support guide.

Scheduled pushes in the user’s time zone

A campaign scheduled with Send in user’s timezone reaches each user when their own clock shows the scheduled date and time. Each user gets it once, at the first check (every minute) that finds their local time has reached it — also when a daylight-saving change or a move to another zone happens while the campaign is going out. The audience is fixed when the campaign starts going out, in the first zone to reach the time. From iOS SDK 1.0.82, every event the SDK sends carries the device’s time zone (its IANA name, from TimeZone.current), and AppDNA keeps the latest one on the identified user’s profile — so you need no code for this. Push quiet hours and journey waits that end at a time of day use the same zone. The zone is resolved in this order:
  1. the timezone trait your app passes to identify (an IANA name such as Europe/Berlin), if you set one — it always takes precedence, and the device’s zone never overwrites it;
  2. the zone the user’s device last reported;
  3. the app’s time zone;
  4. UTC.
A value that is not an IANA zone name is ignored at each step. The device’s zone is recorded for identified users only (after identify), from their next event. Set the trait only when the zone your app knows for the user should win over the device’s — for example a zone the user picked in your settings: