Pushlanedocs
Documentation menu

React Native Quickstart

iOS + Android in one JS module — autolinked, Firebase-backed.

#Prerequisites

  • React Native 0.73 or later
  • @react-native-firebase/messaging configured (a Firebase project for Android FCM)
  • For iOS: a physical device running iOS 14+ and a paid Apple Developer account
  • Your Pushlane Tenant ID, Ingest URL, and write-key — copy them from Settings → Install SDK

#1. Add the Pushlane client (no npm package)

Heads up
@pushlane/react-native is not published on npm (a 404). Don't run npm install @pushlane/react-native. Use the zero-dependency drop-in below (recommended), or build the SDK from source in the Pushlane monorepo.
Note
No package to install — paste one file. Create lib/pushlane.ts and paste the client below. It reads the device push token from @react-native-firebase/messaging (the same library the SDK uses), so your app must have Firebase messaging installed and configured. Your AI agent can scaffold this exact file via the Pushlane MCP (get_install_instructions).
sh
# The drop-in reads the push token from @react-native-firebase/messaging.
# Install it (and its app peer) if your app does not already have it:
npm install @react-native-firebase/app @react-native-firebase/messaging
cd ios && pod install

Paste this into lib/pushlane.ts:

typescript
// Pushlane — zero-dependency drop-in client for React Native. No npm package to install.
// Paste this into your project (e.g. lib/pushlane.ts). Push registration uses
// @react-native-firebase/messaging (already present if your app does push):
// iOS reads the raw APNs token (getAPNSToken), Android the FCM token (getToken).
import { Platform } from 'react-native';
import messaging from '@react-native-firebase/messaging';

type PushlaneValue = string | number | boolean | null;
interface PushlaneConfig {
  tenantId: string;
  publishableKey: string; // your Pushlane write-key (lpk_live_...)
  apiBase?: string;
  pushEnvironment?: 'production' | 'sandbox';
}

const DEFAULT_API = 'https://loop-ingest.loop-push.workers.dev';
let _cfg: PushlaneConfig | null = null;
let _userId: string | null = null;

function uuidv4(): string {
  let s = '';
  for (let i = 0; i < 36; i++) {
    if (i === 8 || i === 13 || i === 18 || i === 23) { s += '-'; continue; }
    if (i === 14) { s += '4'; continue; }
    const r = (Math.random() * 16) | 0;
    s += (i === 19 ? (r & 0x3) | 0x8 : r).toString(16);
  }
  return s;
}
function hex(n: number): string { let s = ''; for (let i = 0; i < n; i++) s += ((Math.random() * 16) | 0).toString(16); return s; }
function traceparent(): string { return '00-' + hex(32) + '-' + hex(16) + '-01'; }
function stripSlash(u: string): string { let s = u; while (s.length > 1 && s.charAt(s.length - 1) === '/') s = s.slice(0, -1); return s; }

async function post(path: string, body: object, extra?: Record<string, string>): Promise<void> {
  if (!_cfg) return;
  const url = stripSlash(_cfg.apiBase || DEFAULT_API) + path;
  const headers: Record<string, string> = { 'Content-Type': 'application/json' };
  if (extra) Object.assign(headers, extra);
  if (_cfg.publishableKey) headers['Authorization'] = 'Bearer ' + _cfg.publishableKey;
  for (let attempt = 0; attempt <= 4; attempt++) {
    try {
      const res = await fetch(url, { method: 'POST', headers, body: JSON.stringify(body) });
      if (res.ok) return;
      if (res.status < 500 && res.status !== 429) return;
    } catch (e) { /* retry */ }
    if (attempt < 4) await new Promise((r) => setTimeout(r, Math.min(Math.pow(2, attempt) * 500, 30000)));
  }
}

export const Pushlane = {
  configure(cfg: PushlaneConfig) { _cfg = cfg; },
  identify(userId: string) { _userId = userId; },
  reset() { _userId = null; },
  track(name: string, properties: Record<string, PushlaneValue> = {}) {
    if (!_cfg || !_userId) return;
    const tp = traceparent();
    void post('/v1/events', {
      eventId: uuidv4(), tenantId: _cfg.tenantId, externalId: _userId, name, properties,
      occurredAt: Date.now(), context: { sdk: 'pushlane-dropin-react-native', sdkVersion: '1.0.0' }, traceparent: tp,
    }, { traceparent: tp });
  },
  async registerForPush() {
    if (!_cfg || !_userId) return;
    await messaging().requestPermission();
    let deviceToken: string | null = null;
    if (Platform.OS === 'ios') {
      await messaging().registerDeviceForRemoteMessages();
      const apns = await messaging().getAPNSToken(); // raw APNs token (what Pushlane delivers to)
      deviceToken = apns ? String(apns).toLowerCase() : null;
    } else {
      deviceToken = await messaging().getToken(); // FCM
    }
    if (!deviceToken) return;
    void post('/v1/register', {
      tenantId: _cfg.tenantId, externalId: _userId, deviceToken,
      pushEnvironment: _cfg.pushEnvironment || 'production',
    });
  },
  setMarketingConsent(optedIn: boolean) {
    if (!_cfg || !_userId) return;
    void post('/v1/consent', { externalId: _userId, category: 'marketing', action: optedIn ? 'opt_in' : 'opt_out' });
  },
  start() { this.track('app_open', {}); },
};
export default Pushlane;
Note
Android — Firebase setup: follow the React Native Firebase messaging guide to add google-services.json. The drop-in calls messaging().getToken() for the FCM token.

#2. Configure the client

Import { Pushlane } from the file you just pasted, ideally in index.ts before AppRegistry.registerComponent. Identify the user before registering for push or tracking any events.

tsx
import { Pushlane } from './lib/pushlane';

// Configure once — call before AppRegistry.registerComponent (e.g. in index.ts).
Pushlane.configure({
  tenantId: 'YOUR_TENANT_ID',
  publishableKey: 'lpk_live_YOUR_KEY', // from Settings → Install SDK
  // apiBase defaults to Pushlane's ingest URL — omit unless you self-host.
});

// Identify the user before push registration and tracking.
Pushlane.identify(currentUser.id);

// Emit app_open once per process.
Pushlane.start();

// Request permission and register the device push token
// (raw APNs token on iOS, FCM token on Android).
await Pushlane.registerForPush();
Heads up
Pushlane.track and Pushlane.registerForPush are no-ops until you have called Pushlane.configure and Pushlane.identify. On logout, call Pushlane.reset():
tsx
// On logout: detach this device from the user.
Pushlane.reset();

#3. Track events

Property values are string | number | boolean | null. The backend coerces values against your event catalogue — never coerce client-side.

tsx
Pushlane.track('workout_completed', { duration: 25, type: 'run' });
Pushlane.track('paywall_viewed', { placement: 'home', paywall_id: 'fall_sale' });
Pushlane.track('subscription_cancelled', { product_id: 'monthly_pro', reason: 'price' });
Note
The drop-in emits app_open only (when you call Pushlane.start()). Unlike the native SDK it does not auto-track opened, received, or session_started — instrument the events you care about explicitly with Pushlane.track. Rename any reserved name such as message_sent (see Events & catalogue).

Pushlane is opt-out by default — a registered device token alone is sufficient for delivery. Only call setMarketingConsent when the user makes an explicit choice in your settings UI.

tsx
// Pushlane is opt-out by default — call this only when the user makes an
// explicit choice in your settings UI.

// User turned the marketing toggle OFF:
Pushlane.setMarketingConsent(false);

// User turned it back ON:
Pushlane.setMarketingConsent(true);

#Push delivery — what works today

Note

Pushlane.track works over HTTP on both platforms — your events and audiences flow into Pushlane immediately.

registerForPush stores the raw APNs token on iOS and Pushlane delivers over APNs, so end-to-end push delivery is proven on iOS. On Android the drop-in registers an FCM token; Pushlane's sender is APNs, so Android delivery is not yet end-to-end.

#5. Verify the integration

Run your app on a physical device. In the Pushlane dashboard, go to Settings → Install SDK and watch the live verification panel.

CheckWhat it means
SDK detectedAt least one event has reached the ingest worker
Events instrumentedDistinct event names from your app appear in the catalogue
Device connectedA device token was registered — you can receive a push

#Next step

To send real pushes to iOS devices you still need to upload your Apple push key (.p8) to Pushlane.

APNs setup (.p8) →