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

@billingbear/purchases

v0.9.5

Published

Cross-platform subscription management SDK for React Native — with remote-configured paywalls

Downloads

1,744

Readme

@billingbear/purchases

Cross-platform subscription management SDK for React Native. Handles Apple, Google, and Stripe subscriptions with anonymous-first user management.

Plug & play — wraps react-native-iap internally. You never touch IAP code directly.

Install

npm install @billingbear/purchases react-native-iap @react-native-async-storage/async-storage

Quick Start

import { Purchases } from '@billingbear/purchases';

// 1. Configure at app startup
await Purchases.configure({
  apiKey: 'pk_live_...',
  productIds: ['com.myapp.premium'],  // or ['com.myapp.premiumMonthly', 'com.myapp.premiumYearly']
});

// 2. Show products
const offering = await Purchases.getOffering();
console.log(offering.monthly?.product.localizedPrice); // "$2.99"

// 3. Purchase — one line, handles everything
const result = await Purchases.purchase(offering.monthly!);
console.log(result.customerInfo.isPremium); // true

That's it. The SDK handles store connection, purchase flow, receipt validation, and entitlement sync.

Full API

Configure

await Purchases.configure({
  apiKey: 'pk_live_...',           // Required
  baseUrl: 'https://custom.url',   // Optional
  appUserId: 'known-user-id',      // Optional, defaults to anonymous
  productIds: ['com.myapp.premium'], // Optional, fetches products on init
});

Products & Offerings

// Raw products
const products = await Purchases.getProducts(['com.myapp.premium']);

// Organized as offering with monthly/yearly
const offering = await Purchases.getOffering();
offering.monthly   // { identifier: 'monthly', product: { localizedPrice: '$2.99', ... } }
offering.yearly    // { identifier: 'yearly', product: { localizedPrice: '$29.99', ... } }

Purchase

// Purchase a package from offering
const result = await Purchases.purchase(offering.monthly!);

// Or purchase a product directly
const result = await Purchases.purchase(products[0]);

// Result
result.customerInfo.isPremium  // true
result.productId               // 'com.myapp.premiumMonthly'
result.transactionId           // '1000000...'

Restore Purchases

const info = await Purchases.restore();
// Finds all store purchases and validates them with the backend
// Transfers subscriptions if they belong to another user

Check Entitlements

const info = await Purchases.getCustomerInfo();
info.isPremium                          // true/false
info.entitlements.premium?.isActive     // true/false
info.activeSubscription?.store          // 'apple', 'google', 'stripe'
info.activeSubscription?.expiresAt      // '2026-12-31T23:59:59Z'

// Shortcuts
await Purchases.isPremium()             // true/false
await Purchases.checkEntitlement('pro') // true/false

Login / Logout

// After your auth system confirms the user
await Purchases.login('firebase-uid-123');
// Merges anonymous purchases into the identified user

// On logout
await Purchases.logout();
// New anonymous ID, identified user keeps everything

// Check
Purchases.isAnonymous  // true/false
Purchases.appUserId    // current ID

Web Checkout (Stripe)

const { url } = await Purchases.createCheckoutSession({
  priceId: 'price_1ABC...',
  successUrl: 'https://myapp.com/success',
  cancelUrl: 'https://myapp.com/cancel',
});
// Open url in browser — payment processed via Stripe webhook

React Hooks

import { usePurchases, useEntitlement } from '@billingbear/purchases';

function PaywallScreen() {
  const { isPremium, loading, refetch } = usePurchases();

  if (loading) return <ActivityIndicator />;
  if (isPremium) return <PremiumContent />;
  return <Paywall />;
}

function FeatureGate({ children }) {
  const { isActive } = useEntitlement('premium');
  if (!isActive) return <UpgradePrompt />;
  return children;
}

Listen for Changes

const unsubscribe = Purchases.addListener((info) => {
  console.log('Premium:', info.isPremium);
});

Cleanup

// On app unmount
await Purchases.dispose();

Android Base Plans

For Android apps using a single product with base plans:

await Purchases.configure({
  apiKey: 'pk_live_...',
  productIds: ['com.myapp.premium'],  // Single product ID
});

const offering = await Purchases.getOffering();
// Base plans are automatically extracted:
offering.monthly  // { product: { offerToken: '...', period: 'monthly' } }
offering.yearly   // { product: { offerToken: '...', period: 'yearly' } }

// offerToken is passed automatically during purchase
await Purchases.purchase(offering.monthly!);

How It Works

Your App                    SDK                         Backend
   │                         │                            │
   ├─ configure() ──────────►│── init IAP connection      │
   │                         │── fetch products           │
   │                         │── fetch entitlements ──────►│
   │                         │◄─────────── customerInfo ──│
   │                         │                            │
   ├─ purchase(monthly) ────►│── requestPurchase (store)  │
   │                         │◄─ store confirms purchase  │
   │                         │── validate receipt ────────►│── validate with Apple/Google
   │                         │◄─────────── customerInfo ──│── update entitlements
   │                         │── finishTransaction        │
   │◄─── PurchaseResult ────│                            │
   │                         │                            │
   ├─ login('user-123') ───►│── merge anonymous → user ──►│── transfer subscriptions
   │◄─── customerInfo ──────│◄─────────── merged ────────│

Native Paywall (SwiftUI / Jetpack Compose)

<Paywall> renders the native paywall — SwiftUI on iOS, Jetpack Compose on Android — when the SDK's autolinked native module is present in your build, and transparently falls back to the JS renderer on Expo Go, react-native-web, SSR and unit tests. Same component, same props, zero JS rendering dependencies. This is the same approach RevenueCat's react-native-purchases-ui uses to bridge to RevenueCatUI.

import { Paywall } from '@billingbear/purchases';

// Renders native on a real iOS/Android build, JS elsewhere — no code change needed.
<Paywall
  identifier="premium-default"
  onPurchaseCompleted={() => navigation.goBack()}
  onClose={() => navigation.goBack()}
/>

You can also branch on availability or render native explicitly:

import { NativePaywall, isNativePaywallAvailable } from '@billingbear/purchases';

isNativePaywallAvailable();          // true on a linked native build
<NativePaywall identifier="…" />;    // force the native view
<Paywall identifier="…" forceJs />;  // force the JS renderer (e.g. visual regression)

How the bridge works: the JS layer fetches the published paywall (stale-while-revalidate) and hands the document to the native renderer, which draws it pixel-native. When the user taps the CTA the native view fires onPurchase back to JS, where the configured Purchases runs the real StoreKit/Play purchase and conversion telemetry — so all billing stays in JS where Purchases is configured.

iOS install

The native paywall view is autolinked via the umbrella podspec (BillingBearPurchases.podspec), which depends on the standalone BillingBear pod (the SwiftUI renderer). Add the renderer to your app's ios/Podfile, then pod install:

# ios/Podfile  (monorepo / local SDK)
pod 'BillingBear', :path => '../node_modules/@billingbear/purchases/../../sdk/ios'
# …or once BillingBear is published to CocoaPods trunk, no Podfile line is needed.

BillingBear pulls Lottie transitively via the lottie-ios pod (animated paywall backgrounds render out of the box). Minimum iOS 15.

Android install

The native paywall view is autolinked via BillingBearPackage. The Gradle library depends on the Android renderer (io.billingbear:purchases), which resolves from Maven Central — no extra setup for published SDK versions. For monorepo development against an unpublished renderer build, publish it to your local Maven repo first:

# from sdk/android
./gradlew publishToMavenLocal     # publishes io.billingbear:purchases:<version>

Host app requirement (SDK ≥ 0.8): the SDK's android module hosts the Compose paywall renderer and applies org.jetbrains.kotlin.plugin.compose, which must be on the host's buildscript classpath or the build fails with Plugin with id 'org.jetbrains.kotlin.plugin.compose' not found. Add it next to the Kotlin plugin in your root android/build.gradle:

buildscript {
  dependencies {
    classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
    classpath("org.jetbrains.kotlin:compose-compiler-gradle-plugin:$kotlinVersion") // ← required
  }
}

The renderer bundles Compose, Coil, Media3/ExoPlayer (video backgrounds) and lottie-compose transitively — animated paywalls work with no extra setup. Minimum SDK 24, JVM target 17.

Verification: the JS (tsc + jest) and the native Kotlin/Swift sources all compile headless. Final on-device rendering (the SwiftUI UIHostingController host and the Compose ComposeView host) must be verified on a simulator/emulator with a host app, since the RN view managers link against React-Core / react-android which only resolve inside a built app.