Supported on: iOS SDK
1.0.61+ · Android SDK 1.0.33+ · Flutter SDK 1.0.3+Platform Setup
Before using push notifications, complete the platform-specific setup:iOS
- 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.
Android
- Set up Firebase Cloud Messaging (FCM) in your Android project.
- Add the
google-services.jsonfile to yourandroid/app/directory. - Upload your Firebase service-account JSON to the AppDNA Console under Settings > Push, in the FCM (Android) card (paste it into Service Account JSON), enter your Firebase Project ID (the
project_idin that JSON), and click Save. It must be a service-account key ("type": "service_account", withclient_emailandprivate_key); other Google credential types, such as a workload identity federation configuration, are refused.
Requires server ≥ this release. Delivered / tapped tracking,
AppDNA.push.handleMessage / handleTap and AppDNAPush.handlePushTap() rely on the push_id, delivery_id and appdna marker that AppDNA’s servers add to every push from this release on; older servers sent no push_id.Push Module
Access the push module through theAppDNA.push accessor, which returns an AppDNAPushModule instance:
Request Permission
Request push notification permission from the user. This presents the system permission dialog only — it does not register for remote notifications. To register for remote notifications, callAppDNA.push.registerForPush() (see below):
bool indicating whether the user granted permission.
Set Token Manually
If you handle token registration yourself (e.g., usingfirebase_messaging), pass the token to the SDK. AppDNA sends iOS pushes directly over APNs, so the token differs per platform:
- iOS: pass the APNs token —
FirebaseMessaging.instance.getAPNSToken(). It returnsnulluntil iOS has registered with APNs (after the permission prompt), so null-check it and retry afterrequestPermission()resolves and on every app start (onTokenRefreshreports FCM tokens only). - Android: pass the FCM token —
FirebaseMessaging.instance.getToken(), afterconfigureand on everyonTokenRefresh.
AppDNA.push.setToken(token) is the same call on the push module.
Get Current Token
Retrieve the registered push token:String? — null if no token has been registered.
Set Permission Status
Manually update the SDK with the current push permission status:Track Delivery and Taps
Track when a push notification is delivered to the device:How push works on iOS
Requires Flutter SDK 1.0.20+.
application(_:didFinishLaunchingWithOptions:) returns. It wraps whatever delegate exists at that moment (FlutterAppDelegate, FlutterFire’s, your own), so a delegate set in didFinishLaunching is wrapped. A delegate assigned later replaces the proxy and cannot be re-wrapped (the proxy installs once per launch; installNotificationProxy() then does nothing): set it in didFinishLaunching instead, or forward AppDNA pushes from it natively with AppDNA.pushModule.handleMessageData / handleNotificationTap (see iOS push). The proxy does not change how your own notifications are presented or handled, unless you set AppDNAForegroundPresentation (see the trade-off below). AppDNA pushes (marked appdna: "1") are tracked and routed by AppDNA, including the cold-start tap — no forwarding and no AppDelegate code is needed on iOS.
- FlutterFire:
getInitialMessage()may also return an AppDNA push that launched the app, depending on install order (withmessageId == nullanddata['appdna'] == '1'). Ignore such messages in your own routing (AppDNA already handled them), or pass them toAppDNA.push.handleTap(message.data)(deduplicated).onMessage/onMessageOpenedAppnever fire for AppDNA pushes on iOS. - Foreground presentation and tracking. When FlutterFire ends up as the outer notification delegate, it keeps deciding how notifications are presented in the foreground — AppDNA pushes included — and AppDNA’s foreground delivered tracking does not run, unless
AppDNAForegroundPresentationis set or the delegate AppDNA wrapped implementswillPresent. To have AppDNA pushes shown and tracked in the foreground, set the Info.plist keyAppDNAForegroundPresentationto an array ofbanner,list,sound,badge(an empty array = do not present). Trade-off: when AppDNA ends up innermost with no previous delegate, your own FCM pushes then get no foreground presentation — leave the key unset if presenting your own FCM pushes in the foreground matters more. - Opt out with the Info.plist key
AppDNADisableNotificationProxy=YES; AppDNA pushes are then not tracked or routed unless your native iOS code forwards them (see iOS push). - Delivered tracking on iOS covers foreground delivery only; taps are tracked from any state.
Taps on Android
A tap that reaches your running app through the activity’sonNewIntent is handled by the plugin: body, button and text-reply taps on notifications the SDK displayed, a tap on a notification FCM displayed, and a tap that brings the app back after Android killed its process. Once the SDK is configured, the tap is tracked, routed and passed to onPushTapped. A tap that arrives before configure waits for it. The plugin hands the native SDK a copy of the intent, so the activity’s intent keeps its extras (appdna, push_id, delivery_id and the rest), and the tap is still recognised as AppDNA’s if you read them and forward the tap yourself.
A tap that launches the app arrives in its launch intent. The plugin hands it to the native SDK when you call configure, as the React Native module does, so it is tracked, routed and passed to onPushTapped without any call from your app. The same goes for a tap that starts a new activity while the Flutter engine is still running (a cached engine, or the activity was closed with Back while the process lived): the plugin hands that activity’s launch intent over when it attaches to it. You can still call await AppDNAPush.handlePushTap(), or forward the tap through FlutterFire (getInitialMessage(), below): handlePushTap() answers for the newest intent only (the last one through onNewIntent, else the launch intent) and returns true for a tap the SDK has already handled without tracking or routing it again. It answers at once, also before configure or after shutdown(); a tap the SDK has not handled yet is then tracked and routed once the SDK is ready. Like the plugin’s own hand-over, it gives the native SDK a copy of the intent, so the intent keeps its extras. Every tap is tracked and routed once: the plugin hands each intent to the native SDK once (calling configure again after shutdown() does not re-run the launch intent), and the SDK remembers the taps it has handled (the last 32, keyed by delivery_id, else push_id), so the same tap forwarded through AppDNA.push.handleTap is not tracked again either.
Set the push delegate before configure. The tap that launched the app is handed to the native SDK during configure, and onPushTapped reaches only a delegate that is already set at that moment: the native SDK keeps no tap for a delegate set later, so a delegate set after configure can miss the launch tap (the SDK still tracks and routes it). Pushes and taps after that reach a delegate set at any time.
If your app already uses Firebase Messaging (Android)
Requires Flutter SDK 1.0.20+.
AppDNAMessagingService that handles only AppDNA messages (it ignores everything else). If your app uses firebase_messaging, keep it as the only handler and forward AppDNA messages to AppDNA:
-
Remove the SDK’s service in
android/app/src/main/AndroidManifest.xml:or set<bool name="appdna_messaging_service_enabled">false</bool>inandroid/app/src/main/res/values/bools.xml. -
Keep
firebase_messagingas the only handler. - Register the token as shown in Set Token Manually — the FCM token on Android, the APNs token on iOS.
-
Forward messages your handler receives:
handleMessagetracks delivery (with itsdelivery_id) and firesonPushReceived; it never displays anything (on Android it calls the nativeAppDNA.push.handleMessageData, not the nativehandleMessage, which posts the notification for any AppDNA message with a title or body). Display is unaffected: AppDNA pushes carry anotificationblock, which FCM displays while the app is in the background; on Android, in the foreground FCM displays nothing, so showing the message then is up to your app. -
Forward taps — taps that reach the running app, and the tap that launched it (at
configure), are handled by the plugin (Taps on Android). Forwarding them as well is safe:await AppDNAPush.handlePushTap()afterconfigure(it takes no argument; the plugin reads the newest intent — the last one throughonNewIntent, else the launch intent — and leaves its extras in place), or:Both paths share one dedup, so doing both is safe. Every forwarding call returnsfalseand does nothing for a message without the AppDNA marker, so you can forward every message.
Pass the marker as a string. AppDNA’s servers send
appdna and push_id as strings ("1", "…"). FCM data values are always strings, so this matters only for a payload you build or rewrite yourself. Keep them strings: iOS reads only string values, so a numeric appdna: 1 is not an AppDNA push there, while the Android bridge converts a number to its string (1 → "1") and accepts it — the two platforms would disagree.trackDelivered / trackTapped for these messages.
Background messages (known limitation). A message that arrives while the app is in the background is handled by
FirebaseMessaging.onBackgroundMessage in a separate isolate; FCM still displays the notification, and the tap is tracked when the app opens (Taps on Android). Forwarding delivered from the background handler is not supported in this release.AppDNAPushDelegate
All 3 methods on this delegate fire from the SDK’s push pipeline:onPushTokenRegistered(token)— fires whenever the SDK registers a new APNs (iOS) or FCM (Android) token (or detects a change). Token registration with the AppDNA backend continues regardless of whether you implement this method.onPushReceived(notification, inForeground)— fires when a push arrives.notificationis the full payload map;inForegroundistruewhen the app is active.onPushTapped(notification, actionId)— fires when the user taps a notification or one of its action buttons.actionIdis the identifier of the tapped action button if the user tapped one, otherwisenull.
NotificationService from the extension-safe AppDNANotificationExtension pod (pod 'AppDNANotificationExtension' in the extension’s target block of your ios/Podfile; make it a top-level target, not one nested in the app’s target, which would inherit the app’s pods into the extension, or add inherit! :none to a nested one), so a new button set shows its buttons on the first push. Keep use_frameworks! in your ios/Podfile (Flutter’s generated Podfile has it): the iOS SDK cannot be built from pods built as static libraries (iOS installation). On Android, buttons appear only on notifications the native SDK builds itself. A push that FCM or your own Firebase Messaging handler displays has none. A button tap arrives as onPushTapped(notification, actionId) with the button’s id. The typed text of a text-reply button is in notification['data']['reply_text'].
Implement the AppDNAPushDelegate abstract class to receive push notification lifecycle callbacks:
Example Implementation
AppDNAPush Helper Methods
TheAppDNAPush class exposes static helpers for permission and token/tap plumbing. Push lifecycle callbacks arrive through AppDNA.push.setDelegate(...) (above) — AppDNAPush has no streams.
Notification Payload Shape
There is no publicPushPayload Dart class. The notification argument delivered to the AppDNAPushDelegate callbacks is a Map<String, dynamic> with camelCase keys. Nested values have real Dart types, so the casts below are safe:
Auto-Tracked Events
The SDK automatically tracks the following push-related events:
Only AppDNA pushes (marked
appdna: "1") are tracked; your own FCM messages are not.
Auto-tracked events are sent alongside any manually tracked events. You do not need to track these events yourself.
Full Example
Push notification behavior varies between iOS and Android. On iOS, the system permission dialog is shown on first request. On Android 13+, the
POST_NOTIFICATIONS runtime permission is requested. The native SDKs handle these differences transparently.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 Flutter SDK 1.0.20, every event the SDK sends carries the device’s time zone (its IANA name, read by the native SDK), 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:

