PushEngage with Expo and Bare Workflow
Which React Native setup should you use with PushEngage? This guide explains the differences, what works, what doesn't, and how to set up PushEngage in each environment.
Quick Answer
| Setup | Supported | Notes |
|---|---|---|
| Bare Workflow | Yes | Full support, recommended |
| Expo Development Build | Yes (with manual native config) | Requires expo-dev-client + npx expo prebuild |
| Expo Go | No | Expo Go cannot run custom native modules |
PushEngage React Native SDK includes native code (Swift/Kotlin) and uses React Native's New Architecture (TurboModules). It requires access to the native ios/ and android/ project directories — which bare workflow provides by default and Expo provides via development builds.
Understanding the Difference
Bare Workflow
A standard React Native project with full native directories (ios/ and android/). You have direct access to Xcode and Android Studio projects.
When to use: You need full control over native configuration, you're already using bare workflow, or you want the simplest PushEngage setup.
Expo Managed Workflow (with Development Builds)
Expo manages most native configuration for you. Use npx expo prebuild to generate native directories, and expo-dev-client to build a custom development app that includes PushEngage's native code.
When to use: Your team already uses Expo and you want to keep the Expo developer experience (EAS Build, OTA updates).
Expo Go
A pre-built app from the App Store / Play Store for quick prototyping. It contains a fixed set of native modules.
Why it doesn't work: Expo Go does not include PushEngage's native SDK. When your JavaScript tries to call PushEngage's native module, it crashes because the native code doesn't exist in the Expo Go binary. This is true for all third-party push notification SDKs — not just PushEngage.
Setup: Bare Workflow (Recommended)
This is the standard setup. If you're using bare React Native, follow these steps. For a complete walkthrough including Firebase and APNs setup, see the React Native Quickstart.
1. Install the SDK
npm install @pushengage/pushengage-react-native
# or
yarn add @pushengage/pushengage-react-native
2. iOS Setup
Install pods:
cd ios && pod install && cd ..
Then in Xcode:
- Add Push Notifications capability to your main target
- Add Background Modes capability and enable Remote notifications
- Add App Groups capability (same group ID on main target and any extensions)
- Add to your
Info.plist:<key>PushEngage_App_Group_Key</key>
<string>group.com.yourcompany.yourapp</string>
For rich notifications (images, action buttons), add a Notification Service Extension target in Xcode.
3. Android Setup
Add the JitPack repository to android/settings.gradle:
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven { url 'https://jitpack.io' }
}
}
Add the Google Services classpath to your project-level android/build.gradle:
buildscript {
dependencies {
classpath 'com.google.gms:google-services:4.4.0'
}
}
Add the Google Services plugin to android/app/build.gradle:
plugins {
id 'com.android.application'
id 'com.google.gms.google-services'
}
Place your google-services.json in android/app/, and add the notification permission to android/app/src/main/AndroidManifest.xml:
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
4. Initialize and Use
Initialize PushEngage in index.js before AppRegistry.registerComponent — this is the earliest entry point and guarantees the SDK is ready before any component renders:
import { AppRegistry } from 'react-native';
import PushEngage from '@pushengage/pushengage-react-native';
import App from './App';
PushEngage.setAppId('YOUR_APP_ID');
AppRegistry.registerComponent('YourAppName', () => App);
Then in your app, listen for deep links and request permission:
import React, { useEffect } from 'react';
import { Button, View } from 'react-native';
import type { EventSubscription } from 'react-native';
import PushEngage from '@pushengage/pushengage-react-native';
function App() {
const subscriptionRef = React.useRef<EventSubscription | null>(null);
useEffect(() => {
// Fires when a notification is tapped and contains a deep link
subscriptionRef.current = PushEngage.onValueChanged(
(event: { deepLink: string; data: { [key: string]: string } }) => {
console.log('Deep link:', event.deepLink);
console.log('Notification data:', event.data);
// Navigate using your router here
},
);
return () => {
subscriptionRef.current?.remove();
subscriptionRef.current = null;
};
}, []);
const handleEnablePush = async () => {
const granted = await PushEngage.requestNotificationPermission();
if (granted) {
console.log('User is now subscribed to push notifications');
}
};
return (
<View>
<Button title="Enable Notifications" onPress={handleEnablePush} />
</View>
);
}
export default App;
You do not need to call PushEngage.subscribe() after requestNotificationPermission(). The SDK automatically subscribes the user when permission is granted. Only call subscribe() to re-subscribe a user who was previously unsubscribed via unsubscribe().
Setup: Expo with Development Builds
If your project uses Expo, you can still use PushEngage by generating native directories and building a custom development client.
Prerequisites
- Expo SDK 49+
- EAS CLI installed (
npm install -g eas-cli) - Apple Developer account (iOS) and Firebase project (Android)
1. Install Dependencies
npx expo install expo-dev-client
npm install @pushengage/pushengage-react-native
2. Generate Native Directories
npx expo prebuild --clean
This creates the ios/ and android/ directories. You now have access to the same native files as bare workflow.
After running prebuild, you're effectively in bare workflow for native configuration. The difference is that Expo tooling (EAS Build, OTA updates) still works.
3. Configure Native Projects
Follow the same iOS and Android configuration steps as bare workflow above:
iOS (in the generated ios/ directory):
- Open
ios/YourApp.xcworkspacein Xcode - Add Push Notifications, Background Modes (Remote notifications), and App Groups capabilities
- Add
PushEngage_App_Group_KeytoInfo.plist - (Optional) Add Notification Service Extension for rich notifications
Android (in the generated android/ directory):
- Add JitPack repository to
android/settings.gradle - Add Google Services classpath to project-level
android/build.gradle - Add Google Services plugin to
android/app/build.gradle - Place
google-services.jsoninandroid/app/ - Add
POST_NOTIFICATIONSpermission toAndroidManifest.xml
4. Build Your Development Client
Locally:
# iOS
npx expo run:ios
# Android
npx expo run:android
With EAS Build:
eas build --profile development --platform ios
eas build --profile development --platform android
Your eas.json should include a development profile:
{
"build": {
"development": {
"developmentClient": true,
"distribution": "internal"
},
"production": {
"distribution": "store"
}
}
}
5. Use PushEngage in Your Code
The JavaScript API is identical to bare workflow. Initialize in index.js:
import { AppRegistry } from 'react-native';
import PushEngage from '@pushengage/pushengage-react-native';
import App from './App';
PushEngage.setAppId('YOUR_APP_ID');
AppRegistry.registerComponent('YourAppName', () => App);
Then use onValueChanged and requestNotificationPermission in your components exactly as shown in the bare workflow section above.
6. Production Build
eas build --profile production --platform all
What About Continuous Native Generation?
If you use Expo's Continuous Native Generation (CNG) pattern — where ios/ and android/ are gitignored and regenerated on each build — you'll need to ensure your native PushEngage configuration is reapplied each time.
Options:
Commit your native directories — Add
ios/andandroid/to version control so your manual changes persist. Simple and reliable, but you lose the "clean slate" benefit of CNG.Custom config plugin — Create a local Expo config plugin that automates the native configuration. This is the recommended approach for CNG workflows.
Example local config plugin:
const { withInfoPlist, withAndroidManifest } = require('expo/config-plugins');
function withPushEngage(config, { appGroupId }) {
// Add PushEngage_App_Group_Key to Info.plist
config = withInfoPlist(config, (c) => {
c.modResults.PushEngage_App_Group_Key = appGroupId;
return c;
});
// Add POST_NOTIFICATIONS permission to AndroidManifest
config = withAndroidManifest(config, (c) => {
const manifest = c.modResults.manifest;
if (!manifest['uses-permission']) {
manifest['uses-permission'] = [];
}
const alreadyAdded = manifest['uses-permission'].some(
(p) => p.$?.['android:name'] === 'android.permission.POST_NOTIFICATIONS',
);
if (!alreadyAdded) {
manifest['uses-permission'].push({
$: { 'android:name': 'android.permission.POST_NOTIFICATIONS' },
});
}
return c;
});
return config;
}
module.exports = withPushEngage;
Register it in app.config.js:
module.exports = {
plugins: [
[
'./plugins/withPushEngage',
{ appGroupId: 'group.com.yourcompany.yourapp' },
],
],
};
Push Notifications and Background Modes capabilities cannot be set via config plugin alone — they require entitlements. You can automate those too using withEntitlementsPlist, but for most projects it's simpler to configure them once in Xcode and commit the ios/ directory.
Feature Comparison
| Feature | Bare Workflow | Expo Dev Build | Expo Go |
|---|---|---|---|
| PushEngage SDK | ✓ | ✓ | ✗ |
| Rich notifications (images) | ✓ | ✓ (with NSE) | ✗ |
| Deep linking | ✓ | ✓ | ✗ |
| Segments & attributes | ✓ | ✓ | ✗ |
| Goal tracking | ✓ | ✓ | ✗ |
| Trigger campaigns | ✓ | ✓ | ✗ |
| Hot reload during dev | ✓ | ✓ | ✓ (no push) |
| OTA updates (EAS Update) | ✓ (with expo-updates) | ✓ | ✓ |
| EAS Build | ✓ | ✓ | N/A |
| Direct Xcode/Android Studio access | ✓ | ✓ (after prebuild) | ✗ |
Troubleshooting
"TurboModuleRegistry: Module not found" Error
Cause: PushEngage's native module isn't available. You're likely running in Expo Go or haven't built a development client yet.
Fix: Build a development client with npx expo run:ios or npx expo run:android. Do not use Expo Go for testing push notifications.
"No Firebase App" Error on Android
Cause: google-services.json is missing or the Google Services plugin isn't applied.
Fix: Ensure google-services.json is in android/app/ and the plugin is applied in android/app/build.gradle. Also verify the Google Services classpath is in the project-level android/build.gradle.
iOS Notifications Not Received
Cause: Missing Push Notifications capability or APNs configuration.
Fix:
- Open Xcode and verify Push Notifications capability is enabled on your main target
- Verify your APNs key is uploaded in the PushEngage Dashboard → Site Settings → Installation → iOS SDK
- Test on a physical device — Simulator cannot receive push notifications
Notifications Work in Dev but Not Production
Cause: aps-environment entitlement mismatch.
Fix: Ensure your production provisioning profile has Push Notifications enabled. Verify the entitlement:
codesign -d --entitlements :- YourApp.app | grep aps-environment
# Should output: <string>production</string>
After npx expo prebuild, Native Changes Are Lost
Cause: Prebuild regenerates native directories from scratch.
Fix: Use a local config plugin (see the CNG section above) to automate native configuration, or commit your ios/ and android/ directories to version control.
Migrating Between Setups
From Expo Go to Development Build
- Run
npx expo prebuild - Install PushEngage:
npm install @pushengage/pushengage-react-native - Run
cd ios && pod install - Configure native projects (capabilities, Firebase, etc.)
- Build and test with
npx expo run:ios/npx expo run:android
From expo-notifications to PushEngage
If you're switching from Expo's built-in push service:
- Remove
expo-notificationsdependency - Remove any Expo push token logic (e.g.,
getExpoPushTokenAsync) - Install PushEngage SDK
- Replace Expo's permission request with
PushEngage.requestNotificationPermission()— this both requests the OS permission and subscribes the user automatically - Replace Expo notification tap handlers with
PushEngage.onValueChangedfor deep link routing on notification tap - Update your backend to send notifications via the PushEngage REST API instead of the Expo push API