npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@masivo/rn

v2.4.1

Published

Masivo SDK for React Native — push notifications and in-app messaging

Readme

@masivo/rn

Official Masivo SDK for React Native — event tracking, push notifications, and in-app messaging.

Important: This SDK requires a CLIENT API key. Do not use SERVER API keys — they will be rejected and the SDK will warn you about exposed server credentials. Create a CLIENT key in the Masivo dashboard.

No native dependencies. This package does not install any native modules. Push token handling is done by your existing push library (Expo Notifications, Firebase, etc.) — you simply pass the token to this SDK.

Installation

npm install @masivo/rn react-native-webview
pnpm add @masivo/rn react-native-webview
yarn add @masivo/rn react-native-webview

react-native-webview is required for custom HTML in-app messages.

Quick Start

import { createRNClient } from "@masivo/rn";

const masivo = createRNClient({
  apiKey: "your-client-api-key",
  brandId: "your-brand-id" // optional default brand
});

await masivo.ready();

if (masivo.getCapabilities()?.inappEnabled) {
  await masivo.inApp.fetchPendingMessages("customer-123", "brand-456");
}

// Track a standard analytics event
await masivo.sendAnalyticsEvent({
  type: "ADD_TO_CART",
  customer_id: "customer-123",
  brand_id: "brand-456",
  product: { sku: "SKU-001", amount: 1, value: 29.99 }
});

// Clean up on logout
masivo.destroy();

API

createRNClient(config, handlers?)

Creates a Masivo client for React Native that includes event tracking, push notification management, and in-app messaging.

| Parameter | Type | Required | Description | | ----------------------------- | ------------------------------------ | -------- | --------------------------------------------------------------------------------------- | | config.apiKey | string | Yes | Your Masivo CLIENT API key | | config.brandId | string \| null | No | Default brand ID injected into events that have a platform but no explicit brand_id | | handlers.onMessagesReady | (messages: InAppMessage[]) => void | No | Called after fetchMessages / fetchPendingMessages returns messages | | handlers.onMessageShown | (message: InAppMessage) => void | No | Called after an in-app message shown event is tracked | | handlers.onMessageClicked | (message: InAppMessage) => void | No | Called after an in-app message clicked event is tracked | | handlers.onMessageDismissed | (message: InAppMessage) => void | No | Called after an in-app message dismissed event is tracked |

Returns a RNClient with:

  • All methods from MasivoClient (sendAnalyticsEvent, ready, getCapabilities, flush, destroy, getToken)
  • client.push — push notification manager
  • client.inApp — in-app messaging manager

ready() boots auth and then calls /api/storefront/v1/inapp/capabilities. The returned capabilities include inappEnabled and inappEventTypes; when a standard analytics event matches inappEventTypes, the RN client fetches eligible in-app messages with that event type as the trigger.


Event Tracking

client.sendAnalyticsEvent(event)

Sends an analytics event. Events are buffered and sent in batches.

| Field | Type | Required | Description | | ------------- | ---------------- | -------- | -------------------------------------------- | | type | string | Yes | Event type (e.g. ADD_TO_CART, custom type) | | customer_id | string | Yes | External customer identifier | | brand_id | string \| null | Yes | Brand identifier, null if not applicable | | ...rest | unknown | No | Any additional event data |

await masivo.sendAnalyticsEvent({
  type: "ADD_TO_CART",
  customer_id: "customer-123",
  brand_id: null,
  platform: "app"
});

client.flush()

Forces an immediate send of all buffered events. Call this before the app goes to background.

// Example: flush on AppState change
import { AppState } from "react-native";

AppState.addEventListener("change", state => {
  if (state === "background") {
    masivo.flush();
  }
});

Push Notifications

@masivo/rn does not request permissions or receive push tokens itself — that is handled by your push library (Expo Notifications, Firebase, etc.). You obtain the token from your library and pass it to this SDK.

client.push.registerDeviceToken(customerId, deviceToken, deviceInfo)

Registers a device push token with Masivo. Call this when your push library provides a token, or when the user logs in on a device that already has a token.

| Parameter | Type | Required | Description | | ---------------------- | -------------------- | -------- | ------------------------------------------- | | customerId | string | Yes | Masivo customer ID | | deviceToken | string | Yes | Push token from APNs (iOS) or FCM (Android) | | deviceInfo.platform | "ios" \| "android" | Yes | Device platform | | deviceInfo.model | string | No | Device model | | deviceInfo.osVersion | string | No | OS version string, e.g. "17.4" |

Returns Promise<{ success: boolean }>.

// Example with Firebase (react-native-firebase) — use the native FCM token
import messaging from "@react-native-firebase/messaging";

const token = await messaging().getToken();

await masivo.push.registerDeviceToken("customer-123", token, {
  platform: "android"
});
// Expo: prefer the native device push token for silent in-app wake (FCM/APNs)
import * as Notifications from "expo-notifications";

const tokenResult = await Notifications.getDevicePushTokenAsync();
const token = tokenResult.data;

await masivo.push.registerDeviceToken("customer-123", token, {
  platform: "ios",
  model: "iPhone",
  osVersion: "17.4"
});

Token type: Register the native FCM/APNs device token (messaging().getToken() or Expo getDevicePushTokenAsync()). Do not use the Expo Push Token (exp.host/...) for Masivo silent push — Masivo sends data-only messages via your Firebase project.

client.push.unregisterDeviceToken(customerId, deviceToken)

Removes a device token from Masivo. Call this on logout or when the user opts out of push notifications.

await masivo.push.unregisterDeviceToken("customer-123", deviceToken);

createPushTokenSync(push) — login, refresh, logout

Keeps Masivo in sync when FCM/APNs rotates the device token. No native deps — wire your push library via callbacks.

| Method | Description | | ------------------------ | ----------------------------------------------------------------------------------------------------------- | | configure(config) | getCustomerId, getDeviceInfo, getToken, optional onTokenRefresh, isPushAllowed, onAppForeground | | sync() | Alias for enablePush() — register token + refresh listener (call on login) | | enablePush() | Register when allowed; skips if isPushAllowed returns false | | disablePush() | Stop refresh listener + unregister last token (opt-out / logout) | | reconcilePushState() | Sync Masivo token with OS permission ("enabled", "disabled", or "skipped") | | registerCurrentToken() | Register token from getToken() once | | registerToken(token) | Register a specific token string | | unregisterLastToken() | Unregister last registered token only | | startRefreshListener() | Subscribe to token rotation without re-registering | | stop() | Unsubscribe refresh listener only |

// lib/masivo.ts
import {
  createRNClient,
  createPushBridge,
  createPushTokenSync
} from "@masivo/rn";
import messaging from "@react-native-firebase/messaging";
import { AppState, Platform } from "react-native";

export const masivo = createRNClient({
  apiKey: process.env.EXPO_PUBLIC_MASIVO_CLIENT_KEY!,
  brandId: process.env.EXPO_PUBLIC_MASIVO_BRAND_ID
});

export const pushBridge = createPushBridge(masivo.inApp);
export const pushTokenSync = createPushTokenSync(masivo.push);

pushBridge.configure({
  getCustomerId: () => session.customerId,
  getBrandId: () => process.env.EXPO_PUBLIC_MASIVO_BRAND_ID ?? null
});

pushTokenSync.configure({
  getCustomerId: () => session.customerId,
  getDeviceInfo: () => ({
    platform: Platform.OS as "ios" | "android",
    osVersion: String(Platform.Version)
  }),
  getToken: async () => messaging().getToken(),
  onTokenRefresh: handler => messaging().onTokenRefresh(handler),
  isPushAllowed: async () => {
    const status = await messaging().hasPermission();
    const authorized = status === messaging.AuthorizationStatus.AUTHORIZED;
    const provisional = status === messaging.AuthorizationStatus.PROVISIONAL;
    return authorized || provisional;
  },
  onAppForeground: handler => {
    const sub = AppState.addEventListener("change", state => {
      if (state === "active") handler();
    });
    return () => sub.remove();
  }
});

// On login
await pushTokenSync.sync();

// On logout
await pushTokenSync.disablePush();
masivo.destroy();

Expo equivalent — swap getToken / onTokenRefresh for getDevicePushTokenAsync() and Expo's push token listener if your setup exposes one; the pattern is the same.

client.push.trackNotificationOpened(event)

Tracks when a user opens a push notification. Delegates to sendAnalyticsEvent under the hood.

| Field | Type | Required | Description | | ------------------- | ------------------------- | -------- | ------------------------------ | | type | string | Yes | Event type, e.g. PUSH_OPENED | | customer_id | string | Yes | Customer ID | | brand_id | string \| null | Yes | Brand ID | | notification_data | Record<string, unknown> | No | Payload from the notification |

// Example: track notification open from background handler
await masivo.push.trackNotificationOpened({
  type: "PUSH_OPENED",
  customer_id: "customer-123",
  brand_id: "brand-456",
  notification_data: {
    campaign_id: "camp-001",
    message_id: "msg-xyz"
  }
});

MMP device lifecycle

Track installs, reinstalls, and uninstalls with a stable device_uuid. The server decides install vs reinstall from device_uuid history — the SDK does not send is_reinstall. Use durable storage (Keychain / EncryptedSharedPreferences) so the UUID survives app reinstall when the OS restores secure storage.

The SDK ignores server-only silent pushes (MMP_PING) used for uninstall detection.

Secure device storage (recommended)

setAsyncStorage works for development. In production, pass a DeviceStorageAdapter so the UUID persists across reinstall when the OS restores Keychain / backup:

import * as SecureStore from "expo-secure-store";
import { createRNClient, setDeviceStorage } from "@masivo/rn";

const secureStoreAdapter = {
  getItem: (key: string) => SecureStore.getItemAsync(key),
  setItem: (key: string, value: string) => SecureStore.setItemAsync(key, value),
  removeItem: (key: string) => SecureStore.deleteItemAsync(key)
};

setDeviceStorage(secureStoreAdapter);

iOS: expo-secure-store or react-native-keychain (Keychain survives uninstall when configured). Android: expo-secure-store with Auto Backup, or EncryptedSharedPreferences via your preferred library.

Native device ID (native_device_id)

Masivo stores an optional OS-scoped device identifier in customer_devices.native_device_id when you register or sync a device:

| Platform | Typical source | Stored as | | --- | --- | --- | | Android | Android ID (Settings.Secure.ANDROID_ID) | native_device_id | | iOS | IDFV (Identifier for Vendor) | native_device_id |

The SDK does not read these APIs automatically — your app must obtain the value and pass nativeDeviceId in getDeviceInfo() (push sync / MMP session) or deviceInfo.native_device_id on device.register(). If you omit it, the server persists null for that field.

This is separate from device_uuid (Masivo-generated, per app install) and from ad_id (IDFA / GAID for advertising — optional, requires platform consent).

Example with Expo Application:

import * as Application from "expo-application";
import { Platform } from "react-native";

let cachedNativeDeviceId: string | undefined;

const loadNativeDeviceId = async () => {
  if (cachedNativeDeviceId) return cachedNativeDeviceId;
  if (Platform.OS === "android") {
    cachedNativeDeviceId = Application.getAndroidId() ?? undefined;
  } else if (Platform.OS === "ios") {
    cachedNativeDeviceId =
      (await Application.getIosIdForVendorAsync()) ?? undefined;
  }
  return cachedNativeDeviceId;
};

// Call once on app start (e.g. before mmpSession.sync)
await loadNativeDeviceId();

mmpSession.configure({
  getCustomerId: () => currentCustomerId,
  getDeviceInfo: () => ({
    platform: Platform.OS as "ios" | "android",
    model: "iPhone",
    osVersion: String(Platform.Version),
    nativeDeviceId: cachedNativeDeviceId
  }),
  getToken: () => messaging().getToken()
});

You can use any library that exposes the same IDs (e.g. react-native-device-info). Wire it the same way into getDeviceInfo or device.register.

Orchestration (createMmpSession)

createMmpSession wires device.register + push.registerDeviceToken on cold start and token refresh:

import { createRNClient, createMmpSession } from "@masivo/rn";

const masivo = createRNClient({ apiKey: "your-client-api-key" });
const mmpSession = createMmpSession(masivo.push, masivo.device);

mmpSession.configure({
  getCustomerId: () => currentCustomerId,
  getDeviceInfo: () => ({
    platform: "ios",
    model: "iPhone",
    osVersion: "17.4",
    nativeDeviceId: cachedNativeDeviceId // optional; see Native device ID above
  }),
  getToken: () => messaging().getToken(),
  onTokenRefresh: handler => messaging().onTokenRefresh(t => handler(t)),
  onAppForeground: handler => AppState.addEventListener("change", s => {
    if (s === "active") handler();
  }).remove
});

await mmpSession.sync();

createPushTokenSync(push, masivo.device) accepts the same optional device manager and calls device.register before registerDeviceToken.

Cold start flow

import AsyncStorage from "@react-native-async-storage/async-storage";
import { createRNClient, setAsyncStorage } from "@masivo/rn";

setAsyncStorage(AsyncStorage);

const masivo = createRNClient({ apiKey: "your-client-api-key" });

// After you know the customer id (anonymous or registered):
await masivo.device.register({
  customerId: "customer-123",
  deviceInfo: {
    platform: "ios",
    model: "iPhone",
    os_version: "17.4",
    native_device_id: cachedNativeDeviceId // optional
  }
});

// Optional: periodic heartbeat while app is active
await masivo.device.heartbeat("customer-123");

When registering a push token via pushTokenSync or createMmpSession, the SDK calls device.register first, then attaches device_uuid to PATCH /customers/{id}/devices.

Anonymous → registered

Pass device_uuid when identifying an anonymous customer so Masivo links the device row to the new customer id:

await fetch(`${baseUrl}/api/storefront/v1/customers/${anonId}/identify/anonymous`, {
  method: "PATCH",
  headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
  body: JSON.stringify({
    name: "Jane",
    email: "[email protected]",
    device_uuid: await masivo.device.getOrCreateUuid()
  })
});

Silent push types

| masivo_type | SDK behavior | | --- | --- | | INAPP_FETCH | Fetches in-app messages | | MMP_PING | Ignored (server uninstall probe) |

handlePushPayload returns true for both types so your listener can skip further handling.


In-App Messaging

Masivo in-app works like Braze: you integrate the SDK only. Fetch, display rules, and impression logging are handled internally — you do not call REST endpoints from your app.

Default UI (recommended)

Wrap your app after login. When MasivoInAppProvider mounts with a customerId, it fetches pending messages, renders by type, and logs impressions/clicks/dismissals automatically.

import { createRNClient, MasivoInAppProvider } from "@masivo/rn";

const masivo = createRNClient({
  apiKey: process.env.EXPO_PUBLIC_MASIVO_CLIENT_KEY!,
  brandId: process.env.EXPO_PUBLIC_MASIVO_BRAND_ID
});

export function AppShell({ customerId }: { customerId: string }) {
  return (
    <MasivoInAppProvider
      client={masivo}
      customerId={customerId}
      brandId={process.env.EXPO_PUBLIC_MASIVO_BRAND_ID}
    >
      <YourApp />
    </MasivoInAppProvider>
  );
}

Supported default layouts: modal, fullscreen, slideup, banner, content_card.

Custom UI (headless)

Set customUI and render your own components. Use useMasivoInApp() for the queue and Braze-style logging methods:

import { MasivoInAppProvider, useMasivoInApp } from "@masivo/rn";

const CustomInApp = () => {
  const {
    currentMessage,
    logImpressionForMessage,
    clickMessage,
    dismissMessage
  } = useMasivoInApp();
  if (!currentMessage) return null;
  // Render your UI, then:
  // await logImpressionForMessage(currentMessage);
  // await clickMessage(currentMessage, button);
  // await dismissMessage(currentMessage);
  return null;
};

<MasivoInAppProvider client={masivo} customerId={id} customUI>
  <YourApp />
  <CustomInApp />
</MasivoInAppProvider>;

SDK methods (no HTTP required)

| Method | Description | | -------------------------------------------------------------------- | ------------------------------------------------------- | | client.inApp.fetchPendingMessages(customerId, brandId?) | Fetch all pending eligible messages (no trigger filter) | | client.inApp.fetchMessages(params) | Fetch by optional trigger, brand, or type | | client.inApp.logImpression(message, baseEvent) | Message was displayed | | client.inApp.successTap(message, baseEvent) | User tapped CTA | | client.inApp.dismissTap(message, baseEvent) | User closed the message | | client.inApp.subscribe(handler) | Listen after each fetch | | client.inApp.handlePushPayload(data, customerId, brandId?) | Handle silent push data payload | | client.inApp.postBridgeAction(message, baseEvent, action, payload) | HTML bridge: custom events, purchases, attributes | | client.inApp.patchCustomerProfile(customerId, body) | Update customer fields from HTML getUser() API |

logImpression, successTap, and dismissTap are the primary API. handleMessageShown, handleMessageClicked, and handleMessageDismissed remain as aliases. logClick and logDismiss are deprecated aliases of successTap and dismissTap.

Message types

| Type | Use case | | -------------- | --------------------- | | modal | Promos, onboarding | | fullscreen | Full-screen campaigns | | slideup | Bottom sheet style | | banner | Top/bottom banners | | content_card | Feed-style cards |

Custom HTML in-app messages

Templates with content.html render in a WebView via MasivoInAppRenderer. The SDK injects window.masivoBridge for HTML/JS creatives:

<button onclick="masivoBridge.logClick()">Track click</button>
<button onclick="masivoBridge.closeMessage()">Close</button>
<button onclick="masivoBridge.logCustomEvent('survey_done', { step: 1 })">
  Complete
</button>
<script>
  masivoBridge.getUser().setCustomUserAttribute("lead_source", "inapp");
</script>

Personalization uses Masivo {{customer.name}} variables, resolved server-side before delivery. Deep links in <a href="..."> open externally and close the message automatically.

Optional offline cache

import { setAsyncStorage } from "@masivo/rn";
import AsyncStorage from "@react-native-async-storage/async-storage";

setAsyncStorage(AsyncStorage);

Silent push wake (near realtime)

When Masivo creates an in-app message, it can send a silent data push to the customer device. Forward that payload to the SDK — no REST calls from your app.

Use createPushBridge to wire FCM or Expo without extra Masivo packages:

// lib/masivo.ts
import { createRNClient, createPushBridge } from "@masivo/rn";

export const masivo = createRNClient({
  apiKey: process.env.EXPO_PUBLIC_MASIVO_CLIENT_KEY!,
  brandId: process.env.EXPO_PUBLIC_MASIVO_BRAND_ID
});

export const pushBridge = createPushBridge(masivo.inApp);

pushBridge.configure({
  getCustomerId: () => session.customerId,
  getBrandId: () => process.env.EXPO_PUBLIC_MASIVO_BRAND_ID ?? null
});

Payload keys (set by Masivo server):

| Key | Value | | -------------------- | ----------------------------------------------- | | masivo_type | INAPP_FETCH | | masivo_trigger | optional event trigger (omit for pending fetch) | | masivo_customer_id | customer id | | masivo_brand_id | optional brand id |

Requirements:

  • Device token registered via masivo.push.registerDeviceToken (native FCM/APNs token)
  • Firebase configured in Masivo dashboard (same as push)
  • Push permission / consent for the customer

With MasivoInAppProvider, fetched messages appear automatically after the silent push handler runs.


Push integration (FCM & Expo)

@masivo/rn has no native push dependencies. You connect your existing push library to pushBridge.handle(data).

React Native Firebase

Foreground (onMessage) and background (setBackgroundMessageHandler in index.js):

import messaging from "@react-native-firebase/messaging";

import { pushBridge } from "@/lib/masivo";

messaging().onMessage(async remoteMessage => {
  const data = remoteMessage.data ?? {};
  await pushBridge.handle(data);
});

// index.js (outside React tree)
messaging().setBackgroundMessageHandler(async remoteMessage => {
  const data = remoteMessage.data ?? {};
  await pushBridge.handle(data);
});

See RN Firebase messaging.

Expo Notifications

Foreground listener and headless background task:

import * as Notifications from "expo-notifications";
import * as TaskManager from "expo-task-manager";

import { pushBridge } from "@/lib/masivo";

const INAPP_TASK = "MASIVO_INAPP_PUSH";

Notifications.addNotificationReceivedListener(async notification => {
  const data = notification.request.content.data as Record<string, unknown>;
  await pushBridge.handle(data);
});

TaskManager.defineTask(INAPP_TASK, async ({ data, error }) => {
  if (error || !data) return;
  const payload = (data as { body?: Record<string, unknown> }).body ?? data;
  await pushBridge.handle(payload as Record<string, unknown>);
});

await Notifications.registerTaskAsync(INAPP_TASK);

On iOS, enable the remote-notification background mode for silent delivery. See Expo headless notifications.

Foreground vs background

  • Background / killed (limited on iOS): pushBridge.handle fetches and caches messages; UI updates when the app opens via MasivoInAppProvider.
  • Foreground: same handler; provider subscribers refresh the queue.

iOS note: Silent data pushes are not guaranteed when the app is force-quit.


Push opt-out & OS permissions

Push delivery involves three layers your app must coordinate. The SDK handles device token registration only; consent and OS permission stay in your app.

| Layer | Responsibility | SDK | | ----------------- | --------------------------------------------- | -------------------------------- | | In-app preference | Toggle in settings UI | disablePush() / enablePush() | | OS permission | FCM requestPermission / Expo permissions | Wire via isPushAllowed | | Masivo consent | purposes.push_notifications on the customer | Your backend/BFF (not in SDK) |

See FCM Token Management and the storefront consent endpoint (PATCH /customers/{id}/consent) for consent updates.

What each layer blocks

| Scenario | Silent push | In-app on open | | ------------------------------------- | ----------- | -------------- | | Token not registered | No | Yes | | push_notifications: false in Masivo | No | Yes | | OS permission denied | No | Yes |

In-app messages loaded via fetchPendingMessages() when the app opens do not depend on push. Only realtime delivery via silent push requires a valid token and consent.

Configure permission + foreground reconciliation

import AsyncStorage from "@react-native-async-storage/async-storage";
import messaging from "@react-native-firebase/messaging";
import { AppState } from "react-native";

const PUSH_PREF_KEY = "pushEnabled";

pushTokenSync.configure({
  getCustomerId: () => session.customerId,
  getDeviceInfo: () => ({
    platform: Platform.OS as "ios" | "android",
    osVersion: String(Platform.Version)
  }),
  getToken: async () => messaging().getToken(),
  onTokenRefresh: handler => messaging().onTokenRefresh(handler),
  isPushAllowed: async () => {
    const pref = await AsyncStorage.getItem(PUSH_PREF_KEY);
    const inAppEnabled = pref !== "false";
    const status = await messaging().hasPermission();
    const authorized = status === messaging.AuthorizationStatus.AUTHORIZED;
    const provisional = status === messaging.AuthorizationStatus.PROVISIONAL;
    const osGranted = authorized || provisional;
    return inAppEnabled && osGranted;
  },
  onAppForeground: handler => {
    const sub = AppState.addEventListener("change", state => {
      if (state === "active") handler();
    });
    return () => sub.remove();
  }
});

When onAppForeground is set, configure automatically calls reconcilePushState() each time the app returns to the foreground (e.g. after changing notification settings in the OS).

Toggle OFF (in-app opt-out)

await pushTokenSync.disablePush();
await AsyncStorage.setItem(PUSH_PREF_KEY, "false");
// Update Masivo consent (purposes.push_notifications: false) via your BFF

Toggle ON

const status = await messaging().requestPermission();
const authorized = status === messaging.AuthorizationStatus.AUTHORIZED;
const provisional = status === messaging.AuthorizationStatus.PROVISIONAL;
const granted = authorized || provisional;
if (!granted) return;

await AsyncStorage.setItem(PUSH_PREF_KEY, "true");
await pushTokenSync.enablePush();
// Update Masivo consent (purposes.push_notifications: true) via your BFF

Logout

disablePush() replaces stop() + unregisterLastToken():

await pushTokenSync.disablePush();
masivo.destroy();

Breaking changes (v2)

| Removed | Replacement | | ------------------------------------ | --------------------------------------------------------------- | | client.inApp.onSessionStart() | client.inApp.fetchPendingMessages() | | Session trigger export | Removed — use fetchPendingMessages() without a trigger | | Default silent-push trigger fallback | Server omits masivo_trigger; SDK fetches all pending messages |


Full Setup Example (Expo)

// lib/masivo.ts
import { createRNClient, createPushBridge } from "@masivo/rn";
import { createPushTokenSync } from "@masivo/rn";
import { MasivoInAppProvider } from "@masivo/rn";
import messaging from "@react-native-firebase/messaging";
import { AppState, Platform } from "react-native";

export const masivo = createRNClient({
  apiKey: process.env.EXPO_PUBLIC_MASIVO_CLIENT_KEY!,
  brandId: process.env.EXPO_PUBLIC_MASIVO_BRAND_ID
});

export const pushBridge = createPushBridge(masivo.inApp);
export const pushTokenSync = createPushTokenSync(masivo.push);

pushTokenSync.configure({
  getCustomerId: () => session.customerId,
  getDeviceInfo: () => ({
    platform: Platform.OS as "ios" | "android",
    osVersion: String(Platform.Version)
  }),
  getToken: async () => messaging().getToken(),
  onTokenRefresh: handler => messaging().onTokenRefresh(handler),
  isPushAllowed: async () => {
    const status = await messaging().hasPermission();
    const authorized = status === messaging.AuthorizationStatus.AUTHORIZED;
    const provisional = status === messaging.AuthorizationStatus.PROVISIONAL;
    return authorized || provisional;
  },
  onAppForeground: handler => {
    const sub = AppState.addEventListener("change", state => {
      if (state === "active") handler();
    });
    return () => sub.remove();
  }
});

pushBridge.configure({
  getCustomerId: () => session.customerId,
  getBrandId: () => process.env.EXPO_PUBLIC_MASIVO_BRAND_ID ?? null
});
// app/_layout.tsx (after login)
import { masivo, MasivoInAppProvider } from "@/lib/masivo";

export function RootLayout({ customerId }: { customerId: string }) {
  return (
    <MasivoInAppProvider client={masivo} customerId={customerId}>
      <Stack />
    </MasivoInAppProvider>
  );
}
// index.js — background silent push (FCM)
import messaging from "@react-native-firebase/messaging";

import { pushBridge } from "@/lib/masivo";

messaging().setBackgroundMessageHandler(async remoteMessage => {
  const data = remoteMessage.data ?? {};
  await pushBridge.handle(data);
});
// On login / logout
import { masivo, pushTokenSync } from "@/lib/masivo";

async function onLogin() {
  await pushTokenSync.sync();
}

async function onLogout() {
  await pushTokenSync.disablePush();
  masivo.destroy();
}

Error Handling

All error classes are re-exported from @masivo/core:

import { MasivoError, MasivoServerKeyError, MasivoAuthError, MasivoNetworkError } from "@masivo/rn";

try {
  await masivo.sendAnalyticsEvent({ ... });
} catch (error) {
  if (error instanceof MasivoServerKeyError) {
    console.error(error.message);
  } else if (error instanceof MasivoNetworkError) {
    console.error("Network error:", error.message);
  } else if (error instanceof MasivoAuthError) {
    console.error("Auth error:", error.message, error.details);
  } else if (error instanceof MasivoError) {
    console.error("API error:", error.status, error.message);
  }
}

| Error class | When | Properties | | ---------------------- | ----------------------------------------- | ------------------------------ | | MasivoServerKeyError | SERVER API key used instead of CLIENT | message | | MasivoNetworkError | Fetch fails (no connectivity, DNS, etc.) | message | | MasivoAuthError | Authorization fails (invalid/expired key) | message, status, details | | MasivoError | API returns an error response | message, status, details |

Allowed Event Types

This SDK uses CLIENT API keys, which can only emit:

  • Tracking eventsADD_TO_CART, EMPTY_CART, and other tracking types.
  • Custom event types — Any custom event type you have created in the Masivo platform.

Default event types like PURCHASE, ABANDONED_CART, BIRTHDAY, REGISTRATION, and TIER_ADJUSTMENT are not allowed from the client SDK. These must be sent from your backend using a SERVER API key directly via the Masivo REST API.

Token Management

  1. On the first event or device registration, the SDK exchanges your CLIENT API key for a bearer token.
  2. The token is proactively refreshed every 5 minutes in the background.
  3. If a request returns 401, the SDK invalidates the token and retries once with a fresh one.
  4. destroy() cancels the refresh interval — always call it on logout.

Compatibility

  • React Native >= 0.71 (with built-in fetch)
  • Expo (managed and bare workflow)
  • No native modules required
  • No Objective-C, Swift, Java, or Kotlin code

Links

License

MIT