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.
npm i @whisperr/react-nativePublished on npm as @whisperr/react-native.
Quickstart
Section titled “Quickstart”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 restorewhisperr.identify("user_123", { email: "ada@acme.com", traits: { plan: "pro" } });
// when something happenswhisperr.track("subscription_cancelled", { reason: "too_expensive" });
// on logoutwhisperr.reset();What you get
Section titled “What you get”- 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_idper event so restart resends dedup server-side. - Consent-friendly —
optIn()/optOut()persist across launches. - Batches to
/v1/events/batchwith 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:
npx expo install @react-native-async-storage/async-storagePass 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 });Push tokens
Section titled “Push tokens”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.
Provider + hook
Section titled “Provider + hook”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" />;}Screens
Section titled “Screens”whisperr.screen("Paywall", { plan: "pro" }); // tracks screen_viewedWire it to your navigator once — Expo Router / React Navigation:
<NavigationContainer onStateChange={() => whisperr.screen(navigationRef.getCurrentRoute()?.name)}>Options
Section titled “Options”| 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.
A note on the API key
Section titled “A note on the API key”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.