React Native Quickstart
iOS + Android in one JS module — autolinked, Firebase-backed.
#Prerequisites
- React Native 0.73 or later
@react-native-firebase/messagingconfigured (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)
@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.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).# 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 installPaste this into lib/pushlane.ts:
// 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;
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.
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();Pushlane.track and Pushlane.registerForPush are no-ops until you have called Pushlane.configure and Pushlane.identify. On logout, call Pushlane.reset():// 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.
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' });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).#4. Consent (opt-out model)
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.
// 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
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.
| Check | What it means |
|---|---|
| SDK detected | At least one event has reached the ingest worker |
| Events instrumented | Distinct event names from your app appear in the catalogue |
| Device connected | A 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.