Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

xtremepush-expo-plugin

Package Overview
Dependencies
Maintainers
1
Versions
22
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

xtremepush-expo-plugin

XtremePush Expo Config Plugin

Source
npmnpm
Version
1.2.6
Version published
Weekly downloads
865
-52.89%
Maintainers
1
Weekly downloads
 
Created
Source

Xtremepush Expo Plugin

A config plugin for Expo applications that integrates XtremePush SDK functionality with full React Native module support for both iOS and Android platforms.

Table of Contents

Requirements

System Requirements

  • Node.js: 18.0 or higher
  • Expo SDK: 51.0 or higher (53.0+ recommended)
  • React Native: 0.73.0 or higher

iOS Requirements

  • iOS Deployment Target: 15.0 or higher
  • Xcode: 14.0 or higher
  • CocoaPods: Latest version
  • Apple Developer Account with Push Notification capability enabled

Android Requirements

  • Minimum SDK: 21 (Android 5.0)
  • Target SDK: 34 (Android 14)
  • Gradle: 8.0 or higher
  • Google Play Services: Required for FCM

Platform Support

PlatformMinimum VersionPush NotificationsIn-App MessagesLocation Services
iOS15.0+YesYesYes
Android5.0+ (API 21)YesYesYes

Installation

Step 1: Install the Plugin

# Using npm
npm install xtremepush-expo-plugin

# Using yarn
yarn add xtremepush-expo-plugin

Step 2: Configure Your App

Add the plugin to your app.json or app.config.js:

{
  "expo": {
    "name": "YourAppName",
    "plugins": [
      [
        "xtremepush-expo-plugin",
        {
          "applicationKey": "YOUR_XTREMEPUSH_APP_KEY",
          "googleSenderId": "YOUR_FCM_SENDER_ID"
        }
      ]
    ]
  }
}

Step 3: Prebuild Your Project

After adding the plugin configuration, rebuild your native projects:

# Clean rebuild (recommended for first-time setup)
npx expo prebuild --clean

# For iOS, install CocoaPods dependencies
cd ios && pod install && cd ..

Step 4: Run Your Application

# For iOS
npx expo run:ios

# For Android
npx expo run:android

Platform Setup

iOS Setup

1. Basic Configuration

The plugin automatically configures these capabilities in your iOS project:

  • Push Notifications (background mode)
  • Remote notification token registration via AppDelegate
  • XtremePush SDK initialisation in AppDelegate

2. App Groups Setup (Required for Rich Media)

For rich media push notifications with images and attachments, you need to configure App Groups:

A. In Apple Developer Console:

  • Log into your Apple Developer Account

  • Create App Group:

    • Click "Identifiers" > "+" > "App Groups"
    • Identifier: group.{your-bundle-id}.xtremepush
    • Description: "XtremePush Rich Media Notifications"
    • Example: group.com.yourcompany.yourapp.xtremepush
  • Enable App Groups for your App ID:

    • Click "Identifiers" > Select your App ID
    • Check "App Groups" capability
    • Click "Configure" > Select your created App Group
    • Save changes
  • Update Provisioning Profiles:

    • Regenerate and download updated provisioning profiles
    • Install them in Xcode or add to your CI/CD

Important: If your iOS build fails after enabling App Groups, you need to regenerate your provisioning profiles. This commonly occurs when App Groups are added to an existing App ID.

Solution:

npx expo prebuild --clean --platform ios
cd ios && pod install && cd ..
npx expo run:ios

B. Configuration in Your Plugin:

{
  "expo": {
    "plugins": [
      [
        "xtremepush-expo-plugin",
        {
          "applicationKey": "YOUR_APP_KEY",
          "googleSenderId": "YOUR_SENDER_ID",
          "enableRichMedia": true,
          "iosAppGroup": "group.com.yourcompany.yourapp.xtremepush"
        }
      ]
    ]
  }
}

C. EAS Build Configuration (if using EAS):

Add to your app.json for EAS builds:

{
  "expo": {
    "extra": {
      "eas": {
        "projectId": "your-project-id",
        "build": {
          "experimental": {
            "ios": {
              "appExtensions": [
                {
                  "targetName": "XtremePushNotificationServiceExtension",
                  "bundleIdentifier": "com.yourcompany.yourapp.XtremePushNotificationServiceExtension",
                  "entitlements": {
                    "com.apple.security.application-groups": [
                      "group.com.yourcompany.yourapp.xtremepush"
                    ]
                  }
                }
              ]
            }
          }
        }
      }
    }
  }
}

3. Verification Steps

After setup, verify your configuration:

# Check if Service Extension was created
ls ios/XtremePushNotificationServiceExtension/

# Verify Podfile includes XtremePush SDK
grep -r "Xtremepush-iOS-SDK" ios/Podfile

# Check App Groups in entitlements
grep -r "com.apple.security.application-groups" ios/

Android Setup

1. Firebase Configuration

  • Create Firebase Project:

  • Add Android App:

    • Package name must match your android.package in app.json
    • Download google-services.json
    • Place in your project root (not in android/ folder)
  • Get FCM Sender ID:

    • Project Settings > Cloud Messaging
    • Copy the Sender ID (a numeric string, e.g. 123456789012)
    • Use this as googleSenderId in plugin configuration

Note: The Sender ID is not the same as the Server Key. The Sender ID is the numeric project number shown in Firebase Cloud Messaging settings.

2. Permissions

The plugin automatically adds required permissions to AndroidManifest.xml when enableLocationServices is true (the default):

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />

If your app does not use location features, set enableLocationServices: false to avoid unnecessary permission declarations that may trigger App Store / Play Store review scrutiny.

3. ProGuard / R8

The plugin automatically injects ProGuard/R8 rules required by the XtremePush SDK into your release build. No manual configuration is needed. The rules are written to android/app/xtremepush-proguard-rules.pro and wired into the release buildType during prebuild.

Configuration

Configuration Options

OptionTypeRequiredDefaultDescription
applicationKeystringYes-Your XtremePush application key
googleSenderIdstringYes-FCM Sender ID (required for Android push)
iosAppKeystringNoapplicationKeyiOS-specific application key (overrides applicationKey for iOS)
androidAppKeystringNoapplicationKeyAndroid-specific application key (overrides applicationKey for Android)
enableDebugLogsbooleanNofalseEnable SDK debug logging
enableLocationServicesbooleanNotrueAdd location permissions to Android manifest and iOS usage descriptions
enablePushPermissionsbooleanNotrueAuto-request push notification permissions on app launch. Set to false to control when the prompt appears via requestNotificationPermissions() from JS.
enableInAppMessagingbooleanNotrueEnable in-app messaging
enableRichMediabooleanNofalseEnable iOS rich media push notifications (creates Notification Service Extension)
iosAppGroupstringNogroup.{bundleId}.xtremepushiOS App Group identifier for data sharing between app and extension
devTeamstringNo-Apple Developer Team ID for the Notification Service Extension
serverUrlstringNoDefault EU serverCustom XtremePush server URL
useUsServerbooleanNofalseUse US data center (sets server URL to https://sdk.us.xtremepush.com)
usServerUrlstringNohttps://sdk.us.xtremepush.comCustom US server URL (used when useUsServer is true)
enablePinningbooleanNofalseEnable SSL certificate pinning (iOS)
certificatePathstringNo-Path to SSL certificate file (.der) for certificate pinning
serverExpectedPublicKeystringNo-Server expected public key for SSL pinning on Android. Injected as .setServerExpectedPublicKey(...) in PushConnector.Builder.
apsEnvironment'development' or 'production'NoAuto (from provisioning profile)Override the APS environment entitlement. Leave unset to let Xcode determine the correct value from your provisioning profile (recommended).
androidDependencystringNoie.imobile.extremepush:XtremePush_lib:9.3.11Override the full Android XtremePush Gradle dependency string (e.g. for Huawei HMS builds)
androidDependencyVersionstringNo9.3.11Override just the version of the default Android dependency. Ignored if androidDependency is set.
messageResponseCallbackstringNoDisabledEvent name emitted when a user taps a push notification. Set to enable. See Callback Functions.
inboxBadgeCallbackstringNoDisabledEvent name emitted when the inbox badge count changes. Set to enable. See Callback Functions.
deeplinkCallbackstringNoDisabledEvent name emitted when a deeplink is received. Set to enable. See Callback Functions.

Full Configuration Example

// app.config.js
export default {
  expo: {
    plugins: [
      [
        "xtremepush-expo-plugin",
        {
          // Required
          "applicationKey": "YOUR_APP_KEY",
          "googleSenderId": "YOUR_FCM_SENDER_ID",

          // Platform-specific keys (optional)
          "iosAppKey": "IOS_SPECIFIC_KEY",
          "androidAppKey": "ANDROID_SPECIFIC_KEY",

          // Server configuration (optional)
          "useUsServer": true,

          // Features (optional)
          "enableDebugLogs": true,
          "enableLocationServices": false,
          "enablePushPermissions": true,
          "enableInAppMessaging": true,

          // Rich media (optional)
          "enableRichMedia": true,
          "iosAppGroup": "group.com.yourcompany.yourapp.xtremepush",
          "devTeam": "ABCDE12345",

          // Callbacks (optional)
          "messageResponseCallback": "onMessageResponse",
          "inboxBadgeCallback": "onInboxBadgeUpdate",
          "deeplinkCallback": "onDeeplinkReceived"
        }
      ]
    ]
  }
};

Server Configuration (US Region Support)

By default, the XtremePush SDK connects to the EU data center. If your account is hosted on the US data center, you need to configure the server URL.

// app.config.js
export default {
  expo: {
    plugins: [
      [
        "xtremepush-expo-plugin",
        {
          "applicationKey": "YOUR_APP_KEY",
          "googleSenderId": "YOUR_SENDER_ID",
          "useUsServer": true
        }
      ]
    ]
  }
};

Option 2: Custom Server URL

// app.config.js
export default {
  expo: {
    plugins: [
      [
        "xtremepush-expo-plugin",
        {
          "applicationKey": "YOUR_APP_KEY",
          "googleSenderId": "YOUR_SENDER_ID",
          "serverUrl": "https://sdk.us.xtremepush.com"
        }
      ]
    ]
  }
};

Important Notes:

  • The server URL is configured at build time via native code injection
  • After changing server configuration, run npx expo prebuild --clean
  • Both iOS and Android will use the same server URL
  • If both serverUrl and useUsServer are specified, serverUrl takes precedence

SSL Certificate Pinning

If your project is provisioned in the US region, you may need to implement certificate pinning for security.

iOS — Certificate File Pinning

  • Place the certificate file in your project:
your-project/
  assets/
    cert.der
  app.json
  package.json
  • Configure:
{
  "enablePinning": true,
  "certificatePath": "assets/cert.der"
}
  • Rebuild: npx expo prebuild --clean

The certificate is automatically copied to the iOS bundle and added to Xcode Build Phases (Copy Bundle Resources) during prebuild.

Android — Public Key Pinning

For Android, use the serverExpectedPublicKey option instead of a certificate file:

{
  "serverExpectedPublicKey": "PUBLIC KEY HERE"
}

This value is injected into the PushConnector.Builder as .setServerExpectedPublicKey(...).

Usage

Import the Module

// Import specific functions (recommended)
import {
  hitEvent,
  hitEventWithValue,
  hitTag,
  hitTagWithValue,
  setUser,
  setExternalId,
  openInbox,
  requestNotificationPermissions,
  getInitialNotification,
  isAvailable,
  onMessageResponse,
  onInboxBadgeUpdate,
  onDeeplinkReceived,
  getInboxMessages,
  deleteInboxMessage,
  getInboxBadge,
  reportMessageOpened,
  reportMessageClicked,
} from 'xtremepush-expo-plugin/plugins/xtremepush';

// Or import the default native module
import Xtremepush from 'xtremepush-expo-plugin/plugins/xtremepush';

Check Module Availability

import { isAvailable } from 'xtremepush-expo-plugin/plugins/xtremepush';

if (isAvailable()) {
  // Native module is loaded and ready
} else {
  // Native module not available (e.g. Expo Go, web, or missing prebuild)
}

Basic Integration

import { useEffect } from 'react';
import { setUser, hitEvent, requestNotificationPermissions } from 'xtremepush-expo-plugin/plugins/xtremepush';

export default function App() {
  useEffect(() => {
    // Set user identifier
    setUser('user@example.com');

    // Track app open event
    hitEvent('app_opened');
  }, []);

  return <YourApp />;
}

User Management

import { setUser, setExternalId } from 'xtremepush-expo-plugin/plugins/xtremepush';

// Set user ID (email, username, or unique ID)
setUser("user@example.com");

// Set external ID (e.g., your CRM ID) // LEGACY
setExternalId("CRM-12345");

Event Tracking

import { hitEvent, hitTag, hitTagWithValue } from 'xtremepush-expo-plugin/plugins/xtremepush';

// Track simple events
hitEvent("purchase_completed");
hitEvent("article_read");

// Track tags (user properties)
hitTag("premium_user");
hitTag("newsletter_subscriber");

// Track tags with values
hitTagWithValue("user_level", "gold");
hitTagWithValue("purchase_amount", "99.99");

Tracking Events with Values

Use hitEventWithValue to track an event name together with a value — for example, "the user made a purchase, and the amount was 49.99".

Parameters
ParameterTypeRequiredDescription
eventstringYesThe name of the event (e.g. "purchase_completed").
valuestringYesA value to attach to the event (e.g. "49.99"). Always pass as a string.
Examples
import { hitEventWithValue } from 'xtremepush-expo-plugin/plugins/xtremepush';

// User viewed a product in the "shoes" category
hitEventWithValue("product_viewed", "shoes");

// User completed a purchase worth 49.99
hitEventWithValue("purchase_completed", "49.99");

// Converting a numeric variable to string
const amount = 49.99;
hitEventWithValue("purchase_completed", String(amount));
Common Mistakes
  • Passing a number instead of a string for the value. The SDK expects both event and value to be strings. Always wrap numbers in quotes or use String():

    // Wrong
    hitEventWithValue("purchase", 49.99);
    
    // Correct
    hitEventWithValue("purchase", "49.99");
    
  • Passing an empty string as the event name. On iOS, the native SDK silently ignores calls where the event name is empty. Always use a non-empty string.

Tip: Choose short, consistent event names using snake_case (for example purchase_completed, article_read). This makes them easier to find and filter in the Xtremepush dashboard.

Push Notifications

import { requestNotificationPermissions, openInbox } from 'xtremepush-expo-plugin/plugins/xtremepush';

// Request notification permissions (shows the OS prompt)
requestNotificationPermissions();

// Open the built-in XtremePush inbox UI
openInbox();

Deferred Push Permission

If you want to control when the push permission prompt appears (e.g. after login), set enablePushPermissions: false in your plugin config and call requestNotificationPermissions() from JS at the appropriate time:

{
  "enablePushPermissions": false
}
// After user logs in:
import { requestNotificationPermissions } from 'xtremepush-expo-plugin/plugins/xtremepush';

requestNotificationPermissions();

Android note: On Android, the device subscribes to FCM during SDK init regardless of enablePushPermissions. On API 33+, requestNotificationPermissions() triggers the OS notification permission dialog. On older Android versions the permission is granted implicitly.

Initial Notification Handling

getInitialNotification() captures push notification payloads when your app is opened from a terminated state. The payload is cleared after the first read.

Basic Usage

import { useEffect } from 'react';
import { getInitialNotification } from 'xtremepush-expo-plugin/plugins/xtremepush';

export default function App() {
  useEffect(() => {
    const checkInitialNotification = async () => {
      try {
        const payload = await getInitialNotification();

        if (payload) {
          console.log('App opened from notification:', payload);

          if (payload.deeplink) {
            console.log('Navigate to:', payload.deeplink);
          }
        }
      } catch (error) {
        console.error('Error getting initial notification:', error);
      }
    };

    checkInitialNotification();
  }, []);

  return <YourApp />;
}

React Navigation Integration

import { useEffect, useState } from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import { getInitialNotification } from 'xtremepush-expo-plugin/plugins/xtremepush';

const Stack = createStackNavigator();

export default function App() {
  const [initialRoute, setInitialRoute] = useState('Home');
  const [initialParams, setInitialParams] = useState({});

  useEffect(() => {
    const handleInitialNotification = async () => {
      try {
        const payload = await getInitialNotification();

        if (payload && payload.deeplink) {
          const route = parseDeeplink(payload.deeplink);
          if (route) {
            setInitialRoute(route.screen);
            setInitialParams(route.params);
          }
        }
      } catch (error) {
        console.error('Error handling initial notification:', error);
      }
    };

    handleInitialNotification();
  }, []);

  return (
    <NavigationContainer>
      <Stack.Navigator initialRouteName={initialRoute}>
        <Stack.Screen name="Home" component={HomeScreen} />
        <Stack.Screen
          name="ProductDetail"
          component={ProductDetailScreen}
          initialParams={initialParams}
        />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

function parseDeeplink(deeplink: string) {
  try {
    const url = new URL(deeplink);
    const pathSegments = url.pathname.split('/').filter(Boolean);
    if (pathSegments.length >= 2) {
      const [section, id] = pathSegments;
      switch (section) {
        case 'product':
          return { screen: 'ProductDetail', params: { productId: id } };
        default:
          return null;
      }
    }
  } catch {
    return null;
  }
  return null;
}

Payload Structure

import type { XtremePushNotificationPayload } from 'xtremepush-expo-plugin';

// The type is also available via import for TypeScript users.
// The payload shape:
{
  id?: string;                    // Notification ID
  campaignId?: string;            // Campaign ID
  title?: string;                 // Notification title
  text?: string;                  // Notification message
  deeplink?: string;              // Deep link URL
  platform?: 'android' | 'ios';  // Platform identifier
  receivedAt?: number;            // Timestamp (ms)
  badge?: number;                 // Badge count (iOS only)
  data?: { [key: string]: any }; // Custom data payload
}

Callback Functions

Callbacks let your app react when a user taps a push notification, when the inbox badge count changes, or when a deeplink arrives.

Setting up callbacks is a two-step process:

  • Enable the callback in your plugin configuration — this tells the native SDK to start listening for the event.
  • Subscribe to the callback in your JavaScript/TypeScript code — this is where you write the function that runs when the event fires.

Important: If you skip step 1, the native SDK will not register the listener and your JavaScript function will never be called. Both steps are required.

Step 1: Enable Callbacks in Your Plugin Config

Add the callback names to your plugin configuration. The string you provide becomes the event name that your app listens for.

{
  "messageResponseCallback": "onMessageResponse",
  "inboxBadgeCallback": "onInboxBadgeUpdate",
  "deeplinkCallback": "onDeeplinkReceived"
}

After changing your plugin config, rebuild: npx expo prebuild --clean

Step 2: Subscribe to Callbacks in Your App

Each subscription function takes two arguments:

ParameterTypeDescription
eventNamestringThe same event name you used in your plugin config. Must match exactly.
handlerfunctionThe function that runs when the event fires.

Each function returns a subscription object with a .remove() method. Call .remove() when your component unmounts.

Message Response Callback

Fires when a user taps a push notification.

Event data:

FieldTypeDescription
messageobjectThe notification content (id, title, text, deeplink, campaignId, custom fields).
responseobjectAdditional response metadata (key-value string pairs).
import { useEffect } from 'react';
import { onMessageResponse } from 'xtremepush-expo-plugin/plugins/xtremepush';

export default function App() {
  useEffect(() => {
    const subscription = onMessageResponse("onMessageResponse", (event) => {
      console.log('User tapped notification:', event);

      if (event.message?.deeplink) {
        console.log('Navigating to:', event.message.deeplink);
      }
    });

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

  return <YourApp />;
}

Inbox Badge Callback

Fires when the inbox unread count changes.

Event data:

FieldTypeDescription
badgenumberCurrent number of unread inbox messages.
import { useEffect, useState } from 'react';
import { onInboxBadgeUpdate } from 'xtremepush-expo-plugin/plugins/xtremepush';

export default function InboxButton() {
  const [unreadCount, setUnreadCount] = useState(0);

  useEffect(() => {
    const subscription = onInboxBadgeUpdate("onInboxBadgeUpdate", (event) => {
      setUnreadCount(event.badge);
    });

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

  return (
    <View>
      <Text>Inbox {unreadCount > 0 ? `(${unreadCount})` : ''}</Text>
    </View>
  );
}

Fires when a deeplink is received while the app is running in the foreground.

Event data:

FieldTypeDescription
deeplinkstringThe deeplink URL.
import { useEffect } from 'react';
import { onDeeplinkReceived } from 'xtremepush-expo-plugin/plugins/xtremepush';

export default function DeeplinkHandler() {
  useEffect(() => {
    const subscription = onDeeplinkReceived("onDeeplinkReceived", (event) => {
      console.log('Deeplink received:', event.deeplink);
    });

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

  return null;
}

All Callbacks Together

import { useEffect, useState } from 'react';
import {
  onMessageResponse,
  onInboxBadgeUpdate,
  onDeeplinkReceived,
} from 'xtremepush-expo-plugin/plugins/xtremepush';

export default function XtremePushCallbackHandler({ navigation }: { navigation: any }) {
  const [inboxBadge, setInboxBadge] = useState(0);

  useEffect(() => {
    const messageSub = onMessageResponse("onMessageResponse", (event) => {
      if (event.message?.deeplink) {
        navigation.navigate('Webview', { url: event.message.deeplink });
      }
    });

    const badgeSub = onInboxBadgeUpdate("onInboxBadgeUpdate", (event) => {
      setInboxBadge(event.badge);
    });

    const deeplinkSub = onDeeplinkReceived("onDeeplinkReceived", (event) => {
      navigation.navigate('Webview', { url: event.deeplink });
    });

    return () => {
      messageSub.remove();
      badgeSub.remove();
      deeplinkSub.remove();
    };
  }, [navigation]);

  return null;
}

Custom Inbox

The plugin provides methods for building a custom inbox UI. These allow you to fetch, display, and manage inbox messages without using the built-in XtremePush inbox screen.

Message shape (v1.2.6+)

Each message returned by getInboxMessages() has the following shape. Message content and tap action are nested under response to mirror the native SDK's XPMessageResponse / Android Message.getResponse() structure, so both platforms return the same object shape.

interface InboxMessage {
  identifier: string;           
  isOpened: boolean;
  isClicked: boolean;
  isDelivered: boolean;           
  createTimestamp: number;        // Unix seconds
  expirationTimestamp: number | null;
  style: Record<string, any>;     // Background colours and title styling
  isCard: boolean;

  response: {
    message: {
      identifier?: string;
      campaignIdentifier?: string;
      title?: string;
      text?: string;               
      icon?: string;               // URL to an icon asset
      data?: Record<string, any>;  // Custom data payload defined in the campaign
      payload?: Record<string, any>;  // Full raw push payload
    };
    action: {
      deeplink?: string;
      url?: string;
      identifier?: string;         // Action identifier — pass to reportMessageClicked / Opened
    };
  };
}

Breaking change (v1.2.6): Prior versions exposed title, alert, cid, and a flat message field at the top level of each item. These were removed because the underlying native parsing was unreliable and frequently returned undefined. All message content is now under response.message; the tap action is under response.action. Read the Changelog entry for v1.2.6 before upgrading.

Important: You must call getInboxMessages() before using reportMessageOpened, or reportMessageClicked — those methods operate on a cached list from the most recent fetch. The cache is cleared on every getInboxMessages() call, so always call getInboxMessages() immediately before acting on a message.

Step-by-step: retrieve and display inbox messages

  • Fetch the list. Call getInboxMessages(limit, offset) — both arguments are clamped to sensible bounds (limit ≥ 1, offset ≥ 0) by the JS wrapper.
    import { getInboxMessages } from 'xtremepush-expo-plugin/plugins/xtremepush';
    
    const messages = await getInboxMessages(20, 0);
    
  • Handle the two error cases separately. getInboxMessages() rejects with one of two codes:
    • ERR_INBOX_FETCH — the SDK surfaced an error (network, auth, etc.). Retry with backoff.
    • ERR_SDK_NOT_READY — the SDK returned a nil list because the device is not registered yet. This is distinct from an empty inbox (which resolves with []). Retry after registration completes, e.g. after onMessageResponse fires or after a short delay on first launch.
    try {
      const messages = await getInboxMessages(20, 0);
      setMessages(messages); // Empty array means inbox is genuinely empty
    } catch (e) {
      if (e.code === 'ERR_SDK_NOT_READY') {
        // Device not yet registered — retry shortly
        setTimeout(loadInbox, 2000);
      } else {
        console.error('Inbox fetch failed:', e);
      }
    }
    
  • Render content from the response object. Read title, body, icon, and custom data from item.response.message:
    <FlatList
      data={messages}
      keyExtractor={(item) => item.identifier}
      renderItem={({ item }) => (
        <View>
          <Text style={{ fontWeight: item.isOpened ? 'normal' : 'bold' }}>
            {item.response.message.title ?? 'No title'}
          </Text>
          <Text>{item.response.message.text ?? ''}</Text>
          {item.response.message.icon && (
            <Image source={{ uri: item.response.message.icon }} style={{ width: 48, height: 48 }} />
          )}
        </View>
      )}
    />
    
  • React to a tap by reporting it. Call reportMessageClicked(identifier) when the user taps a row. This marks the message as both clicked and opened on the server — do not also call reportMessageOpened for the same interaction. Use reportMessageOpened(identifier) only for "message became visible in the list" telemetry.
    const onTap = async (item: InboxMessage) => {
      await reportMessageClicked(item.identifier, item.response.action.identifier);
      if (item.response.action.deeplink) {
        navigation.navigate(item.response.action.deeplink);
      }
    };
    
  • Keep the badge in sync. getInboxMessages() and deleteInboxMessage() both emit the badge count via onInboxBadgeUpdate after completing, so you only need to wire up the subscription once — no polling required.
    onInboxBadgeUpdate('onInboxBadgeUpdate', ({ badge }) => setBadge(badge));
    

Get Badge Count

import { getInboxBadge } from 'xtremepush-expo-plugin/plugins/xtremepush';

const unreadCount = await getInboxBadge();

Report Message Opened

import { reportMessageOpened } from 'xtremepush-expo-plugin/plugins/xtremepush';

await reportMessageOpened(message.identifier);
// Optionally pass an action identifier:
await reportMessageOpened(message.identifier, "view");

Report Message Clicked

import { reportMessageClicked } from 'xtremepush-expo-plugin/plugins/xtremepush';

// Also marks as opened automatically — do not call reportMessageOpened separately.
await reportMessageClicked(message.identifier);

Full Custom Inbox Example

import { useEffect, useState } from 'react';
import { View, Text, FlatList, TouchableOpacity } from 'react-native';
import {
  getInboxMessages,
  getInboxBadge,
  reportMessageClicked,
  deleteInboxMessage,
  onInboxBadgeUpdate,
} from 'xtremepush-expo-plugin/plugins/xtremepush';
import type { InboxMessage } from 'xtremepush-expo-plugin';

export default function CustomInbox() {
  const [messages, setMessages] = useState<InboxMessage[]>([]);
  const [badge, setBadge] = useState(0);

  const loadInbox = async () => {
    try {
      const items = await getInboxMessages(50, 0);
      setMessages(items);
    } catch (e: any) {
      if (e.code === 'ERR_SDK_NOT_READY') {
        // Device not registered yet — retry shortly
        setTimeout(loadInbox, 2000);
      } else {
        console.error('Failed to load inbox:', e);
      }
    }
  };

  useEffect(() => {
    loadInbox();
    // Badge updates are emitted after every inbox fetch and delete,
    // so the subscription stays in sync without polling.
    const sub = onInboxBadgeUpdate('onInboxBadgeUpdate', ({ badge }) => setBadge(badge));
    return () => sub.remove();
  }, []);

  const handlePress = async (msg: InboxMessage) => {
    await reportMessageClicked(msg.identifier, msg.response.action.identifier);
    loadInbox(); // Refresh to update read state
  };

  const handleDelete = async (msg: InboxMessage) => {
    await deleteInboxMessage(msg.identifier);
    loadInbox();
  };

  return (
    <View>
      <Text>Inbox ({badge} unread)</Text>
      <FlatList
        data={messages}
        keyExtractor={(item) => item.identifier}
        renderItem={({ item }) => (
          <TouchableOpacity onPress={() => handlePress(item)}>
            <Text style={{ fontWeight: item.isOpened ? 'normal' : 'bold' }}>
              {item.response.message.title ?? 'No title'}
            </Text>
            <Text>{item.response.message.text ?? ''}</Text>
            <TouchableOpacity onPress={() => handleDelete(item)}>
              <Text>Delete</Text>
            </TouchableOpacity>
          </TouchableOpacity>
        )}
      />
    </View>
  );
}

API Reference

Functions

FunctionParametersReturnsDescription
hitEvent(event: string)voidTrack a named event
hitEventWithValue(event: string, value: string)voidTrack an event with a value
hitTag(tag: string)voidHit a tag for user segmentation
hitTagWithValue(tag: string, value: string)voidHit a tag with a value
setUser(user: string)voidSet the user identifier
setExternalId(id: string)voidSet an external user ID
requestNotificationPermissions()voidRequest push notification permissions
openInbox()voidOpen the built-in inbox UI
getInitialNotification()Promise<object | null>Get the notification payload that launched the app
isAvailable()booleanCheck if the native module is loaded
getInboxMessages(limit: number, offset: number)Promise<InboxMessage[]>Fetch paginated inbox messages
getInboxBadge()Promise<number>Get the current unread count
deleteInboxMessage(messageId: string)Promise<number | void>Delete an inbox message
reportMessageOpened(messageId: string, actionIdentifier?: string)Promise<void>Report a message as viewed
reportMessageClicked(messageId: string, actionIdentifier?: string)Promise<void>Report a message as clicked (also marks as opened)
onMessageResponse(eventName: string, handler: Function){ remove: () => void }Subscribe to notification tap events
onInboxBadgeUpdate(eventName: string, handler: Function){ remove: () => void }Subscribe to inbox badge changes
onDeeplinkReceived(eventName: string, handler: Function){ remove: () => void }Subscribe to deeplink events

Types

All types are importable from the package root:

import type {
  XtremePushPluginConfig,
  XtremePushNotificationPayload,
  XtremePushNativeModule,
  XtremePushMessageResponseEvent,
  XtremePushInboxBadgeEvent,
  XtremePushDeeplinkEvent,
  XtremePushSubscription,
  InboxMessage,
} from 'xtremepush-expo-plugin';

Troubleshooting

iOS Common Issues

  • Wrong Environment: Ensure using Development cert for testing, Production for App Store
  • Expired Certificates: Certificates are valid for 1 year, renew before expiration
  • Bundle ID Mismatch: Certificate must match app's Bundle ID exactly
  • App Groups Build Failure: Regenerate provisioning profiles after enabling App Groups

Android Common Issues

  • Package Name Mismatch: Firebase package name must match your app exactly
  • Missing google-services.json: File must be in project root
  • Push not working: Verify googleSenderId is the numeric Sender ID from Firebase, not the Server Key

General

  • After any config change, always run npx expo prebuild --clean
  • Verify native module is available with isAvailable() before calling SDK methods
  • The plugin does not work in Expo Go — you must use a development build (npx expo run:ios / npx expo run:android)

Delivery Receipts (v1.2.4+)

Push delivery receipts tell the XtremePush platform when a notification is actually delivered to a device, not just sent. Requires iOS SDK ≥ 4.1.8 and Android SDK ≥ 7.9.0.

Basic setup (both platforms)

{
  "plugins": [
    ["xtremepush-expo-plugin", {
      "applicationKey": "YOUR_KEY",
      "googleSenderId": "YOUR_SENDER_ID",
      "enableDeliveryReceipts": true
    }]
  ]
}

When enableDeliveryReceipts is true:

  • iOS: XPush.setDeliveryReceiptsEnabled(true), XPush.enableAppGroups(...), and (v1.2.5+) application(_:didReceiveRemoteNotification:fetchCompletionHandler:) are injected into AppDelegate. The Notification Service Extension is created automatically and its init() is configured per the Xtremepush iOS Enterprise Push docs.
  • Android: .setDeliveryReceiptsEnabled(true) is added to the PushConnector.Builder chain in MainApplication.

Step-by-step: configure delivery receipts on iOS

Follow every step. The most common cause of campaigns stuck at Sent is skipping the Apple Developer Portal provisioning steps — the receipt HTTP call silently fails if the App Group is not actually wired up on the real profile.

  • Enable the option in your plugin config. Add enableDeliveryReceipts: true. If you want receipts delivered to your own server, also set deliveryReceiptsEndpoint. Leave iosAppGroupIdentifier unset to accept the auto-derived value group.<bundleId>.xtremepush.suit.
    {
      "applicationKey": "YOUR_KEY",
      "googleSenderId": "YOUR_SENDER_ID",
      "enableDeliveryReceipts": true
    }
    
  • Register the App Group in Apple Developer Portal. Go to Certificates, Identifiers & Profiles → Identifiers → App Groups and create group.<your.bundle.id>.xtremepush.suit (the .suit suffix is mandatory per the XtremePush SDK). The plugin throws XP_E001 error at prebuild time if the identifier you provide does not end with .xtremepush.suit.
  • Enable the App Group capability on both App IDs. Under Identifiers, open your main app's App ID and the NSE's App ID (<bundleId>.XtremePushNotificationServiceExtension). Add the App Groups capability on both and tick the group you just created. Both IDs must reference the same group.
  • Regenerate both provisioning profiles. Existing profiles predate the new capability and will fail code-signing with cryptic errors like Provisioning profile ... doesn't include the ... entitlement. Download the refreshed profiles to Xcode (or let EAS re-fetch them on the next build).
  • Run npx expo prebuild --clean. A full regeneration is required — incremental prebuild occasionally leaves stale AppDelegate.swift content. After prebuild, verify that the generated files contain these markers:
    • ios/<project>/AppDelegate.swift — three comment markers: // @xtremepush-expo-plugin:delivery-receipts, // @xtremepush-expo-plugin:app-groups, // @xtremepush-expo-plugin:delivery-receipts-receive
    • ios/<project>/AppDelegate.swift — a method application(_:didReceiveRemoteNotification:fetchCompletionHandler:) that forwards to XPush.applicationDidReceiveRemoteNotification(...) (this bridge is what makes the SDK post the delivery receipt; it was missing from v1.2.4 and added in v1.2.5)
    • ios/XtremePushNotificationServiceExtension/NotificationService.swift — an override init() block calling XPush.setDeliveryReceiptsEnabled(true), then XPush.setAppKey(...), then XPush.enableAppGroups(...) in that exact order
    • ios/<project>/<project>.entitlements and ios/XtremePushNotificationServiceExtension/XtremePushNotificationServiceExtension.entitlements — both must contain the same group under com.apple.security.application-groups
  • Verify on a real device. Simulators will not receive APNs pushes, so you must test on hardware. Build with npx expo run:ios --device, install, register for push, and send a test campaign. Watch the Xcode device log for these XPush log lines (taken from the investigation report):
    [XPush] - applicationDidRegisterForRemoteNotificationsWithDeviceToken: {...}
    [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 presence of delivery-receipt = 1 in the payload and a successful actionHit call confirm the NSE received the push and the SDK posted a receipt. The campaign will transition from Sent to Delivered in the dashboard within a few seconds.

Step-by-step: configure delivery receipts on Android

  • Enable the option. Add enableDeliveryReceipts: true. The plugin injects .setDeliveryReceiptsEnabled(true) into the PushConnector.Builder chain.
  • Check the Android SDK version. Delivery receipts require Android SDK ≥ 7.9.0. The plugin emits XP_W001 at prebuild time if your resolved dependency is older; upgrade via androidDependencyVersion: "7.9.0" or later, or pass a fully-qualified androidDependency string.
  • Prebuild and verify. After npx expo prebuild --clean, open android/app/src/main/.../MainApplication.* and confirm the builder chain contains .setDeliveryReceiptsEnabled(true) // @xtremepush-expo-plugin:delivery-receipts (or the two-argument form with your custom endpoint).

Custom reporting endpoint (both platforms)

If you want receipts sent to your own server instead of Xtremepush:

{
  "enableDeliveryReceipts": true,
  "deliveryReceiptsEndpoint": "https://receipts.your-server.com/endpoint"
}

iOS — App Group identifier

Delivery receipts require an App Group registered in Apple Developer Portal, shared between the main app and the Notification Service Extension.

Auto-derived (recommended): When enableDeliveryReceipts is true and iosAppGroupIdentifier is not set, the plugin auto-derives:

group.<your.bundle.id>.xtremepush.suit

Explicit override:

{
  "enableDeliveryReceipts": true,
  "iosAppGroupIdentifier": "group.com.yourcompany.yourapp.xtremepush.suit"
}

Important: The identifier must end with .xtremepush.suit (the .suit suffix is required by the Xtremepush iOS SDK). The plugin will throw a descriptive error (XP_E001) at prebuild time if the suffix is wrong.

You must register the App Group in Apple Developer Portal before building:

  • Go to Identifiers → App Groups → +
  • Create group.<your.bundle.id>.xtremepush.suit
  • Enable the App Group capability for both your main App ID and the NSE App ID
  • Re-generate provisioning profiles

iOS — Notification Service Extension

The NSE is created automatically when enableDeliveryReceipts is true. By default it is named XtremePushNotificationServiceExtension. To override:

{
  "enableDeliveryReceipts": true,
  "nseTargetName": "MyCustomNSETarget"
}

For EAS builds, add the extension to your app.json:

"extra": {
  "eas": {
    "build": {
      "experimental": {
        "ios": {
          "appExtensions": [{
            "targetName": "XtremePushNotificationServiceExtension",
            "bundleIdentifier": "com.yourcompany.yourapp.XtremePushNotificationServiceExtension",
            "entitlements": {
              "com.apple.security.application-groups": [
                "group.com.yourcompany.yourapp.xtremepush.suit"
              ]
            }
          }]
        }
      }
    }
  }
}

Encrypted Push / Messages (v1.2.4+)

iOS encrypted push

{
  "enableEncryptedPush": true
}

Injects XPush.enableEncryptedPush(true) into AppDelegate (Swift) or [XPush enableEncryptedPush] (Obj-C) during SDK initialisation.

Android encrypted messages

Requires Android SDK ≥ 8.1.0.

{
  "enableEncryptedMessages": true
}

Injects .setEncryptedMessagesEnabled(true) into the PushConnector.Builder chain.

iOS SDK Pod Version (v1.2.4+)

Pin the Xtremepush-iOS-SDK CocoaPods dependency to a specific version:

{
  "iosSdkVersion": "6.1"
}

This substitutes ~> <value> into the react-native-xtremepush.podspec. The default is 6.1, which matches v1.2.3 behaviour.

Migrating from 1.2.x

No changes are required if you do not use any of the new options. All new fields default to off and produce byte-identical prebuild output to v1.2.3.

One exception — combined enableRichMedia + enableDeliveryReceipts:

If you enable both enableRichMedia: true and enableDeliveryReceipts: true without specifying iosAppGroupIdentifier, the auto-derived App Group changes:

v1.2.3v1.2.4
group.<bundleId>.xtremepushgroup.<bundleId>.xtremepush.suit

You must:

  • Register group.<bundleId>.xtremepush.suit as a new App Group in Apple Developer Portal
  • Enable it for both your main App ID and the NSE App ID
  • Re-generate and install provisioning profiles

This change is required because the .suit suffix is mandated by the Xtremepush iOS SDK for delivery receipts.

License

This project is licensed under the MIT License.

Changelog

All notable changes to this project are documented here.

[1.2.6] — Latest

Fixed

iOS: getInboxMessages returning items with no content

  • Fix: Rewrote getInboxMessages to extract each documented XPMessage property (identifier, campaignIdentifier, title, text, icon, data, payload) and each XPAction property (deeplink, url, identifier) via explicit KVC reads, each wrapped in its own @try/@catch so a single malformed property no longer aborts the whole item. The extracted content is now returned under a nested response: { message, action } object that mirrors the native XPMessageResponse structure and matches the Android shape.

iOS: getInboxMessages silently resolving [] when the SDK is not ready

  • Fix: getInboxMessages now rejects with ERR_SDK_NOT_READY in this case. An empty inbox still resolves with [].

iOS: malformed inbox item crashing the entire list fetch

  • Fix: Per-item @try/@catch now logs and skips malformed items, so the healthy items are still returned.

Plugin: ...props spread order caused normalised iOS defaults to be overwritten

  • Fix: ...props is now spread first so the explicit normalised values take precedence. Normalisation is deterministic and safe to run after the spread.

Plugin: nseTargetName ignored during Podfile target-ordering check

  • Fix: The target name is resolved from pluginConfig.nseTargetName with a fallback to the default.

Upgrading from 1.2.5

Breaking change to getInboxMessages return shape. Code that read 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 respectively. The old top-level fields were unreliable in v1.2.5 and are no longer emitted. See the Custom Inbox → Message shape section earlier in this README for the full interface.

No plugin config changes required. Rebuild your iOS app after upgrading.

[1.2.5]

Fixed

iOS: inbox messages missing message field (client-reported)

  • Fix: First attempt at fixing the inbox parsing — corrected the top-level extraction path. This fix was incomplete and was superseded in v1.2.6, which rewrites the parser to read each documented property explicitly.

iOS: reportMessageOpened / reportMessageClicked always rejecting with ERR_NO_MESSAGE

  • Fix: Corrected the internal lookup so opened/clicked reporting works on iOS.

iOS: getInboxBadge always returns 0 / onInboxBadgeUpdate never fires (client-reported)

  • Fix: Badge count is now proactively emitted after every getInboxMessages call and after every deleteInboxMessage call (which returns the updated count directly from the SDK). This mirrors Android's listener-driven approach and ensures onInboxBadgeUpdate fires whenever the inbox state changes through the plugin's own operations.

iOS: XPush.enableEncryptedPush(true) causes Swift compile error (client-reported)

  • Fix: Swift injection changed to XPush.enableEncryptedPush() (no argument).

iOS: delivery receipts — missing didReceiveRemoteNotification:fetchCompletionHandler: AppDelegate bridge (client-reported)

  • Issue: The plugin did not inject application(_:didReceiveRemoteNotification:fetchCompletionHandler:) into AppDelegate. Without it the SDK never receives background notification events, no delivery receipt HTTP calls are made, and campaigns remain permanently at Sent in the dashboard.
  • Fix: When enableDeliveryReceipts: true, the plugin now injects this method into both Swift and Obj-C AppDelegates, forwarding to XPush.applicationDidReceiveRemoteNotification(_:fetchCompletionHandler:) before calling super. Injection is idempotent across expo prebuild runs.

Upgrading from 1.2.4

No plugin config changes required. All fixes are internal corrections to generated native code. Rebuild your iOS app after upgrading.

[1.2.4]

Fixed

Android: delivery receipts not configurable from plugin options (client-reported)

  • Issue: PushConnector.Builder did not include .setDeliveryReceiptsEnabled(...). Consumers were forced to patch generated native code manually.
  • Fix: New enableDeliveryReceipts option injects .setDeliveryReceiptsEnabled(true) (or with deliveryReceiptsEndpoint for a custom URL) into the Kotlin and Java MainApplication builder chain.
  • Minimum Android SDK: 7.9.0

Android: encrypted messages not configurable from plugin options (client-reported)

  • Issue: .setEncryptedMessagesEnabled(true) was absent from the builder chain.
  • Fix: New enableEncryptedMessages option injects it.
  • Minimum Android SDK: 8.1.0

iOS: delivery receipts not configurable from plugin options (client-reported)

  • Issue: XPush.setDeliveryReceiptsEnabled(...) and XPush.enableAppGroups(...) were not injected into AppDelegate. No App Group with the .xtremepush.suit suffix was created.
  • Fix: New enableDeliveryReceipts option injects both calls (Swift and Obj-C). App Group auto-derived as group.<bundleId>.xtremepush.suit; override via iosAppGroupIdentifier.
  • Minimum iOS SDK: 4.1.8

iOS: NSE not initialised for delivery receipts (client-reported)

  • Issue: NotificationService.swift had no init() method. The Xtremepush iOS SDK requires setDeliveryReceiptsEnabled, setAppKey, and enableAppGroups in strict order inside init().
  • Fix: When enableDeliveryReceipts: true the plugin generates the init() body with the correct call order. When false, output is structurally equivalent to v1.2.3.

iOS: encrypted push not configurable from plugin options (client-reported)

  • Issue: XPush.enableEncryptedPush(true) / [XPush enableEncryptedPush] was absent from AppDelegate injection.
  • Fix: New enableEncryptedPush option injects the call. Note: Obj-C form takes no argument per SDK docs.

iOS: pod version for Xtremepush-iOS-SDK now configurable

  • Issue: react-native-xtremepush.podspec pinned ~> 6.1 with no override.
  • Fix: New iosSdkVersion option (default: '6.1') substituted at prebuild time.

Added

OptionPlatformDescription
enableDeliveryReceiptsBothEnable push delivery receipts
deliveryReceiptsEndpointBothRoute receipts to a custom server instead of Xtremepush
enableEncryptedPushiOSInject XPush.enableEncryptedPush(true) in AppDelegate
enableEncryptedMessagesAndroidInject .setEncryptedMessagesEnabled(true) in builder chain
iosAppGroupIdentifieriOSExplicit App Group override (must end with .xtremepush.suit)
nseTargetNameiOSOverride the NSE Xcode target name
iosSdkVersioniOSPin the Xtremepush iOS SDK CocoaPod version

Upgrading from 1.2.3

No changes required unless opting in to new features. All new options default to off, producing byte-identical native output to v1.2.3.

One exception — if you enable both enableRichMedia: true and enableDeliveryReceipts: true without specifying iosAppGroupIdentifier, the auto-derived App Group changes from group.<bundleId>.xtremepush to group.<bundleId>.xtremepush.suit. You must register the new group in Apple Developer Portal and re-provision both targets.

[1.2.3] - 2026-03-30

Fixed

  • iOS: Prebuild crash (TypeError: Cannot read properties of null) when enablePinning: true — replaced addResourceFile with IOSConfig.XcodeUtils.ensureGroupRecursively + addResourceFileToGroup.
  • iOS: Certificate not added to Xcode Build Phases — addResourceFileToGroup with isBuildFile: true now registers the file in Copy Bundle Resources so Bundle.main.path() resolves at runtime.
  • iOS: Added fs.existsSync check before Xcode project modification to prevent dangling file references when the cert copy step fails.
  • iOS: POSIX-safe certificate path using template literals instead of path.join().

Removed

  • autoRegisterPush config option (deferred to 1.3.0). Use enablePushPermissions: false + requestNotificationPermissions() from JS.

[1.2.2]

  • Added androidDependency and androidDependencyVersion options for Huawei vs Google Play build variant support.
  • Various iOS and Android initialisation improvements.

Keywords

expo

FAQs

Package last updated on 30 Apr 2026

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts