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.1.0

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
pnpm add @masivo/rn
yarn add @masivo/rn

Quick Start

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

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

// Track a standard 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, flush, destroy, getToken)
  • client.push — push notification manager
  • client.inApp — in-app messaging manager

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 { AppState, Platform } from "react-native";
import messaging from "@react-native-firebase/messaging";
import { createRNClient, createPushBridge, createPushTokenSync } 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);
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"
  }
});

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. The SDK fetches on session start, renders messages 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,
  baseUrl: "http://10.0.2.2:3000"
});

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.logClick(message, baseEvent) | User tapped CTA | | client.inApp.logDismiss(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 |

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

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 |

Optional offline cache

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

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 { AppState } from "react-native";
import messaging from "@react-native-firebase/messaging";
import AsyncStorage from "@react-native-async-storage/async-storage";

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_START_TRIGGER export | Not needed — fetch pending messages without a trigger | | Default masivo_trigger: SESSION_START in server silent push | Server omits masivo_trigger; SDK fetches all pending messages |


Full Setup Example (Expo)

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

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

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