xtremepush-expo-plugin
v1.2.9
Published
XtremePush Expo Config Plugin
Maintainers
Readme
XtremePush Expo Plugin
Expo config plugin that integrates the Xtremepush SDK functionality with full React Native module support for both iOS and Android platforms.
- Latest version: 1.2.9
- Supported platforms: iOS 15.0+, Android 5.0+ (API 21)
- Supported Expo SDK: 51 and later (53+ recommended)
- Distribution: managed Expo projects via EAS, or local
expo run:ios/expo run:androidbuilds. Does not work in Expo Go.
Table of contents
- What's new in 1.2.9
- Requirements
- Installation
- Quick start
- Plugin configuration reference
- iOS setup
- Android setup
- JavaScript API
- Advanced topics
- Validation codes
- Troubleshooting
- Migration guides
- Changelog
- License
What's new in 1.2.9
1.2.9 closes a class of EAS-managed-build failures that had previously required clients to hand-edit several files.
| Change | Impact |
|---|---|
| EAS appExtensions block auto-injected | The plugin writes extra.eas.build.experimental.ios.appExtensions automatically when an NSE is created on an EAS project. No more hand-written JSON in app.json. |
| apsEnvironment auto-derived | Defaults to 'production' when an NSE is created on an EAS project. Covers Ad-Hoc, App Store, and internal-distribution profiles. |
| NSE entitlements include aps-environment | The generated <NSE>.entitlements now carries the aps-environment key, so EAS managed credentials enable Push Notifications on the NSE App ID. Fixes the most common cause of Provisioning profile doesn't include the aps-environment entitlement Xcode signing failures. |
| devTeam is now required when an NSE is created | Throws the new XP_E003 at prebuild if missing, with a clear remediation message. Replaces opaque downstream signing failures. |
| XP_W004 retuned | Fires only for apsEnvironment: 'development' on EAS projects with an NSE — the genuinely risky combination. The auto-derived 'production' value never warns. |
| XP_W005 retuned | Auto-injection runs first; this warning now only flags genuine user-entered mistakes in a hand-written block. |
| XP_W006 (new) | Flags aps-environment mismatches between plugin config and any hand-written appExtensions entry. |
If you're already on 1.2.8, see Migrating from 1.2.8 — most projects need no plugin-config changes, only the addition of devTeam.
Requirements
System
- Node.js 18.0 or later
- Expo SDK 51 or later (53+ recommended)
- React Native 0.73 or later
- EAS CLI 18 or later (only if building with EAS)
iOS
- Deployment target 15.0 or later (the plugin sets this automatically for the NSE)
- Xcode 14.0 or later
- CocoaPods latest
- An Apple Developer Program membership (paid, individual or organisation)
- For rich media or delivery receipts, the ability to register an App Group identifier in Apple Developer Portal
Android
- minSdkVersion 21 (Android 5.0)
- targetSdkVersion 34 (Android 14)
- Gradle 8.0 or later
- Google Play Services for FCM
- A Firebase project with an Android app entry whose package name matches
android.package
Installation
# npm
npm install xtremepush-expo-plugin
# yarn
yarn add xtremepush-expo-plugin
# pnpm
pnpm add xtremepush-expo-pluginThe plugin pulls in the native iOS and Android XtremePush SDKs at build time via CocoaPods and Gradle respectively. There's no separate native install step.
Quick start
This config will enable you to integrate the basic connectivity:
// app.config.js
export default {
expo: {
name: 'YourApp',
slug: 'your-app',
ios: {
bundleIdentifier: 'com.yourcompany.yourapp',
},
android: {
package: 'com.yourcompany.yourapp',
googleServicesFile: './google-services.json',
},
plugins: [
['xtremepush-expo-plugin', {
applicationKey: 'YOUR_XTREMEPUSH_APP_KEY',
iOSAppKey: 'YOUR_IOS_APP_KEY',
androidAppKey: 'YOUR_ANDROID_APP_KEY',
googleSenderId: 'YOUR_FCM_SENDER_ID',
}],
],
},
};Then:
npx expo prebuild --cleanThat gets you basic push delivery. To enable rich media and delivery receipts on iOS — recommended for production — see Quick start with rich media + delivery receipts below.
Quick start with rich media + delivery receipts
// app.config.js
export default {
expo: {
name: 'YourApp',
slug: 'your-app',
ios: {
bundleIdentifier: 'com.yourcompany.yourapp',
},
android: {
package: 'com.yourcompany.yourapp',
googleServicesFile: './google-services.json',
},
plugins: [
['xtremepush-expo-plugin', {
applicationKey: 'YOUR_XTREMEPUSH_APP_KEY',
googleSenderId: 'YOUR_FCM_SENDER_ID',
// iOS rich media + delivery receipts
enableRichMedia: true,
enableDeliveryReceipts: true,
iosAppGroupIdentifier: 'group.com.yourcompany.yourapp.xtremepush.suit',
devTeam: 'ABCDE12345', // your Apple Team ID
}],
],
extra: {
eas: {
projectId: 'your-eas-project-id', // run `eas init` to populate
},
},
},
};The plugin auto-injects the
extra.eas.build.experimental.ios.appExtensionsblock at prebuild. Do not write it by hand. See Apple Developer Portal for the one-time Apple-side setup.
Plugin configuration reference
Every option goes inside the plugin's options object in app.config.js / app.json:
plugins: [
['xtremepush-expo-plugin', { /* options here */ }],
]Required
| Option | Type | Description |
|---|---|---|
| applicationKey | string | Your XtremePush application key (from the dashboard). |
| googleSenderId | string | Firebase numeric Sender ID. Required for Android push. |
Required when an NSE is created
(i.e. when enableRichMedia or enableDeliveryReceipts is true.)
| Option | Type | Description |
|---|---|---|
| devTeam | string | Apple Developer Team ID (e.g. 'ABCDE12345'). The plugin throws XP_E003 at prebuild if missing. Find it at developer.apple.com → Membership or in the output of eas credentials -p ios. |
Push permission control
| Option | Type | Default | Description |
|---|---|---|---|
| enablePushPermissions | boolean | true | Auto-request the OS push prompt at launch. Set to false to defer until you call requestNotificationPermissions() from JS. Also controls whether POST_NOTIFICATIONS is added to the Android manifest. |
Feature flags
| Option | Type | Default | Description |
|---|---|---|---|
| enableRichMedia | boolean | false | iOS rich media (image/video). Creates the Notification Service Extension. |
| enableDeliveryReceipts | boolean | false | Push delivery receipts. iOS: also creates the NSE. Android: requires SDK ≥ 7.9.0. |
| enableEncryptedPush | boolean | false | iOS encrypted push payloads. Requires uploading the public key in the dashboard. |
| enableEncryptedMessages | boolean | false | Android encrypted messages. Requires Android SDK ≥ 8.1.0 plus a dashboard-side key. |
| enableInAppMessaging | boolean | true | In-app message rendering. |
| enableLocationServices | boolean | true | Adds location permissions on Android (ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION, ACCESS_BACKGROUND_LOCATION) and the iOS usage descriptions. Set to false if you don't use location features — recommended to avoid Play / App Store review scrutiny. |
| enableStartSession | boolean | true | Android setEnableStartSession(true) — app-launch session tracking. |
| enableInboxBadge | boolean | true | Android setInboxBadgeEnabled(true) — inbox unread counter. |
| enableDebugLogs | boolean | false | Verbose SDK logging on both platforms. |
iOS
| Option | Type | Default | Description |
|---|---|---|---|
| iosAppKey | string | applicationKey | iOS-specific application key override. |
| iosAppGroupIdentifier | string | auto-derived | App Group shared between the main app and NSE. Must end with .xtremepush.suit when enableDeliveryReceipts is true (XP_E001 otherwise). Auto-derived as group.<bundleId>.xtremepush.suit for delivery receipts, or group.<bundleId>.xtremepush for rich-media-only. |
| iosAppGroup | string | unset | Legacy alias for iosAppGroupIdentifier, used when only rich media is enabled. Prefer iosAppGroupIdentifier. |
| nseTargetName | string | 'XtremePushNotificationServiceExtension' | Override the NSE Xcode target name. Almost never needed. |
| iosSdkVersion | string | '6.1' | Pin the Xtremepush-iOS-SDK CocoaPods version (e.g. '6.1' produces pod 'Xtremepush-iOS-SDK', '~> 6.1'). |
| apsEnvironment | 'development' | 'production' | auto for EAS | (1.2.9+) Auto-derived to 'production' when an NSE is created on an EAS project. Override only if you build with an EAS Development-type provisioning profile and need sandbox tokens. |
Android
| Option | Type | Default | Description |
|---|---|---|---|
| androidAppKey | string | applicationKey | Android-specific application key override. |
| androidDependency | string | 'ie.imobile.extremepush:XtremePush_lib:9.7.1' | Full Gradle dependency string. Use this for Huawei HMS builds or custom artifacts. |
| androidDependencyVersion | string | '9.7.1' | Override just the version of the default dependency. Ignored if androidDependency is set. |
SSL certificate pinning
| Option | Type | Default | Description |
|---|---|---|---|
| enablePinning | boolean | false | Enables iOS file-based pinning. Requires certificatePath. |
| certificatePath | string | unset | Path (relative to project root) to your .der certificate. Used for both the main app and the NSE. |
| serverExpectedPublicKey | string | unset | Android public-key pin. Hex-encoded SubjectPublicKeyInfo. Independent of the iOS file-based options. |
Server region
| Option | Type | Default | Description |
|---|---|---|---|
| useUsServer | boolean | false | Use the US data centre (https://sdk.us.xtremepush.com). |
| serverUrl | string | unset | Custom XtremePush server URL. Takes precedence over useUsServer. |
| usServerUrl | string | 'https://sdk.us.xtremepush.com' | Override the default US URL when useUsServer is true. |
Delivery receipts
| Option | Type | Default | Description |
|---|---|---|---|
| deliveryReceiptsEndpoint | string | unset | If set, receipts POST to this URL instead of XtremePush. Both platforms. |
Callbacks
These three pairs require both a plugin option (which tells the native SDK to register a listener) and a JS subscription with the same event name. Without the plugin option, the native SDK never registers and your JS handler never fires.
| Option | Type | JS subscription helper |
|---|---|---|
| messageResponseCallback | string (event name) | onMessageResponse |
| inboxBadgeCallback | string (event name) | onInboxBadgeUpdate |
| deeplinkCallback | string (event name) | onDeeplinkReceived |
Full example
// app.config.js — every option populated
export default {
expo: {
plugins: [
['xtremepush-expo-plugin', {
// Required
applicationKey: 'YOUR_APP_KEY',
googleSenderId: 'YOUR_FCM_SENDER_ID',
// Platform-specific keys (optional)
iosAppKey: 'IOS_KEY',
androidAppKey: 'ANDROID_KEY',
// Push behaviour
enablePushPermissions: true,
enableInAppMessaging: true,
enableLocationServices: false,
enableStartSession: true,
enableInboxBadge: true,
enableDebugLogs: false,
// Rich media + delivery receipts (creates iOS NSE)
enableRichMedia: true,
enableDeliveryReceipts: true,
deliveryReceiptsEndpoint: 'https://your-server.com/receipts',
iosAppGroupIdentifier: 'group.com.yourcompany.yourapp.xtremepush.suit',
devTeam: 'ABCDE12345',
nseTargetName: 'XtremePushNotificationServiceExtension',
iosSdkVersion: '6.1',
// Encryption (require dashboard-side keys)
enableEncryptedPush: true,
enableEncryptedMessages: true,
// SSL pinning
enablePinning: true,
certificatePath: 'assets/cert.der',
serverExpectedPublicKey: '30820122...010001',
// Server region
useUsServer: true,
// Custom Android dependency (e.g. Huawei)
androidDependency: 'com.huawei.hms:XtremePush_lib:9.7.1',
// Callbacks
messageResponseCallback: 'onMessageResponse',
inboxBadgeCallback: 'onInboxBadgeUpdate',
deeplinkCallback: 'onDeeplinkReceived',
}],
],
},
};iOS setup
Apple Developer Portal
For the most common configuration (rich media + delivery receipts), you need to register an App Group identifier before building. Apple's API does not let EAS or the plugin create this on your behalf in all eas-cli versions. (Recent eas-cli versions can — the credentials wizard will print Created: group.<…> if so. If it doesn't, do it manually.)
- Sign in at developer.apple.com.
- Identifiers →
+→ App Groups → Continue. - Identifier:
group.<your.bundle.id>.xtremepush.suit— must matchiosAppGroupIdentifierin your plugin config exactly. The.suitsuffix is required by the XtremePush iOS SDK. - Save.
EAS auto-creates the App IDs (main app + NSE) on the first build. If you need to do this manually:
- Identifiers →
+→ App IDs → App → Continue. - Bundle ID (Explicit): your bundle identifier.
- Tick capabilities: Push Notifications, App Groups.
- Save.
- Repeat for the NSE bundle ID:
<your.bundle.id>.XtremePushNotificationServiceExtension.
After both App IDs exist, click each one and configure the App Groups capability to point at the App Group from step 3 above.
APNs credentials
APNs Certificate (.p12)
Use this only if your provider mandates certificate-based APNs.
- Apple Developer Portal → Certificates →
+. - Choose either Apple Push Notification service SSL (Sandbox) for development, or Apple Push Notification service SSL (Sandbox & Production) for App Store / TestFlight.
- Continue → select your App ID → continue.
- Upload a Certificate Signing Request — generate one in Keychain Access → menu → Certificate Assistant → Request a Certificate from a Certificate Authority → save to disk.
- Upload the CSR → download the
.cer→ double-click to install in Keychain. - In Keychain Access → My Certificates → expand to expose the private key → right-click → Export as
.p12. Set a password. - Upload to XtremePush dashboard → Settings → iOS with the password.
Building with EAS (iOS)
Add a preview profile to eas.json for sideloadable Ad-Hoc builds (for production App Store submission, see Production builds):
{
"cli": { "version": ">= 18.0.0", "appVersionSource": "remote" },
"build": {
"development": {
"developmentClient": true,
"distribution": "internal",
"ios": { "simulator": false }
},
"preview": {
"distribution": "internal",
"ios": { "simulator": false, "buildConfiguration": "Release" }
},
"production": {
"autoIncrement": true,
"ios": { "resourceClass": "m-medium" }
}
}
}Register your test device once:
eas device:create
# Choose "Website" → open the URL on the iPhone in Safari → install the profileBuild:
eas build --platform ios --profile preview --clear-cacheWhen the credentials wizard runs, answer:
| Prompt | Answer |
|---|---|
| Log in to your Apple account? | yes, then your Apple ID + 2FA |
| (After login, watch for) Synced capabilities | Should print Enabled: Push Notifications (or App Groups, Push Notifications), or No updates. If it says Disabled, Ctrl+C — see Troubleshooting |
| Reuse this distribution certificate? | Y |
| Generate a new Apple Provisioning Profile? | Y |
| Select devices for the ad hoc build | Pick your test iPhone (Space to toggle) |
The wizard runs once for the main target and once for the NSE — answer the same way for both.
Verifying the build with xtremepush-verify-build
Once the build completes (5–15 min), download the .ipa and inspect it before installing:
npx xtremepush-verify-build ~/Downloads/your-build.ipaExpected output (production build):
── Main app (YourApp.app) ─────────────────
ℹ Bundle ID: com.yourcompany.yourapp
✓ aps-environment: production
ℹ Apple Team ID: ABCDE12345
✓ App Groups: group.com.yourcompany.yourapp.xtremepush.suit
── Extension (XtremePushNotificationServiceExtension.appex) ──
ℹ Bundle ID: com.yourcompany.yourapp.XtremePushNotificationServiceExtension
✓ aps-environment: production
ℹ Apple Team ID: ABCDE12345
✓ App Groups: group.com.yourcompany.yourapp.xtremepush.suitBoth rows must show ✓ for aps-environment and App Groups. Failure modes flagged by the tool:
| Symptom | Meaning | Fix |
|---|---|---|
| Main app aps-environment missing | Push capability not enabled on the main App ID | Apple Developer Portal → tick Push Notifications → re-run eas build --clear-cache |
| NSE aps-environment missing | Push capability not enabled on the NSE App ID | Same fix on the NSE App ID |
| Main and NSE aps-environment differ | Profiles regenerated against different environments | Regenerate both profiles together; eas build --clear-cache |
| App Groups don't overlap | App Group not enabled on both App IDs | Wire it on both; rebuild with --clear-cache |
macOS-only: the verifier shells out to
security cms -Dto decode embedded mobileprovision files. iOS.ipas are inspected on macOS in practice, so this isn't a real restriction.
Then install:
eas build:run --platform ios --latestConnect the iPhone via cable. EAS detects it and installs the .ipa directly. Open the app, accept the push prompt, background and foreground once, then check the XtremePush dashboard. Within ~1 minute the device should show Addressable: Yes.
Building locally with Xcode
Local builds work for testing without going through EAS:
npx expo run:ios --devicePick your connected iPhone from the device list. Xcode handles signing automatically using a Personal Team — push will work end-to-end as long as the App Group, App IDs, and APNs credential are configured (see above).
Local builds use
personalTeamprofiles which are sandbox-only and limited to 7-day install duration. For longer-term testing on a real device, use the EASpreviewprofile.
Production builds
For App Store submission:
eas build --platform ios --profile production
eas submit --platform ios --latestEAS uses an App Store distribution certificate and an App Store provisioning profile. The verifier should still show aps-environment: production on both rows (production .p8 or .p12 covers Ad-Hoc, App Store, and internal-distribution).
Android setup
Firebase project
- Sign in at console.firebase.google.com → create a project (or reuse one).
- Add an Android app:
- Package name must match
android.packageinapp.config.jsexactly. - SHA-1 is optional for FCM; required only for additional Firebase services.
- Package name must match
- Download
google-services.jsonfrom the Android app's settings. - Open Project Settings → Cloud Messaging → copy the numeric Sender ID (12 digits). This goes into
googleSenderIdin your plugin config.
google-services.json placement
Place the file at the project root (not android/app/) and reference it from app.json:
"android": {
"package": "com.yourcompany.yourapp",
"googleServicesFile": "./google-services.json"
}expo prebuild copies it into android/app/ on every prebuild. Putting it at the root keeps it portable across machines and visible to EAS uploads (the default .gitignore excludes android/, which would otherwise hide the file from the EAS build server).
Do not commit secrets. If your Firebase project is shared, treat
google-services.jsonas sensitive; use EAS file environment variables to inject it at build time instead of committing it.
XtremePush dashboard credentials (Android)
Modern Firebase projects use the FCM HTTP v1 API, which requires a Service Account JSON key:
- Firebase Console → Project Settings → Service Accounts tab → Generate new private key.
- Save the JSON file.
- XtremePush dashboard → Settings → Android for your app → upload the Service Account JSON.
(If you're on the legacy FCM Server Key flow, upload that instead — but Google has deprecated it.)
Building with EAS (Android)
eas build --platform android --profile previewThe credentials wizard for Android is much simpler than iOS — just one prompt:
✔ Generate a new Android Keystore? › (Y/n)Answer Y. EAS generates a keystore for internal-distribution builds. No Apple Developer-equivalent setup, no certificates to manage.
When the build completes:
eas build:run --platform android --latestWith your phone connected via USB and USB Debugging enabled (Settings → System → Developer options). The .apk installs directly. Or scan the QR code from the EAS terminal output to install over Wi-Fi.
After install: open the app, accept the notification permission prompt (Android 13+), confirm the device shows in the XtremePush dashboard, send a test campaign. Both Android push and rich-media images render through the FCM/XtremePush pipeline; the iOS NSE setup has no equivalent on Android.
JavaScript API
Import from xtremepush-expo-plugin/plugins/xtremepush. Every public function is listed below. TypeScript users can also import types from the package root.
import {
// Identity
setUser, setExternalId,
// Tracking
hitEvent, hitEventWithValue, hitEventWithValues, hitImpression,
hitTag, hitTagWithValue,
// Push
requestNotificationPermissions,
getInitialNotification,
// Inbox UI
openInbox,
// Inbox APIs
getInboxMessages, getInboxBadge, deleteInboxMessage,
reportMessageOpened, reportMessageClicked,
// Subscriptions
onMessageResponse, onInboxBadgeUpdate, onDeeplinkReceived,
// Diagnostics
isAvailable, checkPushNotificationStatus, getCurrentDeviceToken,
constants,
} from 'xtremepush-expo-plugin/plugins/xtremepush';
import type {
XtremePushPluginConfig,
XtremePushNotificationPayload,
XtremePushNativeModule,
XtremePushMessageResponseEvent,
XtremePushInboxBadgeEvent,
XtremePushDeeplinkEvent,
XtremePushSubscription,
InboxMessage,
} from 'xtremepush-expo-plugin';Module availability
import { isAvailable } from 'xtremepush-expo-plugin/plugins/xtremepush';
if (isAvailable()) {
// The native module is loaded.
} else {
// Expo Go, web, or a build that hasn't run prebuild — fall back gracefully.
}The plugin does not work in Expo Go. Use a development build (npx expo run:ios / npx expo run:android) or an EAS build.
User identity
import { setUser, setExternalId } from 'xtremepush-expo-plugin/plugins/xtremepush';
setUser('[email protected]'); // your primary user ID
setExternalId('CRM-12345'); // legacy / external CRM ID
// On logout — pass empty string, null, or undefined to reset.
// (1.2.8+: iOS reached parity with Android on this; previous versions
// silently ignored empty strings on iOS.)
setUser('');
setExternalId('');Events, impressions, and tags
import {
hitEvent, hitEventWithValue, hitEventWithValues,
hitImpression, hitTag, hitTagWithValue,
} from 'xtremepush-expo-plugin/plugins/xtremepush';
// Bare event
hitEvent('app_opened');
// Event with a single string value
hitEventWithValue('purchase_completed', '49.99');
// Event with key-value pairs (cross-platform)
hitEventWithValues('product_added_to_basket', {
product_category: 'Sports',
product_name: 'Home Jersey',
});
// Page / screen impression
hitImpression('home_page');
hitImpression(`article_${articleId}`);
// User-segmentation tag
hitTag('vip');
hitTagWithValue('user_level', 'gold');All values are forwarded as strings on Android (the SDK signature is
HashMap<String, String>). Pass strings explicitly to keep cross-platform parity. Numbers and booleans are coerced; nested objects/arrays are dropped.
The native SDK silently ignores calls with an empty event name on iOS. Always pass non-empty strings.
Push permissions and registration
import { requestNotificationPermissions } from 'xtremepush-expo-plugin/plugins/xtremepush';
requestNotificationPermissions();Deferred prompt (recommended UX pattern)
Set enablePushPermissions: false in plugin config to suppress the launch-time prompt, then call requestNotificationPermissions() from JS at a moment that makes sense for the user (e.g. after onboarding):
// app.config.js
enablePushPermissions: false,// after onboarding
requestNotificationPermissions();Android note: if you set
enablePushPermissions: false, the plugin omitsPOST_NOTIFICATIONSfrom the manifest. To prompt later on Android 13+, add it manually:<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
Initial notification handling
When a user taps a push to launch the app from a terminated state, retrieve the payload once on launch:
import { useEffect } from 'react';
import { getInitialNotification } from 'xtremepush-expo-plugin/plugins/xtremepush';
export default function App() {
useEffect(() => {
getInitialNotification().then((payload) => {
if (payload?.deeplink) {
// navigate to the deeplink…
}
});
}, []);
return /* … */;
}The payload is cleared after the first read — call it once on app start. Shape:
{
id?: string; // notification id
campaignId?: string;
title?: string;
text?: string;
deeplink?: string;
platform?: 'ios' | 'android';
receivedAt?: number; // ms since epoch
badge?: number; // iOS only
data?: Record<string, any>; // custom payload
}Inbox — built-in UI
import { openInbox } from 'xtremepush-expo-plugin/plugins/xtremepush';
openInbox(); // opens the SDK's full-screen inbox UIInbox — custom UI
import {
getInboxMessages, getInboxBadge, deleteInboxMessage,
reportMessageOpened, reportMessageClicked,
onInboxBadgeUpdate,
} from 'xtremepush-expo-plugin/plugins/xtremepush';
import type { InboxMessage } from 'xtremepush-expo-plugin';
// Fetch a page (limit ≥ 1, offset ≥ 0; values are clamped by the JS wrapper).
const messages: InboxMessage[] = await getInboxMessages(20, 0);
// Get the inbox-wide unread total (1.2.8+; earlier versions returned the
// last-page count).
const unread = await getInboxBadge();
// Report opens / clicks. reportMessageClicked also marks the message as
// opened — don't call both for the same interaction.
await reportMessageOpened(message.identifier);
await reportMessageClicked(message.identifier, message.response.action.identifier);
// Delete
await deleteInboxMessage(message.identifier);InboxMessage shape (1.2.7+):
interface InboxMessage {
identifier: string;
isOpened: boolean;
isClicked: boolean;
isDelivered: boolean;
createTimestamp: number; // Unix milliseconds — pass directly to new Date()
expirationTimestamp: number | null;
style: Record<string, any>;
isCard: boolean;
response: {
message: {
identifier?: string;
campaignIdentifier?: string;
title?: string;
text?: string;
icon?: string;
data?: Record<string, any>;
payload?: Record<string, any>;
};
action: {
deeplink?: string;
url?: string;
identifier?: string;
};
};
}Error handling
getInboxMessages rejects with one of two specific codes:
| Code | Meaning | Recommended response |
|---|---|---|
| ERR_INBOX_FETCH | The SDK surfaced an error (network, auth, etc.) | Retry with backoff |
| ERR_SDK_NOT_READY | Device not registered yet (distinct from "inbox is empty", which resolves with []) | Retry after a short delay or after onMessageResponse fires |
try {
const messages = await getInboxMessages(20, 0);
setMessages(messages);
} catch (e: any) {
if (e.code === 'ERR_SDK_NOT_READY') {
setTimeout(loadInbox, 2000);
} else {
console.error('Inbox fetch failed:', e);
}
}Event subscriptions
All three subscriptions take an event name (matching your plugin config) and a handler. They return { remove(): void } — call .remove() from the cleanup function of your effect.
Message response callback
Fires when the user taps a push notification.
// In plugin config:
// "messageResponseCallback": "onMessageResponse"
import { onMessageResponse } from 'xtremepush-expo-plugin/plugins/xtremepush';
useEffect(() => {
const sub = onMessageResponse('onMessageResponse', (event) => {
if (event.message?.deeplink) {
navigation.navigate(event.message.deeplink);
}
});
return () => sub.remove();
}, [navigation]);Event shape:
message:{ id, title, text, deeplink, campaignId, ...customFields }response:Record<string, string>— additional metadata
Inbox badge callback
Fires when the inbox-wide unread count changes.
// In plugin config:
// "inboxBadgeCallback": "onInboxBadgeUpdate"
import { onInboxBadgeUpdate } from 'xtremepush-expo-plugin/plugins/xtremepush';
useEffect(() => {
const sub = onInboxBadgeUpdate('onInboxBadgeUpdate', ({ badge }) => {
setUnreadCount(badge);
});
return () => sub.remove();
}, []);Deeplink callback
Fires when a deeplink is received with the app in the foreground.
// In plugin config:
// "deeplinkCallback": "onDeeplinkReceived"
import { onDeeplinkReceived } from 'xtremepush-expo-plugin/plugins/xtremepush';
useEffect(() => {
const sub = onDeeplinkReceived('onDeeplinkReceived', ({ deeplink }) => {
navigation.navigate(deeplink);
});
return () => sub.remove();
}, [navigation]);Advanced topics
Server region and custom URLs
By default the SDK connects to the EU data centre. To use the US data centre:
// app.config.js
['xtremepush-expo-plugin', {
applicationKey: 'YOUR_KEY',
googleSenderId: 'YOUR_SENDER',
useUsServer: true,
}]For a custom URL (overrides useUsServer):
serverUrl: 'https://sdk.your-tenant.xtremepush.com',After changing server config, run npx expo prebuild --clean. Both platforms use the same URL.
Delivery receipts
Tells the dashboard a push was actually delivered (not just sent). Requires iOS SDK ≥ 4.1.8 and Android SDK ≥ 7.9.0.
['xtremepush-expo-plugin', {
applicationKey: 'YOUR_KEY',
googleSenderId: 'YOUR_SENDER',
enableDeliveryReceipts: true,
iosAppGroupIdentifier: 'group.com.yourcompany.yourapp.xtremepush.suit',
devTeam: 'ABCDE12345',
}]What the plugin generates:
- iOS:
XPush.setDeliveryReceiptsEnabled(true),XPush.enableAppGroups(...),application(_:didReceiveRemoteNotification:fetchCompletionHandler:)injected into AppDelegate. The Notification Service Extension is created with itsinit()configured per the XtremePush iOS Enterprise Push docs. - Android:
.setDeliveryReceiptsEnabled(true)added to thePushConnector.Builderchain inMainApplication.
After enabling, send a test push and watch the device log for:
[XPush] - userNotificationCenter willPresentNotification: { ... "delivery-receipt" = 1 ... }
[XPush] - Send request: https://sdk.<tenant>.xtremepush.com/push/api/actionHit ...
[XPush] - Request finished with api: .../actionHit ... code = 200; success = 1;The campaign should transition Sent → Delivered in the dashboard within seconds.
Encrypted push
iOS
enableEncryptedPush: trueInjects XPush.enableEncryptedPush(true) (Swift) or [XPush enableEncryptedPush] (Obj-C) into the AppDelegate setAppKey block. The Swift form is argumentless from SDK 6.1.0+.
Android
enableEncryptedMessages: trueInjects .setEncryptedMessagesEnabled(true) into the builder chain. Requires Android SDK ≥ 8.1.0.
Dashboard step (both platforms)
Upload your public encryption key in the XtremePush dashboard before enabling. Without it the SDK silently drops decryption attempts on incoming pushes.
SSL certificate pinning
Pinning the server cert is independent on each platform.
iOS — file-based pinning
Place the .der certificate in your project (e.g. assets/cert.der) and configure:
enablePinning: true,
certificatePath: 'assets/cert.der'The plugin copies the cert into the iOS bundle, registers it with Xcode's Copy Bundle Resources phase, and binds it to both the main app and the NSE target (1.2.8+). When delivery receipts are also enabled, the same cert powers TLS pinning inside the NSE.
Android — public-key pinning
serverExpectedPublicKey: 'YOUR_EXPECTED_SERVER_KEY_HERE...'Hex-encoded SubjectPublicKeyInfo. Injected as .setServerExpectedPublicKey(...) in PushConnector.Builder. To extract the key from your server cert:
echo | openssl s_client -connect sdk.your-tenant.xtremepush.com:443 -servername sdk.your-tenant.xtremepush.com 2>/dev/null \
| openssl x509 -pubkey -noout \
| openssl pkey -pubin -outform DER \
| xxd -p -c 1000If the value doesn't match what your XtremePush tenant administrator gave you, ask before pinning to it (could be MITM).
Custom Notification Service Extension
By default the NSE is named XtremePushNotificationServiceExtension. To use a different name:
nseTargetName: 'MyCustomNSETarget'The plugin creates the Xcode target, source files, entitlements, and Info.plist under that name. Update your extra.eas.build.experimental.ios.appExtensions block to match (the plugin auto-injects with the resolved name).
Custom delivery receipt endpoints
To route receipts to your own server instead of XtremePush:
enableDeliveryReceipts: true,
deliveryReceiptsEndpoint: 'https://your-server.com/receipts'Both platforms use the SDK's two-argument form, which POSTs the receipt JSON to your endpoint.
Validation codes
The plugin emits machine-readable codes at prebuild time so config errors are unambiguous. Errors block prebuild; warnings don't.
Errors
XP_E001
iosAppGroupIdentifier doesn't end with .xtremepush.suit while enableDeliveryReceipts is true. The XtremePush iOS SDK requires the suffix.
Fix: change the identifier to end with .xtremepush.suit, or omit it to accept the auto-derived value.
XP_E002
ios.bundleIdentifier is missing while enableDeliveryReceipts is true and no explicit iosAppGroupIdentifier is set. The plugin can't auto-derive the App Group without a bundle identifier.
Fix: add ios.bundleIdentifier to your Expo config, or set iosAppGroupIdentifier explicitly.
XP_E003
devTeam is missing while an NSE will be created (enableRichMedia or enableDeliveryReceipts is true). Without it, the NSE Xcode target is signed with DEVELOPMENT_TEAM = undefined, which fails downstream.
Fix: add devTeam: 'YOUR_TEAM_ID'. Find it at developer.apple.com → Membership.
Warnings
| Code | Triggered when | What to do |
|---|---|---|
| XP_W001 | Resolved Android SDK is older than the minimum required for enableDeliveryReceipts (7.9.0) | Bump androidDependencyVersion |
| XP_W002 | Resolved Android SDK is older than the minimum required for enableEncryptedMessages (8.1.0) | Bump androidDependencyVersion |
| XP_W003 | deliveryReceiptsEndpoint doesn't look like a URL | Check the value |
| XP_W004 | apsEnvironment: 'development' set on an EAS project with an NSE | Remove the override; the auto-derive picks 'production' |
| XP_W005 | A hand-written appExtensions block is present and a field is wrong | Fix the field, or remove the hand-written entry to fall back to auto-injection |
| XP_W006 | apsEnvironment plugin option disagrees with a hand-written aps-environment in appExtensions | Make the values match, or remove the hand-written value |
XP_W001
enableDeliveryReceipts requires the Android SDK to be at least 7.9.0. If your androidDependency or androidDependencyVersion resolves to an older version, the warning fires and the resulting build will silently drop delivery receipts.
XP_W002
Same idea for enableEncryptedMessages, which needs Android SDK ≥ 8.1.0.
XP_W003
deliveryReceiptsEndpoint doesn't parse as a URL. Common causes: missing https:// prefix or a trailing comma.
XP_W004
apsEnvironment: 'development' was explicitly set on a project where:
enableRichMediaorenableDeliveryReceiptsistrue, andextra.eas.projectIdis set (project uses EAS).
EAS internal-distribution and App Store builds produce production-environment APNs tokens. A development entitlement here will mismatch and produce BadDeviceToken. The auto-derived 'production' value never warns.
XP_W005
A hand-written appExtensions block was found, and the plugin couldn't safely auto-fill the missing fields without overwriting your data. The warning lists the specific fields that need attention. Remove your hand-written entry to let auto-injection handle it, or fix the listed fields.
XP_W006
The plugin is about to write apsEnvironment: X into the main app's entitlements, and a hand-written appExtensions entry has aps-environment: Y (where X ≠ Y). Apple will sign the targets against different APNs environments, producing BadDeviceToken or silent NSE failures.
Troubleshooting
Disabled push notifications during credentials sync
If eas build prints ✔ Synced capabilities: Disabled: Push Notifications, EAS read your local entitlements file, didn't find aps-environment, and disabled push on the corresponding App ID. Press Ctrl+C immediately — continuing rebuilds a broken profile.
The 1.2.9 plugin should never produce this output, because it writes aps-environment into both the main app and NSE entitlements. If you see it:
- Confirm your installed plugin version:
npm list xtremepush-expo-plugin. Should be 1.2.9 or later. - Confirm
extra.eas.projectIdis set on your Expo config. - Run
cat ios/<NSE>/<NSE>.entitlementsafternpx expo prebuild --clean. The file should contain both<key>aps-environment</key>and the App Group. - If the file is missing
aps-environment, file an issue with the output ofnpx expo config --type public 2>&1 | grep -A 30 appExtensions.
After fixing the entitlements file, re-enable Push Notifications on the affected App ID in Apple Developer Portal and rerun eas build --clear-cache.
BadDeviceToken on every campaign
The .ipa's APNs environment doesn't match the dashboard's APNs credential. Run:
npx xtremepush-verify-build path/to/build.ipaThe summary line tells you which environment your build produces tokens in. The XtremePush dashboard must have a credential for that environment:
aps-environment: production→ production.p12(Sandbox & Production type) or universal.p8.aps-environment: development→ sandbox.p12or universal.p8.
A universal .p8 Auth Key serves both, so prefer that.
Campaigns stuck at "Sent" (never reach "Delivered")
enableDeliveryReceipts is off, or the NSE is missing the Push Notifications entitlement. Run xtremepush-verify-build to confirm the NSE's aps-environment line is ✓. If it's ✗, the NSE App ID doesn't have Push Notifications enabled.
Watch the device log for delivery-receipt = 1 and the actionHit HTTP call (see Delivery receipts).
Push works but rich media images don't render
The NSE doesn't have the App Group entitlement at signing time. Run the verifier — the App Groups line on the NSE row should match the main app's. If they don't match, the App Group isn't enabled on the NSE App ID; fix in Apple Developer Portal and rebuild with --clear-cache.
Permission prompt never appears
You set enablePushPermissions: false and didn't call requestNotificationPermissions() from JS. Either flip the flag back to true (which restores the launch-time prompt) or invoke the JS function from your app at the right moment.
Provisioning profile ... doesn't include the ... entitlement at build time
Existing profiles predate a capability change. Run:
eas credentials -p ios
# Provisioning Profile → Remove (for both targets)Then eas build --platform ios --clear-cache.
XP_E001 iosAppGroupIdentifier must end with .xtremepush.suit
Hardcoded App Group without the required suffix. Either change iosAppGroupIdentifier to end with .xtremepush.suit, or omit it entirely to use the auto-derived value.
Stale AppDelegate.swift after toggling flags
Always use npx expo prebuild --clean (the --clean flag is non-negotiable for push-related changes). Incremental prebuild occasionally leaves outdated AppDelegate content.
Android File google-services.json is missing on EAS
The file is in android/app/ but .gitignore excludes /android, so EAS doesn't upload it. Move the file to the project root and add:
"android": {
"googleServicesFile": "./google-services.json"
}See google-services.json placement.
Android SSLHandshakeException: checkServerTrusted: Expected public key
serverExpectedPublicKey doesn't match what the server presents. Either disable pinning while testing (enablePinning: false controls iOS only — for Android you simply remove serverExpectedPublicKey), or extract the correct key from the server (see SSL certificate pinning) and update the value.
Inbox returns ERR_SDK_NOT_READY
The device hasn't completed registration yet. This is distinct from an empty inbox (which resolves with []). Retry after a short delay, or after onMessageResponse first fires. See Inbox — custom UI § Error handling.
Migration guides
Migrating from 1.2.8
Most projects don't need plugin-config changes. The exception:
devTeamis now required when an NSE will be created. If you didn't have it set, add it. Find it at developer.apple.com → Membership or in the output ofeas credentials -p ios.- Remove any hand-written
extra.eas.build.experimental.ios.appExtensionsblock inapp.jsonif it only existed for the XtremePush NSE — the plugin auto-injects it now. If you keep it, the plugin merges into your entry without overwriting. - Remove
apsEnvironment: 'development'unless you genuinely build with an EAS Development-type provisioning profile. The plugin auto-sets'production'on EAS.
After upgrading:
npm install [email protected]
npx expo prebuild --clean
cd ios && pod install && cd ..
eas build --platform ios --clear-cacheThe --clear-cache is important: EAS caches the previous (now-incorrect) provisioning profile, and a fresh credentials sync is needed to pick up the new entitlements state.
Migrating from 1.2.7
getInboxBadge() semantics changed in 1.2.8: the value is now the inbox-wide unread total (server-authoritative), not the unread count of the most recently fetched page. Apps that paginated through every page just to compute a global badge can stop and trust getInboxBadge() > 0.
Migrating from 1.2.6
InboxMessage.createTimestamp and expirationTimestamp are now Unix milliseconds on both platforms (previously seconds on Android, ms on iOS). Pass directly to new Date(...) — remove any * 1000 workaround.
Android inbox items now match the iOS shape: read content under item.response.message.*, action under item.response.action.*. Code that previously read item.title / item.alert / item.cid on Android must migrate.
Migrating from 1.2.5
getInboxMessages() now returns content under response.message and response.action instead of flat top-level fields. Migrate item.title / item.alert / item.message.* to item.response.message.*.
Migrating from 1.2.3 / 1.2.4
If you enable both enableRichMedia and enableDeliveryReceipts without specifying iosAppGroupIdentifier, the auto-derived App Group identifier is now group.<bundleId>.xtremepush.suit (the .suit suffix is mandated by the iOS SDK for delivery receipts). Register the new App Group in Apple Developer Portal and re-provision both targets.
Changelog
[1.2.9]
Fixed
- iOS / EAS: NSE provisioning profile lacked the
aps-environmententitlement under EAS managed credentials. The plugin now writesaps-environmentinto the NSE's.entitlements, so EAS keeps Push Notifications enabled at credential-sync time. ResolvesProvisioning profile doesn't include the aps-environment entitlementat Xcode signing.
Added
- EAS
appExtensionsblock — auto-injected at prebuild when an NSE is created on an EAS project. Non-destructive: existing user-defined fields are preserved, entries for unrelated targets are untouched. apsEnvironmentauto-derived to'production'for EAS+NSE builds.devTeamvalidation (XP_E003) — now blocks prebuild when an NSE is created withoutdevTeam.XP_W006— informational warning whenapsEnvironmentand a hand-writtenaps-environmentinappExtensionsdisagree.
Changed
XP_W004retuned: fires only forapsEnvironment: 'development'on EAS projects with an NSE.XP_W005retuned: fires only for genuine user-entered mistakes after auto-injection runs.- Android:
getInboxBadge()always returned0. The bridge now reads the SDK-authoritative counter (PushConnector.getInboxBadge()) , which is reconciled by push receipts, foreground refresh, and theInboxBadgeUpdateListenerregistered unconditionally inMainApplication. Pair this withenableInboxBadge: true(default) to receive badge updates. - Android: R8 / ProGuard warnings for optional dependencies. Added
-dontwarn org.altbeacon.beacon.**(Beacon Services) and-dontwarn com.google.android.gms.ads.**(Play Services Ads) toxtremepush-proguard-rules.pro. Release builds no longer surface noisy warnings when these compile-only dependencies are absent.
[1.2.8]
Fixed
- Android:
getInboxBadge()always returned 0. Plugin now injectssetInboxEnabled(true)intoMainApplication. - Android:
getInboxData(limit, offset)returned the full inbox regardless of arguments. Pagination now works as documented. - iOS:
getInboxBadge()andonInboxBadgeUpdatereflected only the latest fetched page. Badge cache is now driven exclusively by SDK-authoritative sources (XPushInboxBadgeChangeNotificationon iOS,InboxBadgeUpdateListeneron Android), andgetInboxMessages()triggers an on-demand SDK refresh. - iOS:
reportMessageOpened()/reportMessageClicked()failed for messages outside the latest page. iOS reporting now uses the public string-ID API (reportInboxMessageOpened:actionIdentifier:). - iOS:
setUser('')/setExternalId('')were silently ignored. Empty values,null, andundefinednow flow through and reset the user identifier (parity with Android). - iOS: NSE pinning cert was never bound to the NSE target. A single
cert.deris now shared between main app and NSE via twoPBXBuildFileentries pointing at onePBXFileReference. Legacy duplicate atios/<NSE>/cert.deris auto-cleaned on prebuild.
Behaviour changes
getInboxBadge()returns the inbox-wide unread total, not the unread count of the most recently fetched page. Apps can stop paginating to answer "any unread?".onInboxBadgeUpdatefires reliably on every server-confirmed badge change (didn't fire at all on Android in 1.2.7 due to missingsetInboxEnabled(true)).
[1.2.7]
Fixed
- Inbox timestamps rendering as 1970 dates. Both bridges now multiply SDK seconds by 1000; documented unit is now milliseconds.
- Android inbox shape didn't match iOS 1.2.6. Android now builds the same nested
response: { message, action }object. getInboxBadge()/onInboxBadgeUpdatealways returning 0. Badge derived from inbox list, with mutations applied by deletes and SDK push-driven listeners.- iOS:
enablePushPermissions: falsecould leave a stale launch-time prompt. AppDelegate mod now reconciles on every prebuild. - Android:
POST_NOTIFICATIONSmanifest entry not auto-injected. Now added whenenablePushPermissions: true.
[1.2.6]
Fixed
- iOS:
getInboxMessagesreturning items with no content. Rewrote the parser to extract each documentedXPMessageproperty via explicit KVC reads, each wrapped in@try/@catch. - iOS:
getInboxMessagessilently resolving[]when the SDK is not ready. Now rejects withERR_SDK_NOT_READY. - Plugin:
...propsspread order caused normalised iOS defaults to be overwritten.
Breaking change: getInboxMessages return shape. Code reading item.title, item.alert, item.cid, or item.message must migrate to item.response.message.title, item.response.message.text, item.response.message.campaignIdentifier, and item.response.message.
[1.2.5]
Fixed
- iOS:
reportMessageOpened/reportMessageClickedalways rejecting withERR_NO_MESSAGE. - iOS:
getInboxBadgealways returns 0 /onInboxBadgeUpdatenever fires. Badge now proactively emitted after every fetch and delete. - iOS:
XPush.enableEncryptedPush(true)Swift compile error. Changed injection toXPush.enableEncryptedPush()(no argument). - iOS: delivery receipts — missing
application(_:didReceiveRemoteNotification:fetchCompletionHandler:)AppDelegate bridge. Now injected (Swift + Obj-C, idempotent).
[1.2.4]
Added
enableDeliveryReceipts(both platforms),deliveryReceiptsEndpoint(both),enableEncryptedPush(iOS),enableEncryptedMessages(Android),iosAppGroupIdentifier,nseTargetName,iosSdkVersion.- iOS NSE auto-creation when
enableDeliveryReceipts: true, with theinit()body callingsetDeliveryReceiptsEnabled→setAppKey→enableAppGroupsin the SDK-mandated order.
Behaviour change: with both enableRichMedia: true and enableDeliveryReceipts: true and no explicit iosAppGroupIdentifier, the auto-derived App Group changes from group.<bundleId>.xtremepush to group.<bundleId>.xtremepush.suit (the .suit suffix is mandated by the iOS SDK for delivery receipts).
[1.2.3] — 2026-03-30
Fixed
- iOS prebuild crash when
enablePinning: true— replacedaddResourceFilewithIOSConfig.XcodeUtils.ensureGroupRecursively+addResourceFileToGroup. - iOS certificate not added to Xcode Build Phases.
Removed
autoRegisterPushconfig option (deferred to 1.3.0). UseenablePushPermissions: false+requestNotificationPermissions()from JS.
[1.2.2]
- Added
androidDependencyandandroidDependencyVersionfor Huawei vs. Google Play build variants. - iOS and Android initialisation improvements.
License
MIT.
