Skip to content

React Native

Reliable churn-signal tracking for React Native — zero native code, zero dependencies. Works in Expo Go, bare React Native, and dev clients alike: nothing to link, no config plugin, no prebuild.

Terminal window
npm i @whisperr/react-native

Published on npm as @whisperr/react-native.

import AsyncStorage from "@react-native-async-storage/async-storage";
import { Whisperr } from "@whisperr/react-native";
const whisperr = Whisperr.init({ apiKey: "wrk_…", storage: AsyncStorage });
// after the user logs in / on session restore
whisperr.identify("user_123", { email: "ada@acme.com", traits: { plan: "pro" } });
// when something happens
whisperr.track("subscription_cancelled", { reason: "too_expensive" });
// on logout
whisperr.reset();
  • Anonymous → identified — events tracked before login buffer on-device and attribute to the user retroactively on identify().
  • Never loses events — a durable on-device queue (via your storage adapter), automatic flush when the app backgrounds, and a stable $message_id per event so restart resends dedup server-side.
  • Consent-friendlyoptIn() / optOut() persist across launches.
  • Batches to /v1/events/batch with retry and backoff per the delivery contract.

The SDK is pure TypeScript, so it runs in Expo Go unmodified and ships updates over-the-air with expo-updates. The one recommended step is durable storage:

Terminal window
npx expo install @react-native-async-storage/async-storage

Pass it as storage: (works in Expo Go too — it’s bundled in the Go client). Without it the SDK still works, but the queue is memory-only and events captured right before an app kill are lost.

The SDK never imports a native module itself — any getItem/setItem/removeItem adapter works: AsyncStorage, expo-sqlite/kv-store, or a thin wrapper over MMKV.

Have a push token from expo-notifications? Pass it on identify:

whisperr.identify("user_123", { pushToken: expoPushToken });

The SDK never bundles a push library — hand it the token your messaging setup produces and Whisperr keeps the push channel current. setPushToken is safe to call on every launch and from a refresh listener: repeated tokens are a no-op, a rotated token opts the previous one out, and a token set before login buffers and attaches to the next identify().

With @react-native-firebase/messaging:

import messaging from "@react-native-firebase/messaging";
import { useWhisperrPushToken } from "@whisperr/react-native";
function PushBridge() {
const [token, setToken] = useState<string | null>(null);
useEffect(() => {
messaging().getToken().then(setToken);
return messaging().onTokenRefresh(setToken);
}, []);
useWhisperrPushToken(token); // forwards to whisperr.setPushToken()
return null;
}

With expo-notifications:

import * as Notifications from "expo-notifications";
const [token, setToken] = useState<string | null>(null);
useEffect(() => {
Notifications.getDevicePushTokenAsync().then((t) => setToken(t.data));
const sub = Notifications.addPushTokenListener((t) => setToken(t.data));
return () => sub.remove();
}, []);
useWhisperrPushToken(token);

After reset() (logout), call setPushToken again once the next user logs in.

import { WhisperrProvider, useWhisperr } from "@whisperr/react-native";
export default function App() {
return (
<WhisperrProvider options={{ apiKey: "wrk_…", storage: AsyncStorage }}>
<Root />
</WhisperrProvider>
);
}
function CancelButton() {
const whisperr = useWhisperr();
return <Button onPress={() => whisperr.track("cancel_tapped")} title="Cancel" />;
}
whisperr.screen("Paywall", { plan: "pro" }); // tracks screen_viewed

Wire it to your navigator once — Expo Router / React Navigation:

<NavigationContainer
onStateChange={() => whisperr.screen(navigationRef.getCurrentRoute()?.name)}
>
Option Default Notes
apiKey App ingestion key (wrk_…). Required.
storage AsyncStorage-compatible adapter for the durable queue.
baseUrl https://api.whisperr.net Ingestion base URL.
flushAt / flushIntervalMs 20 / 10000 Batch triggers.
flushOnAppBackground true Flush when the app leaves the foreground.
maxQueueSize 1000 Oldest events drop beyond this.
debug false Verbose logging.
onError Delivery failures (auth / dropped / retry_exhausted).

Event names must be snake_case — see Event design.

The ingestion key ships in your app bundle by design — it can only ingest events for your app. Treat it as publishable, like a Segment write key.