npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@whisperr/react-native

v0.2.1

Published

Whisperr React Native SDK — reliable, Expo-friendly churn-signal event tracking with zero native code.

Readme

@whisperr/react-native

Reliable, Expo-friendly churn-signal event tracking for React Native — zero native code, works in Expo Go, bare React Native, and dev clients alike.

npm i @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: "[email protected]", traits: { plan: "pro" } });

// when something happens
whisperr.track("subscription_cancelled", { reason: "too_expensive" });

// on logout
whisperr.reset();
  • Pure TypeScript, zero dependencies, zero native modules — nothing to link, nothing to prebuild; fully compatible with Expo Go.
  • Anonymous → identified — events before login buffer on-device and attribute to the user on identify().
  • Never loses events — durable on-device queue (via your storage adapter), automatic flush when the app backgrounds, batching, retry/backoff, and a stable $message_id per event so the backend dedups at-least-once retries.
  • Consent-friendlyoptIn() / optOut() persist across launches.

Storage (durability)

The SDK never imports a native module itself — you hand it any AsyncStorage-compatible adapter (getItem/setItem/removeItem):

// The common case:
import AsyncStorage from "@react-native-async-storage/async-storage";
Whisperr.init({ apiKey: "wrk_…", storage: AsyncStorage });

// Or MMKV, expo-sqlite/kv-store, SecureStore — anything with the same shape.

Without storage the SDK still works, but the queue is memory-only: events captured right before a crash or app kill are lost. In Expo, install the adapter with npx expo install @react-native-async-storage/async-storage.

React bindings

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

whisperr.screen("Paywall", { plan: "pro" }); // tracks screen_viewed

Wire it to your navigator once, e.g. React Navigation:

<NavigationContainer
  onStateChange={() => whisperr.screen(navigationRef.getCurrentRoute()?.name)}
>

Identify

whisperr.identify("user_123", {
  traits: { name: "Ada", plan: "pro" },
  email: "[email protected]",
  phone: "+15551234567",
  pushToken: expoPushToken, // expands to an opted-in push channel
});

// Full channel control (consent / verification):
whisperr.identify("user_123", {
  channels: [
    { type: "email", address: "[email protected]", verified: true },
    { type: "sms", address: "+15551234567", optedIn: false },
  ],
});

Push notifications

The SDK never bundles a push library — hand it the token your own messaging setup produces and Whisperr keeps the push channel current:

whisperr.setPushToken(token);
  • Called after login, it re-identifies the push channel immediately.
  • Called before login, the token is buffered and attached to the next identify().
  • Token rotation is handled: the previously sent token is opted out and the new one opted in, so stale tokens don't accumulate — and tokens from the user's other devices are never touched.
  • Setting the same token twice is a no-op, so it's safe to call on every launch and from onTokenRefresh. The last-sent (user, token) pair is persisted through your storage adapter, so the no-op holds across app restarts — and a rotation that happens after a relaunch still opts out the stale token.
  • After reset() (logout), call setPushToken again once the next user logs in.

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

Delivery

  • Events send to POST /v1/events/batch; identity to POST /v1/identify, authenticated with X-API-Key (the ingestion key is publishable).
  • Event names must be lowercase snake_case; invalid names are dropped before queueing and surfaced through onError.
  • 401/403 pause delivery and retain the queue (auth); 429/5xx/network errors retry with backoff, then retain (retry_exhausted); other 4xx drop the offending batch (dropped).

Options

Whisperr.init({
  apiKey: "wrk_…",
  storage: AsyncStorage,      // durable queue + identity (recommended)
  flushAt: 20,                // flush when this many events are queued
  flushIntervalMs: 10000,     // periodic flush
  flushOnAppBackground: true, // flush when the app leaves the foreground
  maxQueueSize: 1000,         // oldest events drop past this
  maxRetries: 6,
  onError: (e) => console.warn("whisperr:", e.type, e.message),
});

Whisperr.init() is an idempotent singleton; construct WhisperrClient directly for explicit lifetimes (call close() when done).

Development

The test suite consumes the shared whisperr-spec fixtures:

WHISPERR_SPEC_PATH=../whisperr-spec/conformance/wire.json npm test

Whisperr — predict churn, automate interventions, recover revenue.