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-notificationsor@react-native-firebase/messaging) and forwarded to WebEngage using thereact-native-webengageAPIs. - iOS — push is configured automatically by the
webengage-expo-pushconfig 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 yourgoogle-services.jsonin the project, referenced viaandroid.googleServicesFileinapp.json— do not remove it when migrating off@react-native-firebase.
Option A — Using expo-notifications
expo-notificationsIf 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-manageryarn add expo-notifications expo-task-manager
Payload shapeWebEngage's native
onMessageReceivedreads the FCM data map frommessage.dataand only handles messages wheredata.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 FirebaseRemoteMessage, so the FCM data map is under itsdatafield
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
AppStatecheck? 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 whenAppState.currentStateisactive, 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
@react-native-firebase/messagingIf 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-pushyarn add webengage-expo-pushConfiguration
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-exporequires 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; ifwebengage-expois already configured elsewhere in yourpluginsarray, just add thewebengage-expo-pushentry to it.
Important: The
modevalue must match thepush.modevalue configured inwebengage-expo.
Configuration Parameters
| Key | Description | Required |
|---|---|---|
mode | APNs environment: development or production. Must match push.mode in webengage-expo. Defaults to development if omitted. | Yes |
iPhoneDeploymentTarget | Minimum iOS version for extensions. Defaults to 15.1. | No |
iosNSEFilePath | Path to a custom NotificationService.swift file. | No |
iosCEFilePath | Path to a custom NotificationViewController.swift file. | No |
devTeam | Apple Developer Team ID, applied to the main app and both notification extension targets. | No |
useSPM | Use Swift Package Manager instead of CocoaPods for the extension dependencies. Defaults to false. | No |
iosNSETargetName | Custom name for the Notification Service Extension target. Defaults to NotificationService. | No |
iosCETargetName | Custom name for the Content Extension (NotificationViewController) target. Defaults to NotificationViewController. | No |
disableNSE | Skip Notification Service Extension creation entirely — no target, no files, no Podfile entry. Defaults to false. | No |
iosNSEExistingTarget | Set 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 |
appGroupName | Custom iOS App Group name for entitlements. Defaults to group.{bundleId}.WEGNotificationGroup. | No |
iosCEFilePath,devTeam,useSPM,iosNSETargetName,iosCETargetName, anddisableNSErequire[email protected]or later. Check your installed version withnpm ls webengage-expo-pushbefore using them — on0.0.1these 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 prebuildAfter 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 --cleanso the notification extensions are applied. See When should I rebuild? in the integration guide.
Supported Environments
| Environment | Supported |
|---|---|
| Expo Go | No |
| EAS Build (Managed) | Yes |
| Prebuild Workflow | Yes |
| Custom Dev Client | Yes |
| Bare React Native | Yes |
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.jsonis present in your project and referenced viaandroid.googleServicesFileinapp.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 wheredata.source === "webengage". - Confirm you forward the payload as
{ data }(an object with adatafield), not the data map on its own. - Confirm you are reading from the correct path:
notification.request.trigger.remoteMessage.datain the foreground, and the background task payload'sdatafield 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
defineTaskandregisterTaskAsyncis identical.
iOS
Notifications not received on iOS
- Confirm
modematches your build type (developmentorproduction). - 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-pushis listed inpluginsinapp.json. - Confirm the
NotificationServiceandNotificationViewControllerfolders exist inios/after prebuild. - Confirm the App Group
group.<bundleId>.WEGNotificationGroupis 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 --cleanto 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
NotificationServicetarget. Solution: Always runnpx expo prebuild --cleanwhen re-prebuilding — for example, after adding another native dependency.--cleanregenerates 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: trueand provide a merged Swift file viaiosNSEFilePaththat handles both. Do not setiosNSEExistingTarget: trueunless you are also providingiosNSEFilePath.
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 credentialsto inspect and reset provisioning profiles.
Local build fails with a code signing error
- On
[email protected]or later, setdevTeam(your Apple Developer Team ID) in the plugin config — it's applied automatically to the main app and both notification extension targets on the nextexpo prebuild. - On
[email protected], there is no plugin config option to set the Team. After runningexpo prebuild, open the generated Xcode project and manually select your Team under Signing & Capabilities for all three targets — the main app,NotificationService, andNotificationViewController. - 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:
Updated about 3 hours ago