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

@redchurn/react-native-sdk

v0.5.0

Published

RedChurn React Native SDK — cancel save, billing recovery and win-back flows

Readme

@redchurn/react-native-sdk

Stop subscription churn from inside your React Native app. Redchurn turns the three moments that quietly lose you revenue — a tapped Cancel, a failed payment, a lapsed subscriber — into native in-app retention flows you control from a dashboard, without shipping a new build.

<RedchurnProvider config={{ sdkKey: "rdchrn_live_…", rcAppUserId: user.rcId }}>
  <App />
</RedchurnProvider>

That's the install. Titles, offers, surveys, styling, and on/off switches live in your Redchurn dashboard and reach the app in under a minute.

  • 🛟 Cancel Save — intercept the cancel tap, run an optional survey, present retention offers (pause / downgrade / discount / support).
  • 💳 Billing Recovery — detect a failed payment and prompt the user to fix it, on-screen and via notifications.
  • 👋 Win-back — re-engage churned subscribers with a tailored offer.
  • 🎛 Remote-driven — copy, styling, offers, and flow toggles propagate live (≤ 60 s). No App Store / Play release required.
  • 🧱 Drop-in components and headless hooks if you want your own UI.
  • 🔔 Notifications — local reminders via Notifee + server push via FCM/APNs.
  • 🛡 Fail-safe — never throws into your app, never blocks a cancellation, degrades to a no-op when misconfigured.

Version 0.4.0 · React 18+ · React Native 0.72+ · works in Expo dev builds (or use @redchurn/expo for the managed workflow). Pairs with RevenueCat.

Using Expo's managed workflow? Install @redchurn/expo instead — it re-exports everything here and adds an Expo config plugin + an expo-notifications adapter.


🇫🇷 Documentation en français : Guide d'intégration Redchurn

Table of contents

  1. How it works
  2. Installation
  3. Quick start
  4. Provider configuration
  5. Flow 1 — Cancel Save
  6. Applying a cancel offer
  7. Flow 2 — Billing Recovery
  8. Flow 3 — Win-back
  9. Notifications (local + push)
  10. Headless hooks
  11. Fail-safe guarantees
  12. Public API
  13. Troubleshooting

How it works

Redchurn is install-once, configure-from-the-dashboard (the same model as RevenueCat Paywalls or Superwall). You mount the provider once; everything the subscriber sees is edited remotely.

Which prompt a user is eligible for is decided by their subscriber status (active, billing_issue, churned), which Redchurn learns from your RevenueCat webhook. The SDK polls a tiny /version endpoint and only re-downloads config when it actually changed.

| Flow | Trigger | Component | Hook | | --- | --- | --- | --- | | Cancel Save | User taps your "Cancel" button | CancelSaveFlow | useCancelFlow | | Billing Recovery | Subscriber billing_issue | BillingRecoveryFlow | useBillingRecovery | | Win-back | Subscriber churned | WinBackFlow | useWinBack |


Installation

npm install @redchurn/react-native-sdk @notifee/react-native
# or
yarn add @redchurn/react-native-sdk @notifee/react-native
# Expo dev build
npx expo install @redchurn/react-native-sdk @notifee/react-native

Peer dependencies (all notification-related ones are optional — the SDK runs without them, just without local notifications / FCM push):

| Peer | Required? | Used for | | --- | --- | --- | | react ≥ 18, react-native ≥ 0.72 | yes | — | | @notifee/react-native ≥ 9 | optional | Local billing/win-back notifications | | @react-native-firebase/messaging ≥ 18 | optional | FCM/APNs server-push tokens | | @react-native-async-storage/async-storage | optional | Persisting background notification taps |


Quick start

// index.js — register the background handler ONCE, outside React.
// Required so notification taps work while the app is backgrounded/killed.
import { registerRedchurnBackgroundEventHandler } from "@redchurn/react-native-sdk";
registerRedchurnBackgroundEventHandler();
// App.tsx
import {
  RedchurnProvider,
  CancelSaveFlow,
  BillingRecoveryFlow,
  WinBackFlow,
} from "@redchurn/react-native-sdk";

export default function App() {
  const rcAppUserId = user.revenueCatAppUserId;

  return (
    <RedchurnProvider
      config={{
        sdkKey: "rdchrn_live_xxxxxxxx",
        rcAppUserId,
        notificationsEnabled: true,
        androidNotificationSmallIcon: "ic_notification", // white silhouette drawable
      }}
    >
      <Home rcAppUserId={rcAppUserId} />
    </RedchurnProvider>
  );
}

function Home({ rcAppUserId }) {
  return (
    <>
      {/* Render only when the subscriber actually needs them */}
      <BillingRecoveryFlow rcAppUserId={rcAppUserId} />
      <WinBackFlow rcAppUserId={rcAppUserId} onCtaPress={() => openPaywall()} />

      {/* Intercept your own cancel button */}
      <CancelSaveFlow
        rcAppUserId={rcAppUserId}
        onOfferAccepted={applyOffer} // see "Applying a cancel offer"
        render={({ interceptCancel }) => (
          <Button
            title="Cancel subscription"
            onPress={async () => {
              if (!(await interceptCancel())) openStoreSubscriptions();
            }}
          />
        )}
      />
    </>
  );
}

Provider configuration

RedchurnProvider takes a single config object. Only sdkKey is required.

| Field | Type | Default | Description | | --- | --- | --- | --- | | sdkKey | string | required | Live key rdchrn_live_…. | | rcAppUserId | string | — | RevenueCat App User ID. Required to fetch prompts / record outcomes. | | apiBaseUrl | string | https://app.redchurn.io | Override for dev/staging. | | locale | string | auto | BCP-47 tag; server falls back to English. | | platform | "ios" \| "android" | auto | Sent on register. | | debug | boolean | false | Log SDK diagnostics. | | onError | (error: RedchurnError) => void | — | Non-fatal error sink (never throws). | | autoRegister | boolean | true | Register + heartbeat at mount. | | prefetchConfig | boolean | true | Fetch remote config at mount. | | versionPollIntervalMs | number | 60000 | /version poll cadence. 0 disables. | | configCacheTtlMs | number | 60000 | Local config cache TTL. | | notificationsEnabled | boolean | true | Schedule local billing/win-back notifications. | | androidNotificationSmallIcon | string | ic_launcher | Drawable name (use a white silhouette). | | pushToken / pushProvider | string / "fcm" \| "apns" | — | Provide a server-push token explicitly. | | deviceLabel | string | — | Label shown in the dashboard. | | pushEnvironment | "production" \| "sandbox" | __DEV__ ? sandbox : production | APNs environment. |

Set the user later if it isn't known at mount:

const { setRCAppUserId } = useRedchurn();
setRCAppUserId(user.revenueCatAppUserId);

Flow 1 — Cancel Save

Intercept your own "Cancel subscription" button before opening the store's cancellation UI. interceptCancel() returns true when a save sheet is shown (stop there) or false when there's nothing to show (proceed to the store).

<CancelSaveFlow
  rcAppUserId={rcAppUserId}
  onOfferAccepted={applyOffer}
  onUnavailable={(reason) => console.log("No save sheet:", reason)}
  render={({ interceptCancel }) => (
    <Button
      title="Cancel subscription"
      onPress={async () => {
        if (!(await interceptCancel())) openStoreSubscriptions();
      }}
    />
  )}
/>

Omit render to get the batteries-included sheet (survey → offers, styling, accessibility). Provide render for full control while keeping the SDK's logic.

onUnavailable reasons: "no_user", "flow_disabled", "no_offers", "ready", "already_open".

Fail-open: if the SDK can't show a sheet for any reason, interceptCancel() returns false and the user always reaches the store. You never trap anyone.


Applying a cancel offer

When the user accepts an offer, your onOfferAccepted handler applies it through RevenueCat/StoreKit. Redchurn records the save only after your handler resolves without throwing.

import { getCancelOfferApplicationParams } from "@redchurn/react-native-sdk";

async function applyOffer(offer) {
  const params = getCancelOfferApplicationParams(offer);
  switch (params.type) {
    case "discount":
      // Apply a StoreKit/Play promotional offer via react-native-purchases
      await applyDiscountWithRevenueCat(params);
      break;
    case "pause":
      await pauseSubscription({ months: params.pauseMonths });
      break;
    case "downgrade":
      openPaywall({ productId: params.targetProductId });
      break;
    case "support":
      Linking.openURL(params.contactUrl ?? `mailto:${params.contactEmail}`);
      break;
  }
}

Prefer a declarative style? applyCancelOffer(offer, { discount, pause, downgrade, support }) dispatches to the matching handler for you.

Throw on failure. If applyOffer throws, the sheet stays open and no save is credited — exactly what you want when a purchase fails or is cancelled.

If a user un-cancels later via the store without going through an offer, the RevenueCat UNCANCELLATION webhook still counts as a save server-side. No double counting — the backend de-duplicates by event id.


Flow 2 — Billing Recovery

Renders nothing until the subscriber is in billing_issue. Mount it once near the top of your home screen.

<BillingRecoveryFlow
  rcAppUserId={rcAppUserId}
  presentation="banner"       // "banner" (default) or "sheet"
  onCtaPress={() => openUpdatePaymentFlow()}
/>

For custom UI use useBillingRecovery({ rcAppUserId }){ visible, title, message, ctaLabel, ctaUrl, check, dismiss, markRecovered }.


Flow 3 — Win-back

Renders only for churned subscribers.

<WinBackFlow rcAppUserId={rcAppUserId} onCtaPress={() => openReactivationPaywall()} />

Custom UI: useWinBack({ rcAppUserId }){ visible, title, message, ctaLabel, ctaUrl, dismissLabel, check, dismiss, markReactivated }.


Notifications (local + push)

Billing-recovery and win-back can reach users two ways. Use both:

| Channel | Delivered by | Works when app is closed? | | --- | --- | --- | | Local | Notifee, scheduled by the SDK while the app runs | ✅ once scheduled | | Server push | Redchurn via FCM/APNs, triggered by the RevenueCat webhook | ✅ even if never reopened |

Local (Notifee)

  1. Keep notificationsEnabled: true.
  2. Add a white silhouette drawable and pass its name as androidNotificationSmallIcon (not your colored launcher icon).
  3. On Android 13+, call requestNotificationPermission() once after login.
  4. Register registerRedchurnBackgroundEventHandler() in index.js (see Quick start) so taps work when the app is backgrounded.
import { requestNotificationPermission } from "@redchurn/react-native-sdk";
await requestNotificationPermission();

Server push (FCM/APNs)

With @react-native-firebase/messaging installed, the SDK auto-registers the FCM token. To pass a token yourself, set pushToken / pushProvider in the provider config, or use the notifications API from useRedchurnNotifications().

Preview on-device

import { scheduleTestNotification } from "@redchurn/react-native-sdk";
await scheduleTestNotification({ title: "Payment failed", body: "Update your card." });

Deeplinks (email → app)

Open the right flow when a user taps a link — e.g. an "Update payment" button in a billing-recovery email — and track the open for attribution. Your app owns its URL scheme / universal links; the SDK interprets the URL. A Redchurn deeplink carries a flow (plus optional step, campaign, source):

myapp://redchurn?flow=billing_recovery&step=payment_failed&campaign=oct&source=email
https://app.example.com/r?flow=win_back&source=email

Mount the handler once near your root:

import { useRedchurnDeeplinkHandler } from "@redchurn/react-native-sdk";

useRedchurnDeeplinkHandler({
  onBillingRecovery: () => billing.check(),
  onWinBack: () => winBack.check(),
});

It handles cold-start and live links, ignores non-Redchurn URLs, and fires a LINK_OPENED event with { flow, source, campaign } so opens are attributable in the dashboard. Use parseRedchurnDeeplink(url) to parse a URL yourself.

One hook for both. Notification taps and deeplinks route to the same flows, so useRedchurnOpenHandler({ onBillingRecovery, onWinBack }) wires both with a single set of callbacks. Reach for the two hooks directly only if you need to handle the sources differently.

Configure your app's deeplink base URL in the dashboard so billing-recovery / win-back emails automatically link to the app with these params (they fall back to the web CTA when it's unset).

Headless hooks

Build your own UI on the same logic the components use:

| Hook | Returns | | --- | --- | | useRedchurn() | Context: { ready, remoteConfig, refreshConfig, setRCAppUserId, trackEvent, recordOutcome, getPrompts, isFlowInAppEnabled, … } | | useCancelFlow(options) | { isVisible, session, busy, offers, interceptCancel, selectSurveyReason, handleOutcome, dismiss } | | useBillingRecovery({ rcAppUserId }) | { visible, title, message, ctaLabel, ctaUrl, check, dismiss, markRecovered } | | useWinBack({ rcAppUserId }) | { visible, title, message, ctaLabel, ctaUrl, dismissLabel, check, dismiss, markReactivated } | | useRedchurnLifecycle() | Tracks APP_OPEN on mount + foreground | | useRedchurnNotifications() | Sync/cancel local notifications, register push tokens, schedule test | | useRedchurnNotificationHandler() | Wires foreground/background/pending tap routing |


Fail-safe guarantees

| Rule | Consequence for your app | | --- | --- | | Never throws to your code | API calls return result objects / sensible defaults. | | Fail-open on cancel | If tracking is down, the user can still cancel. | | No-op on bad key | An invalid sdkKey → flows render nothing, no crash. | | 12 s timeout + bounded retries | Only /outcomes is retried (it credits saved MRR). | | Isolated onError | Your callback is non-throwing. |


Public API

// Provider & context
RedchurnProvider, useRedchurn

// Flow components (batteries-included)
CancelSaveFlow, BillingRecoveryFlow, WinBackFlow
// Lower-level presentational pieces
CancelSaveSheet, BillingRecoveryBanner, BillingRecoverySheet, WinBackPrompt

// Hooks
useCancelFlow, useBillingRecovery, useWinBack,
useRedchurnLifecycle, useRedchurnNotifications, useRedchurnNotificationHandler

// Notifications
registerRedchurnBackgroundEventHandler, requestNotificationPermission,
isNotifeeAvailable, scheduleTestNotification, getFcmToken,
isFirebaseMessagingAvailable, parseRedchurnNotificationData

// Offer utilities
applyCancelOffer, getCancelOfferApplicationParams, resolveCancelOffer,
resolvedOfferToMetadata, getPauseMonths, getDiscountPercent, resolveDowngradePath

// Styling & client
parseInAppStyle, DEFAULT_IN_APP_STYLE, createRedchurnClient, RedchurnClient, safeOpenUrl

// Constants & types
SDK_VERSION, DEFAULT_API_BASE_URL, FLOW_TYPES, CANCEL_OFFER_KEYS
// + RedchurnConfig, RedchurnError, SdkRemoteConfig, ResolvedCancelOffer, FlowType, …

Troubleshooting

| Symptom | Likely cause | Fix | | --- | --- | --- | | Dashboard says "SDK not detected" | Register never ran | Check sdkKey, network, and that the provider mounts. | | Cancel sheet never shows | Flow/offers disabled, or no rcAppUserId | Enable Cancel Save + an offer; pass a valid id. | | Notification taps do nothing when killed | Background handler not registered | Call registerRedchurnBackgroundEventHandler() in index.js. | | Android notifications absent | Missing permission / wrong icon | Call requestNotificationPermission(); use a white silhouette icon. | | Local notifications never appear | Notifee not installed | npm install @notifee/react-native (optional peer). |

Tests

npm run test       # vitest
npm run typecheck  # tsc --noEmit

Changelog · CHANGELOG.mdDashboard · app.redchurn.ioSupport · [email protected] · License · MIT