Expo Push

This guide covers WebEngage push notifications for both platforms in an Expo app:

  • Android — push is received through your JavaScript push library (expo-notifications or @react-native-firebase/messaging) and forwarded to WebEngage using the react-native-webengage APIs.
  • iOS — push is configured automatically by the webengage-expo-push config plugin, with no manual native changes required. It also enables rich push notifications (images, carousels, rating widgets, and action buttons).

Important: Before continuing, complete the WebEngage Expo Plugin Integration first. This ensures your base setup is correct and push notifications will work as expected.

Android

webengage-expo-push does not configure Android push notifications — it's an iOS-only plugin. On Android, push is handled through your push library (expo-notifications or @react-native-firebase/messaging), and you connect it to WebEngage using the react-native-webengage JavaScript APIs.

Android still needs FCM: Push delivery on Android goes through Firebase Cloud Messaging under the hood, even with expo-notifications. Keep your google-services.json in the project, referenced via android.googleServicesFile in app.json — do not remove it when migrating off @react-native-firebase.

Option A — Using expo-notifications

If your app uses expo-notifications to receive push, connect it to WebEngage in four steps. This option also needs expo-task-manager for step 3, so install both up front:

npm install expo-notifications expo-task-manager
yarn add expo-notifications expo-task-manager

Payload shape

WebEngage's native onMessageReceived reads the FCM data map from message.data and only handles messages where data.source === "webengage". So you must always forward an object of the form { data: { source: "webengage", ... } } — not the data map on its own.

expo-notifications exposes the FCM data map in two places:

  • Foreground: notification.request.trigger.remoteMessage.data
  • Background: data.data — the background task payload (data) is the Firebase RemoteMessage, so the FCM data map is under its data field

1. Pass the device token to WebEngage (Android only)

Get the native FCM token with getDevicePushTokenAsync and send it to WebEngage when your app starts. This applies to Android only — on iOS getDevicePushTokenAsync returns the APNs token, which WebEngage handles natively.

import { useEffect } from 'react';
import { Platform } from 'react-native';
import * as Notifications from 'expo-notifications';
import WebEngage from 'react-native-webengage';

const webengage = new WebEngage();

useEffect(() => {
  const registerToken = async () => {
    if (Platform.OS !== 'android') return;
    const { data: token } = await Notifications.getDevicePushTokenAsync();
    webengage.push.sendFcmToken(token as string); // Registers the FCM token with WebEngage, like a native onNewToken callback
  };

  registerToken();
}, []);
import { useEffect } from 'react';
import { Platform } from 'react-native';
import * as Notifications from 'expo-notifications';
import WebEngage from 'react-native-webengage';

const webengage = new WebEngage();

useEffect(() => {
  const registerToken = async () => {
    if (Platform.OS !== 'android') return;
    const { data: token } = await Notifications.getDevicePushTokenAsync();
    webengage.push.sendFcmToken(token); // Registers the FCM token with WebEngage, like a native onNewToken callback
  };

  registerToken();
}, []);

2. Handle foreground notifications

When a notification arrives while the app is open, use addNotificationReceivedListener to read the FCM data map from the Android remoteMessage and forward it to WebEngage as { data }.

import { useEffect } from 'react';
import * as Notifications from 'expo-notifications';
import WebEngage from 'react-native-webengage';

const webengage = new WebEngage();

useEffect(() => {
  const subscription = Notifications.addNotificationReceivedListener((notification: Notifications.Notification) => {
    const trigger = notification.request.trigger as Notifications.PushNotificationTrigger;
    const data = trigger?.remoteMessage?.data;
    if (data?.source === 'webengage') {
      webengage.push.onMessageReceived({ data }); // Hands the foreground push to WebEngage so it can track/render it
    }
  });

  return () => subscription.remove();
}, []);
import { useEffect } from 'react';
import * as Notifications from 'expo-notifications';
import WebEngage from 'react-native-webengage';

const webengage = new WebEngage();

useEffect(() => {
  const subscription = Notifications.addNotificationReceivedListener((notification) => {
    const data = notification?.request?.trigger?.remoteMessage?.data;
    if (data?.source === 'webengage') {
      webengage.push.onMessageReceived({ data }); // Hands the foreground push to WebEngage so it can track/render it
    }
  });

  return () => subscription.remove();
}, []);

3. Handle background notifications

Register a background task with registerTaskAsync (defined via TaskManager.defineTask) so notifications received while the app is in the background or killed are passed to WebEngage. In the background task, the payload is the Firebase RemoteMessage, so the FCM data map is under its data field.

Why the AppState check? This task does not run only in the background — Expo also invokes it while the app is in the foreground. Since step 2 already handles foreground notifications, the same push would reach WebEngage twice. To prevent that, the task returns early when AppState.currentState is active, so it forwards to WebEngage only when the app is in the background or killed.

import { AppState } from 'react-native';
import * as Notifications from 'expo-notifications';
import * as TaskManager from 'expo-task-manager';
import WebEngage from 'react-native-webengage';

const BACKGROUND_NOTIFICATION_TASK = 'WEBENGAGE_BACKGROUND_NOTIFICATION_TASK';

TaskManager.defineTask<Notifications.FirebaseRemoteMessage>(
  BACKGROUND_NOTIFICATION_TASK,
  ({ data: remoteMessage, error }) => {
    if (error) return;
    // Foreground delivery is handled by addNotificationReceivedListener (step 2),
    // so skip here to avoid handling the same push twice.
    if (AppState.currentState === 'active') return;
    const data = remoteMessage?.data;
    if (data?.source === 'webengage') {
      const webengage = new WebEngage();
      webengage.push.onMessageReceived({ data }); // Hands the background/killed-state push to WebEngage so it can track/render it
    }
  }
);

Notifications.registerTaskAsync(BACKGROUND_NOTIFICATION_TASK);
import { AppState } from 'react-native';
import * as Notifications from 'expo-notifications';
import * as TaskManager from 'expo-task-manager';
import WebEngage from 'react-native-webengage';

const BACKGROUND_NOTIFICATION_TASK = 'WEBENGAGE_BACKGROUND_NOTIFICATION_TASK';

TaskManager.defineTask(BACKGROUND_NOTIFICATION_TASK, ({ data: remoteMessage, error }) => {
  if (error) return;
  // Foreground delivery is handled by addNotificationReceivedListener (step 2),
  // so skip here to avoid handling the same push twice.
  if (AppState.currentState === 'active') return;
  const data = remoteMessage?.data;
  if (data?.source === 'webengage') {
    const webengage = new WebEngage(); // Created locally: background tasks can run in a separate JS context with no access to a module-scope instance
    webengage.push.onMessageReceived({ data }); // Hands the background/killed-state push to WebEngage so it can track/render it
  }
});

Notifications.registerTaskAsync(BACKGROUND_NOTIFICATION_TASK);

4. Handle Android 13+ push permission

From Android 13 onwards, apps must explicitly request notification permission from the user. Until the user grants it, they are not opted in for push — even after installing the app.

Use expo-notifications' own permission APIs to request it, then pass the result to WebEngage:

import * as Notifications from 'expo-notifications';
import WebEngage from 'react-native-webengage';

const webengage = new WebEngage();

const requestPermissionsAndOptIn = async () => {
  const settings = await Notifications.getPermissionsAsync();
  let status = settings.status;

  if (status !== 'granted') {
    const request = await Notifications.requestPermissionsAsync();
    status = request.status;
  }

  if (status === 'granted') {
    webengage.user.setDevicePushOptIn(true); // Tells WebEngage this device is opted in to receive push
  } else {
    webengage.user.setDevicePushOptIn(false); // Tells WebEngage this device is opted out of push
  }
};
import * as Notifications from 'expo-notifications';
import WebEngage from 'react-native-webengage';

const webengage = new WebEngage();

const requestPermissionsAndOptIn = async () => {
  const settings = await Notifications.getPermissionsAsync();
  let status = settings.status;

  if (status !== 'granted') {
    const request = await Notifications.requestPermissionsAsync();
    status = request.status;
  }

  if (status === 'granted') {
    webengage.user.setDevicePushOptIn(true); // Tells WebEngage this device is opted in to receive push
  } else {
    webengage.user.setDevicePushOptIn(false); // Tells WebEngage this device is opted out of push
  }
};

Note: Managing when to prompt for push permission is the app's responsibility. If the user denies permission, they will not receive push notifications.

Option B — Using @react-native-firebase/messaging

If your app uses @react-native-firebase/messaging, follow the official WebEngage guide, which covers token registration and foreground/background message handling for this library:

WebEngage React Native Push Documentation

iOS

Installation

npm install webengage-expo-push
yarn add webengage-expo-push

Configuration

Add webengage-expo-push under the plugins key, alongside your existing webengage-expo entry from the WebEngage Expo Plugin Integration guide:

{
  "expo": {
    "plugins": [
      ...
      [
        "webengage-expo-push",
        {
          "mode": "development"
        }
      ]
    ]
  }
}
🚧

webengage-expo requires its full configuration object — it cannot be listed as a bare plugin name ("webengage-expo" with no config). Omitting it throws "You are trying to use the WebEngage plugin without any props." at prebuild time. The example above shows both plugins together for clarity; if webengage-expo is already configured elsewhere in your plugins array, just add the webengage-expo-push entry to it.

Important: The mode value must match the push.mode value configured in webengage-expo.

Configuration Parameters

KeyDescriptionRequired
modeAPNs environment: development or production. Must match push.mode in webengage-expo. Defaults to development if omitted.Yes
iPhoneDeploymentTargetMinimum iOS version for extensions. Defaults to 15.1.No
iosNSEFilePathPath to a custom NotificationService.swift file.No
iosCEFilePathPath to a custom NotificationViewController.swift file.No
devTeamApple Developer Team ID, applied to the main app and both notification extension targets.No
useSPMUse Swift Package Manager instead of CocoaPods for the extension dependencies. Defaults to false.No
iosNSETargetNameCustom name for the Notification Service Extension target. Defaults to NotificationService.No
iosCETargetNameCustom name for the Content Extension (NotificationViewController) target. Defaults to NotificationViewController.No
disableNSESkip Notification Service Extension creation entirely — no target, no files, no Podfile entry. Defaults to false.No
iosNSEExistingTargetSet to true if another plugin already creates the Notification Service Extension target. Skips NSE target/file creation and only adds the WEServiceExtension pod to the existing target — use together with iosNSEFilePath. Defaults to false.No
appGroupNameCustom iOS App Group name for entitlements. Defaults to group.{bundleId}.WEGNotificationGroup.No
🚧

iosCEFilePath, devTeam, useSPM, iosNSETargetName, iosCETargetName, and disableNSE require [email protected] or later. Check your installed version with npm ls webengage-expo-push before using them — on 0.0.1 these keys are rejected with "You have provided an invalid property" at prebuild time.

Build

Run the following command to generate the native iOS project:

npx expo prebuild

After prebuild, the plugin automatically adds the required iOS notification extensions to your project. No further native changes are needed.

Note: If you added this plugin to a project that was already prebuilt, regenerate the native projects with npx expo prebuild --clean so the notification extensions are applied. See When should I rebuild? in the integration guide.

Supported Environments

EnvironmentSupported
Expo GoNo
EAS Build (Managed)Yes
Prebuild WorkflowYes
Custom Dev ClientYes
Bare React NativeYes

Note: Push notifications do not work with Expo Go. Use a development build or EAS Build.

Troubleshooting

Android

Notifications not received on Android

  • Confirm google-services.json is present in your project and referenced via android.googleServicesFile in app.json.
  • Confirm your FCM credentials are uploaded to the WebEngage dashboard.
  • Confirm the device token is being sent — check that webengage.push.sendFcmToken(token) runs on app start (Android only).
  • Test on a physical device or an emulator with Google Play services.

Token not reaching WebEngage

  • Confirm getDevicePushTokenAsync() is called after the app has notification permission.
  • On Android 13+, confirm the notification permission has been requested and granted, otherwise no token flow completes.

Notification received but not rendered by WebEngage

  • Confirm the payload has source: "webengage" — WebEngage only handles messages where data.source === "webengage".
  • Confirm you forward the payload as { data } (an object with a data field), not the data map on its own.
  • Confirm you are reading from the correct path: notification.request.trigger.remoteMessage.data in the foreground, and the background task payload's data field in the background.

The same notification is handled twice

  • Confirm the background task returns early when AppState.currentState === 'active', so foreground pushes are not processed by both the listener and the background task.

Background notifications not handled

  • Confirm the background task is registered with Notifications.registerTaskAsync(BACKGROUND_NOTIFICATION_TASK).
  • Confirm the task name passed to defineTask and registerTaskAsync is identical.

iOS

Notifications not received on iOS

  • Confirm mode matches your build type (development or production).
  • Confirm your APNs certificate or key is uploaded to the WebEngage dashboard.
  • Test on a physical device — push does not work on the iOS simulator.
  • Confirm notification permission is granted in iOS Settings.

Rich push (images, carousels) not showing

  • Confirm webengage-expo-push is listed in plugins in app.json.
  • Confirm the NotificationService and NotificationViewController folders exist in ios/ after prebuild.
  • Confirm the App Group group.<bundleId>.WEGNotificationGroup is enabled for all three targets (main app + both extensions) in your Apple Developer account.

Notification extensions missing after prebuild

  • If the plugin was added to an already-prebuilt project, run npx expo prebuild --clean to regenerate the native project and apply the extensions.

"Notification Service Extension target already exists" error on re-prebuild

  • This error occurs when another plugin has already created a NotificationService target. Solution: Always run npx expo prebuild --clean when re-prebuilding — for example, after adding another native dependency. --clean regenerates the native project from scratch and avoids extension conflicts.
  • If you intentionally want both WebEngage and another push provider in the same NSE, use iosNSEExistingTarget: true and provide a merged Swift file via iosNSEFilePath that handles both. Do not set iosNSEExistingTarget: true unless you are also providing iosNSEFilePath.

EAS Build fails with a provisioning profile error

  • Confirm both extension bundle IDs are registered in your Apple Developer account.
  • Confirm the App Groups capability is enabled for all three identifiers.
  • Run eas credentials to inspect and reset provisioning profiles.

Local build fails with a code signing error

  • On [email protected] or later, set devTeam (your Apple Developer Team ID) in the plugin config — it's applied automatically to the main app and both notification extension targets on the next expo prebuild.
  • On [email protected], there is no plugin config option to set the Team. After running expo prebuild, open the generated Xcode project and manually select your Team under Signing & Capabilities for all three targets — the main app, NotificationService, and NotificationViewController.
  • Not needed for EAS Build, which manages signing automatically regardless of plugin version.

Need Help?

If you have any questions or issues with the WebEngage Expo push integration, contact:

[email protected]


Did this page help you?