npm.io
2.1.1 • Published 3d ago

@masivo/rn

Licence
MIT
Version
2.1.1
Deps
1
Size
246 kB
Vulns
0
Weekly
0

@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
});

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"
  }
});

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.

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

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

License

MIT