Supported on: iOS SDK
1.0.70+ · Android SDK 1.0.42+ · React Native SDK 1.0.7+ (New Architecture only)Platform Setup
Before using push notifications, configure the native push services for each platform: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
- Add your
google-services.jsonfile to theandroid/appdirectory. - 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: delivered / tapped tracking,
AppDNA.push.handleMessage / handleTap and tap routing need an AppDNA server from the release that ships React Native SDK 1.0.19 or later — older servers sent no push_id. AppDNA pushes carry the marker appdna: "1" in their data; the SDK handles only marked messages and ignores every other push.How push works on iOS
Requires React Native SDK 1.0.19+.
application(_:didFinishLaunchingWithOptions:) returns. It wraps whatever delegate exists at that moment, so a delegate set in didFinishLaunching is wrapped. RNFirebase and notifee install their own delegate at that same moment, so the order is not fixed: AppDNA may wrap them, or they may wrap AppDNA. Each keeps the delegate it replaced and forwards to it, so either order works. A delegate assigned later replaces the proxy and cannot be re-wrapped (the proxy installs once per launch): set it in didFinishLaunching instead, or forward AppDNA pushes from it with handleMessageData / handleNotificationTap as shown under Opt out below. The proxy does not change how your own notifications are presented or handled, unless you set AppDNAForegroundPresentation (see the trade-off below). AppDNA pushes are tracked and routed by AppDNA, including the cold-start tap — no forwarding and no AppDelegate code is needed.
- RNFirebase and notifee do not surface AppDNA pushes at all, so your own handlers never see them.
- Delivered tracking covers foreground delivery only, on iOS and on Android (in the background FCM displays AppDNA pushes itself); background delivery is not tracked. The tap is still tracked when the app opens, on both platforms automatically (on Android see Taps on Android).
- Foreground presentation. When a push library such as RNFirebase ends up as the outer notification delegate, it keeps deciding how notifications are shown in the foreground, and AppDNA’s foreground delivered tracking does not run — unless
AppDNAForegroundPresentationis set or the delegate AppDNA wrapped implementswillPresent. To have AppDNA pushes presented 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 are then not presented in the foreground — leave the key unset if that matters more. - Opt out with the Info.plist key
AppDNADisableNotificationProxy=YES, then forward from your own nativeUNUserNotificationCenterDelegate:
- If your build drops the SDK’s launch hook (aggressive dead-stripping), you can call the native iOS
installNotificationProxy()from Swift after you set your owncenter.delegate. It installs the proxy only if it is not installed yet; once installed, the call does nothing and cannot re-wrap a delegate assigned later:
Taps on Android
The module hands every tap intent to the native Android SDK’shandlePushTap(intent): the activity’s intent when you call configure (a tap that launched the app), every intent it receives afterwards (onNewIntent), and the current activity’s intent whenever the app resumes (a tap that started a new activity while React Native kept running, for example after the activity was closed with Back). That covers body, button and text-reply taps on notifications the SDK displayed, which no JS push library sees, and taps on notifications FCM displayed. Once the SDK is configured, the tap is tracked, routed and passed to onPushTapped, once. The module 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): if your JS reads them and calls AppDNA.push.isAppDNAMessage(data) or AppDNA.push.handleTap(data), the tap is still recognised as AppDNA’s, and handleTap returns true without tracking or routing it again. The tap is tracked and routed once: the module hands each intent to the native SDK once (calling configure again, or a reload, 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 (AppDNA.push.setDelegate) before you call configure. The cold-start tap is handed over as soon as configure finishes, and a tap that arrives while no delegate is set is still tracked and routed, but it does not reach onPushTapped.
A tap that restores the app after Android killed its process reaches MainActivity.onNewIntent before React Native is running, and React Native drops it. To keep it, store it as the activity’s intent, so the module finds it at configure:
If your app already uses Firebase Messaging
Requires React Native SDK 1.0.19+.
@react-native-firebase/messaging), keep it as the only handler and forward AppDNA messages to the SDK:
- Android — remove the SDK’s messaging service. Android delivers each message to one
FirebaseMessagingServiceonly. Inandroid/app/src/main/AndroidManifest.xml(withxmlns:tools="http://schemas.android.com/tools"on<manifest>):
android/app/src/main/res/values/bools.xml:
- Keep
@react-native-firebase/messagingas your only handler. - Pass the right token per platform. AppDNA sends iOS pushes directly over APNs.
getToken()) on iOS: the SDK rejects it with BAD_TOKEN and every iOS push then fails.
- Forward AppDNA messages (Android). iOS is automatic (above).
notification block, which FCM displays in the background.
- Forward taps (Android). Optional: the module already hands every tap intent to the native SDK (Taps on Android). Forwarding as well is safe, because each tap is tracked once.
false and does nothing for a non-AppDNA message, and each message is tracked once, so forwarding everything is safe. Do not also call trackPushDelivered / trackPushTapped for these messages — they carry no delivery_id and would double-count.
Known limitation: a message that arrives while the app is in the background is handled by RNFirebase’s
setBackgroundMessageHandler; 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.Static Methods
TheAppDNA class exposes top-level static methods for common push operations:
Set Token
Pass a device push token to the SDK manually:Set Permission Status
Update the SDK with the current push permission status:Track Delivery
Track delivery manually — only for pushes the SDK does not see. These calls carry nodelivery_id, so combined with forwarding (AppDNA.push.handleMessage) or the iOS automatic handler they double-count:
Track Tap
Track a tap manually, with an optional action identifier — only for pushes the SDK does not see (same caveat as above):Push Module
Access the push module through theAppDNA.push property for advanced usage:
Module Methods
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.Request Permission
Request push notification permission from the user. This presents the system permission dialog:Get Current Token
Retrieve the current device push token:AppDNAPushDelegate
Set a delegate to receive push notification lifecycle callbacks: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. Your ios/Podfile needs use_frameworks!, dynamic or :linkage => :static: 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.
Example Implementation
Standalone AppDNAPush Class
For a more React-idiomatic approach, use the standaloneAppDNAPush class with callback-based listeners:
Methods
Example with Listeners
PushPayload
ThePushPayload type contains the notification content. The keys are snake_case — they are the
keys native puts on the wire, not camelCase:
Each entry in
actions is a PushAction:
Native events
Native emits the following to the push delegate:Use
AppDNAPush or AppDNA.push.setDelegate(...). There is no supported way to subscribe to these
events directly, and a NativeEventEmitter subscription would never fire.Auto-Tracked Events
The SDK tracks the following push-related events for AppDNA pushes it sees — through the Android messaging service, the iOS automatic handler, or your forwarding calls:You do not need to track these events yourself. Pushes that are not AppDNA pushes are never tracked, and
onPushReceived / onPushTapped do not fire for them.Full Example
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 React Native SDK 1.0.19, 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:

