React Native push notifications
Contents
Set up Workflows push notifications in the React Native SDK. For the concept and channel setup, see Push notifications.
Available in posthog-react-native 4.62.0 and newer, and requires the optional @posthog/react-native-plugin package (2.3.0+). Not supported on React Native Web or macOS.
Requirements
Configure push in your app as usual (for example with @react-native-firebase/messaging) and request notification permission from the user.
Automatic registration and open tracking (default)
Both behaviors are on by default. An app that already has push configured and the plugin installed starts sending its device token to PostHog after upgrading, with no code change:
On iOS the native SDK hooks the app delegate's remote-notification registration callback, so it picks up the APNs token once your app registers for remote notifications. On Android it fetches the FCM token at startup when Firebase Messaging is on the classpath (@react-native-firebase/messaging sets this up). The token is registered under the current distinct ID, so it follows the user across identify().
Open coverage differs per platform.
On iOS every tap on a remote notification is captured, whether the notification cold-launched the app or it was already running. Cold-launch capture requires @posthog/react-native-plugin 2.8.2 or newer, which installs the native hook at launch instead of waiting for the SDK to set up from JavaScript – by then iOS has already reported the tap. Automatic capture requires iOS 14 or newer; below that the native SDK installs no push hooks at all. Locally-scheduled notifications are ignored.
On Android every tap on a notification from the system tray is captured, whether the notification cold-launched the app or it was already running, with no code in your activity. Warm-start capture requires @posthog/react-native-plugin 2.6.0 or newer; older versions only capture cold-start taps. A tap is recognized by the google.message_id extra that Firebase puts on the intent, so push delivered outside FCM isn't seen. One state needs an override in your MainActivity: when the app's process was killed but its task stayed in the recent apps list, React Native drops the tap's intent before any library in the process can see it.
iOS only reports a notification tap to your app through UNUserNotificationCenter.current().delegate. If nothing in your app sets one, the SDK has nothing to observe and $push_notification_opened is never captured on iOS, in any app state.
The Android startup fetch doesn't see later token refreshes, so forward those yourself to keep the registered token current:
Android taps after the process is killed
When Android kills your app's process but leaves its task in the recent apps list, React Native drops the tap's intent and never updates getIntent(), so the tap is invisible to every library in the process – PostHog, @react-native-firebase/messaging and your deep links alike. Overriding onNewIntent in your MainActivity fixes all three.
If you use the posthog-react-native/expo config plugin, expo prebuild writes that override for you from posthog-react-native 4.72.0. Opt out with patchMainActivityNewIntent:
A bare React Native app that never runs expo prebuild adds the override itself, in MainActivity.kt. setIntent(intent) has to come before the super call, or React Native has already swallowed the intent:
If your MainActivity already overrides onNewIntent, the config plugin leaves it alone and warns instead – add setIntent(intent) as the first statement of your own override.
Manual registration
If you manage push tokens yourself, turn the automatic flags off and call the SDK directly:
It's safe to call this before the SDK finishes initializing; the call is queued rather than dropped.
When appId is unset, iOS registers the token as an APNs token under your bundle id and Android as an FCM token under your default Firebase project id. To use FCM on iOS instead, set capturePushNotificationSubscriptions: false and pass your Firebase project id as appId, otherwise the device registers twice (once per provider) and a Workflow connected to both channels delivers twice.
Unregister the token when a user signs out so it isn't left bound to them:
Calling posthog.reset() on logout already moves the registered token to the new anonymous identity, so this is only needed when you manage subscriptions yourself.
Registration and unregistration are durable. If the device is offline or the request fails, the SDK retries on the next flush, identity change, or app launch.
Capturing opens
Taps on remote notifications are captured for you (see above). Automatic capture can't see locally-scheduled notifications on either platform, notifications you display yourself from a foreground message, or push delivered outside FCM on Android. Call the manual API only for those:
Don't wire this to messaging().onNotificationOpenedApp or getInitialNotification(). The SDK already captures those taps. On Android and iOS, from @posthog/react-native-plugin 2.8.2, a repeat of a notification PostHog sent is skipped when the same notification was captured in the last 5 minutes. Notifications PostHog didn't send are never deduplicated, so a handler you wire up yourself still counts those opens twice. If you added an onNotificationOpenedApp handler for Android on an earlier plugin version, remove it when you upgrade.
The $push_notification_opened event includes $notification_title and $notification_body (plus $notification_subtitle on iOS), and $notification_action for action-button taps. On iOS, notification content is only captured for notifications sent by PostHog. On Android, taps captured automatically carry no title or body, because Firebase strips the content from the intent; those properties are only set when you pass them to posthog.capturePushNotificationOpened yourself.
The event is built and sent by the native SDK, so your JS before_send never sees it. Redact anything sensitive before passing it to capturePushNotificationOpened.
Opting out
Set capturePushNotificationSubscriptions: false or capturePushNotificationOpened: false in the initialization options.
On iOS from @posthog/react-native-plugin 2.8.2, capturePushNotificationOpened: false stops the event but not the hook that catches a tap cold-launching the app: the plugin installs that at launch, before any of your JavaScript runs, and only releases it when setup() sees the flag. To skip installing it at all, set com.posthog.posthog.CAPTURE_PUSH_NOTIFICATION_OPENED to false in Info.plist, or in ios.infoPlist on Expo:
Identity verification
If your push channel requires identity verification, supply a backend-minted token through pushIdentityProvider:
Troubleshooting
| Issue | Check |
|---|---|
| Token never registers | Confirm @posthog/react-native-plugin is installed, push is set up in your app, and the user granted notification permission. On Android, confirm Firebase Messaging is on the classpath (@react-native-firebase/messaging sets this up). |
| Push doesn't arrive | Confirm the channel's Firebase project (Android) or APNs environment and bundle id (iOS) match your app. |
$push_notification_opened never fires | On iOS, confirm something in your app sets UNUserNotificationCenter.current().delegate, that the device runs iOS 14 or newer, and, for a tap that cold-launches the app, that you're on @posthog/react-native-plugin 2.8.2 or newer. On Android, confirm the notification is sent through FCM (detection keys on the google.message_id intent extra), that you're on @posthog/react-native-plugin 2.6.0 or newer for taps that arrive while the app is already running, and that your MainActivity overrides onNewIntent for taps after the process was killed. |
| Registration rejected on a Required channel | Your pushIdentityProvider isn't returning a valid token in time. See Identity verification. |