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+).APNs Setup
Before using push notifications, configure Apple Push Notification service (APNs) for your app:- In Xcode, enable the Push Notifications capability under your target’s Signing & Capabilities tab.
- Generate an APNs authentication key (.p8 file) in the Apple Developer Portal.
- 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.
Request Permission
Request push notification permission from the user. This presents the system permission dialog and registers for remote notifications:Bool indicating whether the user granted permission.
Set Token Manually
If you handle token registration yourself (e.g., inapplication(_:didRegisterForRemoteNotificationsWithDeviceToken:)), pass the raw token data to the SDK:
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).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,onPushTappedfires, and the AppDNA action is routed (show_screen, deep link / URL). A push delivered or tapped beforeconfigure(for example a cold-start tap) is buffered — up to 8 — and handled onceconfigurehas set up push; beyond 8, further ones are dropped. No forwarding code is needed. - Foreground delivery — tracked as
push_deliveredandonPushReceived(…, 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 withhandleMessageDatais tracked too.
AppDNA.diagnose() reports notificationDelegate: wrapped-or-replaced); forward with the AppDNA.pushModule API below.
Info.plist keys
Requires iOS SDK1.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.
Forwarding yourself (proxy disabled)
Requires iOS SDK1.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.
SceneDelegate may also forward the cold-start tap from connectionOptions; it is deduplicated against the proxy:
installNotificationProxy()
Requires iOS SDK1.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 withhandleMessageData(theninForegroundreports 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 orhandleNotificationTap).
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
actionsarray andaps.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:- In a Notification Service Extension (recommended). Add a Notification Service Extension target whose principal class subclasses
NotificationServicefrom theAppDNANotificationExtensionlibrary. Link that library to the extension, notAppDNASDK:AppDNASDKuses 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.AppDNANotificationExtensionhas 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 withmutable-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. - When the app sees the push: foreground delivery, or a tap.
- At
configure, from the AppDNA notifications still in Notification Centre.
Add the library to the extension target only:-
Swift Package Manager: the
AppDNANotificationExtensionproduct of the AppDNA iOS package. -
CocoaPods:
pod 'AppDNANotificationExtension'in the extension’s own target block (the app keepspod 'AppDNASDK'). Make that block a top-level target, not one nested inside the app’stargetblock: a nested target inherits the app’s pods, soAppDNASDKand Firebase would be linked into the extension. If you keep it nested, addinherit! :noneto it:The Podfile needsuse_frameworks!, dynamic or:linkage => :static, as for any app that usesAppDNASDKthrough CocoaPods (Installation). With pods built as static libraries (nouse_frameworks!),pod installstops: “The Swift podFirebaseCoreInternaldepends uponGoogleUtilities, which does not define modules”. Adding modular headers does not fix it:use_modular_headers!gets pastpod install, but thengRPC-C++does not compile (“module map file ’…/gRPC-Core.modulemap’ not found”);:modular_headers => trueonGoogleUtilitiesandFirebaseFirestoreInternalonly gets through the compile, but the app does not link (“ld: framework ‘FirebaseFirestoreInternal’ not found”), becauseFirebaseFirestorelinksFirebaseFirestoreInternalas a framework, which a static-library build does not produce.
{{…}}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; theactionsinPushPayloadkeep 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 atconfigurewhile one of its notifications is in Notification Centre. - In a Notification Service Extension (recommended). Add a Notification Service Extension target whose principal class subclasses
-
Button taps go through the same path as a body tap. The tap is tracked as
push_tappedwith the button’s id asaction, andonPushTapped(notification:, actionId:)fires with the button’s id. The SDK then routes the button’s own action:deep_linkandopen_url(the Console’s URL type) go to your deep-link handler, andshow_screenopens that screen.dismissroutes nowhere and is shown as a destructive button; it never opens the app, even if a payload marks itforeground. A button with Open app in foreground switched off is handled without bringing the app to the foreground. -
Text reply. A
text_replybutton is a text-input action whose placeholder is the button’s value. The typed text reachesonPushTappedasnotification.data["reply_text"]. If you forward taps yourself withhandleNotificationTap, copy(response as? UNTextInputNotificationResponse)?.userTextinto the userInfo underappdna_reply_textfirst. -
Custom sound is
aps.sound: the file name exactly as authored (for examplechime.wav). Bundle the file in the app’s main bundle orLibrary/Sounds.defaultplays the system sound.
PushPayload
ThePushPayload struct contains the notification content:
PushAction
ThePushAction 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, fromTimeZone.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:
- the
timezonetrait your app passes toidentify(an IANA name such asEurope/Berlin), if you set one — it always takes precedence, and the device’s zone never overwrites it; - the zone the user’s device last reported;
- the app’s time zone;
- UTC.
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:

