react-native-mehery-event-sender
React Native SDK for push notifications, in-app notifications, interactive polls, and real-time event tracking.
Overview
The Mehery Event Sender SDK helps you engage users with rich push notifications, in-app polls, and precise journey tracking. Use it to register devices, authenticate against PushApp, sync user profiles, and deliver interactive feedback overlays.
Key features
- Advanced push notifications with rich media, carousels, and action buttons
- Interactive polls via overlay, inline, and tooltip containers
- Event tracking for login, page open, app lifecycle, and custom business events
- User lifecycle management: link/unlink devices and sync profile segments
- Background-optimized notification handling on Android and iOS
Prerequisites checklist
Important: Your app will not receive notifications correctly if these steps are skipped.
Before integrating, ensure the consumer app has:
- Firebase config files:
- Android:
android/app/google-services.json - iOS:
ios/GoogleService-Info.plist
- Android:
- Google Services Gradle plugin enabled (
com.google.gms.google-services) - Peer dependencies:
@react-native-firebase/app,@react-native-firebase/messaging,@react-native-async-storage/async-storage,react-native-push-notification - Notification and network permissions in Manifest / Info.plist
- iOS Background Modes:
remote-notification - App credentials: prod, sandbox, and development pairs in Info.plist (iOS) and
strings.xml(Android) - On every cold start:
readCredentialsForEnvironment(environment)→pushappAuth(xApiId, xApiKey)beforeinitSdk PollOverlayProvidermounted once at the app root
Installation
npm install react-native-mehery-event-sender
npm install @react-native-firebase/app @react-native-firebase/messaging
npm install @react-native-async-storage/async-storage react-native-push-notification
For iOS:
cd ios && pod install
Platform configuration
Android
Gradle
Project-level android/build.gradle:
buildscript {
dependencies {
classpath 'com.google.gms:google-services:4.3.15'
}
}
App-level android/app/build.gradle:
apply plugin: 'com.google.gms.google-services'
Manifest permissions
In AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
One Firebase messaging service (required for background rich push)
React Native Firebase registers io.invertase.firebase.messaging.ReactNativeFirebaseMessagingService with a no-op onMessageReceived. This SDK provides com.meheryeventsender.MyFirebaseMessagingService (a subclass) for BigPicture, custom layouts, and CTA actions.
If both services remain in the merged manifest, background delivery can hit the wrong service and images and action buttons will not show.
Add xmlns:tools on the root <manifest> if needed, then inside <application> remove the default RNFB service:
<service
android:name="io.invertase.firebase.messaging.ReactNativeFirebaseMessagingService"
android:exported="false"
tools:node="remove" />
Native FCM logs (logcat)
The SDK logs FCM handling at INFO from MyFirebaseMessagingService (not Debug), so default filters that hide Log.d still show them.
# Quote '*:S' on zsh (otherwise the shell treats * as a glob and errors).
adb logcat '*:S' 'MyFirebaseMessagingService:I'
Simpler (also safe in zsh):
adb logcat -s MyFirebaseMessagingService:I
If you see no lines tagged MyFirebaseMessagingService when a push arrives in the background, onMessageReceived did not run. Common causes:
- A top-level FCM
notificationpayload while the app is backgrounded (the OS may show a stock tray notification and skip your service) - Missing
tools:node="remove"so the wrongFirebaseMessagingServicehandles the message
Use data-only, high-priority FCM for Android and confirm the manifest step above. Look for Mehery FCM: onNewToken after install or token refresh to confirm this service class is active.
iOS
Capabilities and Info.plist
Enable Push Notifications and Background Modes (Remote notifications) in Xcode for your app target. Ensure Info.plist includes:
<key>UIBackgroundModes</key>
<array>
<string>remote-notification</string>
</array>
App credentials
Store three PushApp credential pairs in native config — one per initSdk environment. Use the same environment value for readCredentialsForEnvironment, pushappAuth, and initSdk so API host and auth always match.
initSdk env | Host | iOS keys | Android strings |
|---|---|---|---|
false | pushapp.ai | MeheryProdAppId / MeheryProdAppKey | mehery_prod_app_id / mehery_prod_app_key |
true | pushapp.net.in | MeherySandboxAppId / MeherySandboxAppKey | mehery_sandbox_app_id / mehery_sandbox_app_key |
'development' | pushapp.co.in | MeheryDevAppId / MeheryDevAppKey | mehery_dev_app_id / mehery_dev_app_key |
Legacy single-key names (MeheryAppId / MeheryAppSecretKey, mehery_app_id / mehery_app_secret_key) still work as a production fallback only.
iOS — add to your app target Info.plist:
<key>MeheryProdAppId</key>
<string>pa_your_prod_app_id</string>
<key>MeheryProdAppKey</key>
<string>pas_your_prod_app_key</string>
<key>MeherySandboxAppId</key>
<string>pa_your_sandbox_app_id</string>
<key>MeherySandboxAppKey</key>
<string>pas_your_sandbox_app_key</string>
<key>MeheryDevAppId</key>
<string>pa_your_dev_app_id</string>
<key>MeheryDevAppKey</key>
<string>pas_your_dev_app_key</string>
Android — add to android/app/src/main/res/values/strings.xml:
<string name="mehery_prod_app_id" translatable="false">pa_your_prod_app_id</string>
<string name="mehery_prod_app_key" translatable="false">pas_your_prod_app_key</string>
<string name="mehery_sandbox_app_id" translatable="false">pa_your_sandbox_app_id</string>
<string name="mehery_sandbox_app_key" translatable="false">pas_your_sandbox_app_key</string>
<string name="mehery_dev_app_id" translatable="false">pa_your_dev_app_id</string>
<string name="mehery_dev_app_key" translatable="false">pas_your_dev_app_key</string>
Rebuild the native app after changing these values.
Do not generate new API keys unless you must. Credentials live in native config (
Info.plist/strings.xml), not a remote config service. A newpa_/pas_pair from the dashboard requires updating the matching native values and shipping a new app release — existing installs keep the old credentials until users update. Prefer keeping your current keys; only rotate when required for security.
Match credentials to environment. If you pass sandbox credentials while
initSdk(..., false)points at production, requests hit the wrong host with the wrong app identity.
Authenticate and initialize
App authentication (pushappAuth)
On every cold start before initSdk, read credentials for your environment and pass them to pushappAuth. The SDK compares them with locally stored values and updates storage only when they change. Headers x-api-id and x-api-key are then included on every API request.
import {
pushappAuth,
readCredentialsForEnvironment,
type SdkInitEnvironmentParam,
} from 'react-native-mehery-event-sender';
const environment: SdkInitEnvironmentParam = false; // production
const { xApiId, xApiKey } = await readCredentialsForEnvironment(environment);
await pushappAuth(xApiId, xApiKey);
Optional manual override (for testing): await pushappAuth('pa_your_app_id', 'pas_your_app_key').
pushappAuth() with no arguments reads production credentials only (legacy / single-env apps).
SDK initialization (initSdk)
Initialize as early as possible (for example in App.tsx or index.js). Always await initSdk so device registration and WebSocket connect finish before you rely on SDK state.
| Param | Type | Default | Description |
|---|---|---|---|
context | any | — | Reserved; pass null |
identifier | string | — | Channel ID, format "<tenant>_<channel>" |
environment | boolean \| 'development' | true | Host: false = pushapp.ai, true = pushapp.net.in, 'development' = pushapp.co.in |
logs | boolean | true | Enable or disable SDK console logging |
import { useEffect } from 'react';
import {
pushappAuth,
initSdk,
readCredentialsForEnvironment,
type SdkInitEnvironmentParam,
} from 'react-native-mehery-event-sender';
const App = () => {
useEffect(() => {
const bootstrap = async () => {
const environment: SdkInitEnvironmentParam = false;
const { xApiId, xApiKey } =
await readCredentialsForEnvironment(environment);
await pushappAuth(xApiId, xApiKey);
// identifier format: "<tenant>_<channel>"
await initSdk(null, 'your_tenant_your_channel_id', environment, false);
// await initSdk(null, 'your_tenant_your_channel_id', true); // sandbox
// await initSdk(null, 'your_tenant_your_channel_id', 'development'); // development
};
bootstrap();
}, []);
return <MainNavigator />;
};
Environment host mapping:
false— production:pushapp.aitrue— sandbox:pushapp.net.in(default)'development'— development:pushapp.co.in
Logging (logs fourth argument):
true— SDK debug output is printed to the JS console (default)false— all SDK-ownedconsole.log/warn/errorcalls are suppressed
Native Android FCM logcat lines are unaffected (native code).
Optional startup helpers
Set geo context before init (used by register and event payloads):
import { setGeoIP } from 'react-native-mehery-event-sender';
setGeoIP({
ip: '203.0.113.1',
location: { lat: 19.076, lng: 72.8777 },
country: { iso_code: 'IN', name: 'India' },
});
Wait for init to complete before login if needed:
import { waitForSdkReady, OnUserLogin } from 'react-native-mehery-event-sender';
await waitForSdkReady();
await OnUserLogin('user_123');
Register the FCM background handler early in index.js when you need JS background message handling:
import { registerFcmBackgroundHandler } from 'react-native-mehery-event-sender';
registerFcmBackgroundHandler();
On Android 13+, request notification permission when appropriate:
import { ensureAndroidNotificationPermission } from 'react-native-mehery-event-sender';
await ensureAndroidNotificationPermission();
Mount polls
Mount PollOverlayProvider once at the root of your application for full-screen / popup polls.
import { PollOverlayProvider } from 'react-native-mehery-event-sender';
export default function App() {
return (
<>
<MainLayout />
<PollOverlayProvider />
</>
);
}
Optional in-app poll placeholders:
import {
InlinePollContainer,
TooltipPollContainer,
} from 'react-native-mehery-event-sender';
| Component | Description |
|---|---|
PollOverlayProvider | Global provider for full-screen or popup polls |
InlinePollContainer | Renders poll cards inside scrolling content |
TooltipPollContainer | Shows polls as tooltips relative to specific UI elements |
Track user events
Authentication events
import { OnUserLogin, OnUserLogOut } from 'react-native-mehery-event-sender';
// After successful sign-in or sign-up — map this device/session to your user ID
await OnUserLogin('unique_user_id_123');
// Before or after clearing local auth state on sign-out
await OnUserLogOut('unique_user_id_123');
User profile enrichment
Sync user metadata for targeted segments. The SDK keeps the last successfully pushed profile locally and only calls PUT /v1/customer/profile when additionalInfo or cohorts change. The snapshot is cleared on logout.
import { updateUserProfile } from 'react-native-mehery-event-sender';
await updateUserProfile(
{ name: 'Jane Doe', email: 'jane@example.com', city: 'Mumbai' },
{ segment: 'premium', plan: 'enterprise' }
);
Engagement and navigation
import {
OnPageOpen,
OnPageClose,
OnAppOpen,
OnAppLaunch,
sendCustomEvent,
} from 'react-native-mehery-event-sender';
// When a screen/page is shown (use your route or screen name)
OnPageOpen('dashboard_main');
// When leaving a page (no page name argument)
OnPageClose();
// App lifecycle helpers
OnAppOpen();
await OnAppLaunch();
// Custom business events — any keys your analytics needs
sendCustomEvent('purchase_completed', {
item_id: 'prod_99',
value: 49.99,
currency: 'USD',
campaign_id: 'spring_launch_2026',
});
If you already fire events from another SDK, keep the same event names and properties and swap the call to sendCustomEvent — minimal code change.
Braze
// Before
Braze.logCustomEvent('custom_event_with_properties', {
key1: 'value1',
key2: ['value2', 'value3'],
key3: false,
});
// After
import { sendCustomEvent } from 'react-native-mehery-event-sender';
sendCustomEvent('custom_event_with_properties', {
key1: 'value1',
key2: ['value2', 'value3'],
key3: false,
});
CleverTap
// Before
var props = { Name: 'XYZ', Price: 123 };
CleverTap.recordEvent('Product Viewed', props);
// After
import { sendCustomEvent } from 'react-native-mehery-event-sender';
const props = { Name: 'XYZ', Price: 123 };
sendCustomEvent('Product Viewed', props);
MoEngage
// Before
let properties = new MoEProperties();
properties.addAttribute('quantity', 1);
properties.addAttribute('product', 'iPhone');
properties.addAttribute('currency', 'dollar');
properties.addAttribute('price', 699);
properties.addAttribute('new_item', true);
properties.addAttribute('models', ['iPhone15', 'iPhone14']);
ReactMoE.trackEvent('Purchase', properties);
// After
import { sendCustomEvent } from 'react-native-mehery-event-sender';
sendCustomEvent('Purchase', {
quantity: 1,
product: 'iPhone',
currency: 'dollar',
price: 699,
new_item: true,
models: ['iPhone15', 'iPhone14'],
});
WebEngage
// Before
webengage.track(eventName, eventData);
// After
import { sendCustomEvent } from 'react-native-mehery-event-sender';
sendCustomEvent(eventName, eventData);
AppsFlyer
// Before
appsFlyer.logEvent('af_purchase', {
af_revenue: 49.99,
af_currency: 'USD',
af_content_id: 'prod_99',
});
// After
import { sendCustomEvent } from 'react-native-mehery-event-sender';
sendCustomEvent('af_purchase', {
af_revenue: 49.99,
af_currency: 'USD',
af_content_id: 'prod_99',
});
Adjust
// Before
Adjust.trackEvent(
new AdjustEvent('abc123') // or event token + callback params via your Adjust setup
);
// With partner/callback params, map those keys into a plain object:
// After
import { sendCustomEvent } from 'react-native-mehery-event-sender';
sendCustomEvent('purchase_completed', {
revenue: 49.99,
currency: 'USD',
product_id: 'prod_99',
});
Optional shared wrapper so existing call sites barely change:
import { sendCustomEvent } from 'react-native-mehery-event-sender';
export function track(eventName: string, eventData?: Record<string, unknown>) {
sendCustomEvent(eventName, eventData);
}
Public API reference
| API | Description |
|---|---|
pushappAuth(appId?, appSecretKey?) | Persist credentials locally; inject x-api-id / x-api-key on every API call |
readCredentialsForEnvironment(environment) | Read prod / sandbox / dev credentials from native config for the given initSdk environment |
readNativeAppCredentials() | Read production credentials only (legacy / single-env) |
initSdk(context, identifier, environment?, logs?) | Initialize SDK; identifier format "<tenant>_<channel>" |
waitForSdkReady() | Resolves when initSdk has finished |
setGeoIP(geo) | Set geo context for register / events |
OnUserLogin(userId) / OnUserLogOut(userId) | Link or delink device to host user |
OnPageOpen(name) / OnPageClose() | Screen lifecycle events |
OnAppOpen() / OnAppLaunch() | App lifecycle events |
sendCustomEvent(name, data?, options?) | Custom business events |
updateUserProfile(additionalInfo, cohorts?, options?) | Sync profile to PushApp |
updatePushToken(token) | Refresh FCM / APNs token with backend |
setDeviceMetadata(headers) | Merge extra string headers into every request |
getDeviceId() | Persistent SDK device UUID |
PollOverlayProvider, showPollOverlay, hidePollOverlay | Poll overlay UI |
InlinePollContainer, TooltipPollContainer | Inline / tooltip poll placeholders |
registerFcmBackgroundHandler() | Register background FCM handler early in index.js |
ensureAndroidNotificationPermission() | Request POST_NOTIFICATIONS on Android 13+ |
setNotificationUrlHandler, configureNotificationLinkRewrites, openNotificationLink | Notification deep-link helpers |
triggerCarouselNotification, triggerLiveActivity | Local test helpers for rich push / Live Activity |
Exported types: SdkInitEnvironmentParam, GeoIpInput, GeoIpPayload, OnUserLoginResult, SendCustomEventOptions, UpdateUserProfileOptions, UpdateUserProfileResult, TriggerCarouselNotificationParams, NotificationLinkRewrite, FcmBackgroundMessageListener.
Example app and iOS extensions
The example/ios project ships native targets you can adapt for rich notifications and Live Activities. Copy or adapt the Swift, plists, and assets into your own app; branding and UI design are yours — these paths only show where the hooks and data live.
| Area | Path |
|---|---|
| Rich notification UI (content extension) | example/ios/ImagePreviewExtension/NotificationViewController.swift (and MainInterface.storyboard, Assets.xcassets in the same folder) |
| Modify notification content before display (service extension) | example/ios/ImageServiceExtension/NotificationService.swift |
| Live Activity / delivery-style widget | example/ios/DeliveryActivity/ (Swift sources, Info.plist, Assets.xcassets) |
Mirror the same targets and capabilities in Xcode if you are not using the example workspace directly.
Notification payload notes
Android: FCM data-only for rich background notifications
Foreground and background use different code paths.
- Foreground: JavaScript (
messaging().onMessage) can show a local notification with big-picture image and CTA actions from the payload. - Background / not running: Only the native
MyFirebaseMessagingServicecan add BigPicture, custom layouts, andNotificationCompatactions.
On Android, Firebase will not call onMessageReceived for messages that use a top-level FCM notification block while the app is in the background. The system shows a default tray notification instead, without this SDK’s custom images or action buttons.
To get rich background notifications:
- Send data-only FCM messages: put title, body, image URL, and CTA fields as string values in the FCM
datamap. - Set high priority for the Android message (FCM HTTP v1: e.g.
android.priority: HIGH) so delivery is not deferred too long under Doze. - If iOS still needs a
notificationor APNs block, use per-platform FCM overrides (data-only for Android;notification+ APNs for iOS).
Do not rely on setBackgroundMessageHandler alone to draw rich Android notifications; it cannot make onMessageReceived run when the system does not deliver the message to your service.
How to verify: With the app backgrounded, send a data-only test push, then:
adb logcat -s MyFirebaseMessagingService:I
You should see Mehery FCM: onMessageReceived and FCM[raw] / FCM[merged] lines. With a notification block, the service often will not log in background, and the tray shows a basic system notification. That contrast explains “works in foreground, not in background.”
Android image keys
- Single image:
image,imageUrl,image_url - Carousel:
imageUrls,image_urls,carousel_images, orimage1,image2, …
Body tap opens a link (notification_url)
When the push payload includes notification_url (or notificationUrl), tapping the notification body (title/message area) opens that URL in the system browser. CTA action buttons still use their own URLs (cta_buttons / url1–url3 on Android; buttons array / action IDs on iOS). Without notification_url, body tap only launches the app.
The SDK resolves the body URL from the root payload and from Mehery template nests: style.notification_url, stringified style, templateData, and template.style / template.data (same shape as the template API style object).
| Platform | Payload field | Body tap handler |
|---|---|---|
| Android | FCM data.notification_url (or nested style / templateData) | Native NotificationCtaUrlActivity + foreground JS Linking.openURL |
| iOS | APNs / FCM data (root or nested style / templateData) | Example AppDelegate.swift urlForNotificationBody + JS Linking.openURL fallback |
Push server requirement:
/send-notificationmust includenotification_urlon the device payload (flattened in FCMdatais best). If it only exists in the templatestyleobject, the send step must still forward that object (or flatten it) so iOS/Android can read it on tap. Mapping onlybuttons→url1/title1/action1is not enough.
iOS Live Activity: The example app only starts a Live Activity when activity_id is set and the payload looks like a live template (message1–message3, progressPercent, live_activity=true, or a hero image URL). Simple templates with only activity_id no longer show an empty Live Activity before the banner.
Example payload after correct server mapping:
{
"title": "Hello",
"body": "World",
"notification_url": "https://example.com/article",
"url1": "https://mehery.com",
"title1": "Yes",
"action1": "PUSHAPP_YES"
}
iOS host apps: The npm package does not ship AppDelegate. Merge urlForNotificationBody and the default-action URL open from the example app into your target’s AppDelegate (see Example app and iOS extensions).
iOS action categories
To show three action buttons, send category THREE_BUTTON_CATEGORY in the APNs payload. Action IDs received in JS / native tap handling:
PUSHAPP_ACTION_1PUSHAPP_ACTION_2PUSHAPP_ACTION_3
ProGuard
-keep class com.mehery.pushapp.** { *; }
Support
Raise bugs or feature requests in GitHub Issues.