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-js

v0.7.0

Published

Web subscription management SDK for BillingBear — gate premium features via Stripe web checkout. Same REST client as @billingbear/purchases but built on fetch + localStorage (no react-native-iap / AsyncStorage).

Downloads

389

Readme

@billingbear/purchases-js

Web subscription SDK for BillingBear — gate premium features in any web app and take payment through Stripe Checkout. Same REST backend as the React Native SDK (@billingbear/purchases), but built on plain fetch + localStorage with SSR guards. No react-native-iap, no AsyncStorage, no native modules — it runs in the browser and during Next.js / SSR server render without throwing.

npm install @billingbear/purchases-js

Quick start

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

// Call once at app startup. Resolves (and persists) an anonymous appUserId
// in localStorage if you don't pass one.
const info = await Purchases.configure({ apiKey: 'pk_live_...' });
if (info.isPremium) unlockApp();

Gating premium

if (await Purchases.isPremium()) { /* ... */ }

// Or check a specific entitlement identifier:
if (await Purchases.checkEntitlement('pro')) { /* ... */ }

// Latest snapshot (re-fetches entitlements):
const info = await Purchases.getCustomerInfo();

Stripe web checkout

// Creates a Checkout session and redirects the browser to Stripe.
// successUrl / cancelUrl default to the current page.
await Purchases.startWebCheckout({ priceId: 'price_123' });

// Or create the session yourself and control the redirect:
const { url } = await Purchases.createCheckoutSession({
  priceId: 'price_123',
  successUrl: 'https://app.example.com/welcome',
  cancelUrl: 'https://app.example.com/pricing',
});
window.open(url, '_blank');

Offerings → checkout

Fetch your offerings and check out the right package without hard-coding the Stripe price id. For the stripe provider a product's productId IS the Stripe Price id; the stripePriceId helper pulls it out for you:

import { Purchases, stripePriceId } from '@billingbear/purchases-js';

const { offerings } = await Purchases.getOfferings();
const current = offerings.find((o) => o.isCurrent) ?? offerings[0];

const monthly = current?.packages.find((p) => p.identifier === 'monthly');
const priceId = stripePriceId(monthly);          // -> "price_123" | null
if (priceId) {
  await Purchases.startWebCheckout({ priceId }); // redirect to Stripe Checkout
}

Each OfferingProduct also carries display price metadata when the backend has it (priceString, price, currencyCode, billingPeriod, trialPeriod, localizedPrices) so you can render a paywall with the real price.

Attribution

The same attribution / MMP setters as the React Native SDK are available — one line each, writing the reserved $-prefixed subscriber attributes:

await Purchases.setAdjustID('adjust-id');
await Purchases.setAppsflyerID('appsflyer-id');
await Purchases.setCampaign('spring_sale');
// also: setFBAnonymousID, setMparticleID, setOnesignalID, setAirshipChannelID,
// setCleverTapID, setMixpanelDistinctID, setFirebaseAppInstanceID,
// setMediaSource, setAdGroup, setAd, setKeyword, setCreative

Identity

// Link the anonymous user to your own user id (merges entitlements server-side).
await Purchases.login('your-user-id');

// Reset to a fresh anonymous user.
await Purchases.logout();

Paywalls

The SDK ships a built-in paywall renderer — compiled-to-vanilla DOM with inline styles, zero framework dependencies (no React, no Tailwind, no CSS to import). Fetch a paywall and present it as a modal:

import { Purchases, presentPaywall } from '@billingbear/purchases-js';

const paywall = await Purchases.getPaywall('main');
if (paywall) {
  const result = await presentPaywall({
    paywall,
    variables: { 'user.firstName': 'Marco' }, // fills {{ user.firstName }} in the copy
    // The chosen product identifier IS the Stripe Price id by default; map it
    // explicitly if your dashboard product identifiers differ:
    // resolvePriceId: (productId) => priceMap[productId],
  });
  // { outcome: 'purchased' | 'dismissed' | 'restored', productId? }
  if (result.outcome === 'purchased') unlock();
}

presentPaywall mounts a fullscreen overlay (high z-index, body scroll-lock), renders all five layouts (vertical-features, horizontal-products, comparison-table, minimal, hero-image) with solid/gradient/image/video/ lottie backgrounds, the trial badge, urgency countdown, social proof, and the optional survey overlay — faithful to the dashboard's hosted page.

Lottie backgrounds (optional)

Paywalls with a lottie background animate via lottie-web, which the SDK loads on-demand (a guarded dynamic import) so it stays a zero-required-dependency package. lottie-web is declared as an optional dependency / optional peer — install it only if you ship lottie paywalls:

npm install lottie-web

When the module isn't present (or the animation fails to load), the renderer falls back to the paywall's configured poster image, so a lottie paywall always looks intentional without the extra dependency.

Redirect note: the CTA calls Purchases.startWebCheckout, which redirects the browser to Stripe. When that happens the page navigates away, so the promise may never resolve — the user lands on your successUrl instead. Pass successUrl / cancelUrl to control where they return.

Inline embed

To embed the paywall inside your own page instead of a modal, use renderPaywall — same options, no overlay, outcome delivered via onOutcome:

import { renderPaywall } from '@billingbear/purchases-js';

const controller = renderPaywall(document.getElementById('paywall')!, {
  paywall,
  onOutcome: (r) => { if (r.outcome === 'purchased') unlock(); },
});
// controller.dismiss() / controller.destroy()

Events

Both entry points emit the standard paywall event stream (identical to the React Native SDK) — subscribe for funnel analytics:

import { addPaywallEventListener } from '@billingbear/purchases-js';

const off = addPaywallEventListener((e) => analytics.track(e.type, e));
// paywall_impression | paywall_dismiss | paywall_purchase_started/completed/failed
// | paywall_custom_action | paywall_survey_response

Exposures are recorded automatically: viewed at mount, dismissed / purchased at the outcome. Survey answers also write the $survey_<id> subscriber attribute for audience targeting.

Headless / custom renderer

getPaywall tags the request as platform: 'web' and passes the current appUserId for A/B variant assignment, recording a best-effort viewed exposure. If you render the paywall yourself, opt out and record the funnel manually:

const paywall = await Purchases.getPaywall('main', { recordExposure: false });
if (paywall) {
  // …your renderer…
  Purchases.recordPaywallExposure(paywall, 'viewed');
  Purchases.recordPaywallExposure(paywall, 'dismissed'); // or 'purchased'
}

recordPaywallExposure is fire-and-forget — it never awaits or throws, and forwards the paywall's assigned experiment id + variant automatically. The low-level building block renderPaywallDocument(container, document, options) is also exported if you want the DOM without the modal/telemetry shell.

Customer Center

A drop-in, themeable in-app subscription-management screen for your end users — the web counterpart of the React Native <CustomerCenter />. Compiled-to-vanilla DOM with inline styles, zero framework dependencies. Present it as a modal:

import { presentCustomerCenter } from '@billingbear/purchases-js';

const result = await presentCustomerCenter();
// { outcome: 'closed', lastAction?: { outcome, subscriptionId } }

On open it loads the Customer Center payload (subscriptions + the themeable config) for the tracked appUserId, renders each subscription with its server-driven action buttons, and wires them to the SDK:

  • manage_in_store → opens the store / billing-portal URL in a new tab
  • cancel / cancel_at_period_end → optional cancellation survey, then a win-back offer (managed Stripe/Paddle subs) or a retention prompt, before the cancel POST
  • reactivate → re-enables a period-end cancellation
  • request_refund → confirm, then request a refund
  • change_plan → switches to a target product, or falls back to support

Support links, custom sections and deep links from the config render below, and virtual-currency balances (best-effort) show in a "Balances" group. Theming and copy (localizations) come entirely from the dashboard config; pass a themeOverride to force a mode/accent per mount. After every mutation the payload is re-fetched so the cards reflect the new state.

await presentCustomerCenter({
  appUserId: 'user-123',          // defaults to the tracked Purchases.appUserId
  themeOverride: { mode: 'dark' },
});

Inline embed

To embed the Customer Center inside your own page instead of a modal, use renderCustomerCenterInline — same options, no overlay, returns a controller:

import { renderCustomerCenterInline } from '@billingbear/purchases-js';

const controller = renderCustomerCenterInline(document.getElementById('cc')!, {
  onClose: () => hide(),
  onAction: (outcome, subId) => analytics.track('cc_action', { outcome, subId }),
});
// controller.destroy()

The lower-level building blocks are also exported: Purchases.getCustomerCenter() returns the raw payload, and renderCustomerCenter(container, data, options) renders the DOM with your own transport if you want to drive it headlessly.

Rendered components

The SDK also ships real, embeddable React components — the same renderer the hosted /p/{slug} page uses, packaged for any web app (and react-native-web / Electron, since they use only DOM + inline styles — no Tailwind, no CSS file, no Next.js). react is an optional peer dependency: install it only if you use these components.

npm install react react-dom        # peer deps for the components
npm install lottie-web             # only if you use a Lottie background

<Paywall>

Renders the full PaywallDocument with every background (solid / gradient / image / video / lottie) and component block (features, social proof, "how it works" timeline, tier tabs, free-trial switch, comparison table, products, urgency countdown), and wires the purchase button to Stripe checkout.

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

// Fetch + render by identifier (calls Purchases.getPaywall under the hood):
<Paywall identifier="main" projectName="Acme" onClose={() => setOpen(false)} />

// Or render a document you already have, with real Stripe prices merged in:
<Paywall
  document={doc}
  prices={[
    { productIdentifier: 'yearly', priceId: 'price_123', formattedPrice: '$79.99', recurringInterval: 'year' },
  ]}
  collectEmail
  onCheckout={async (productId, email) => { /* your checkout */ }}
/>

The default checkout opens a Stripe Checkout session via Purchases.startWebCheckout using the matched price's priceId (or the product identifier itself when no prices are supplied). Pass onCheckout to override. Video backgrounds render with a native <video>; Lottie backgrounds load lottie-web lazily (poster fallback if it isn't installed).

<CustomerCenter>

A drop-in, themeable subscription-management screen for end users (RevenueCat Customer Center parity) built from the data the SDK fetches. Renders each subscription with its server-driven action buttons and wires them to the SDK: manage-in-store, cancel (with optional survey + win-back offer + retention step), reactivate, request-refund and change-plan, plus support links and virtual-currency balances.

import { CustomerCenter } from '@billingbear/purchases-js';

<CustomerCenter onClose={() => setOpen(false)} />

Both components are framework-agnostic React — drop them in a modal, a route, or a side panel.

RevenueCat migration

Swap the import and most code keeps working:

// - import Purchases from '@revenuecat/purchases-js';
import Purchases from '@billingbear/purchases-js/revenuecat';

await Purchases.configure({ apiKey: 'pk_live_...', appUserID: 'user-123' });
const info = await Purchases.getCustomerInfo();
if (info.entitlements.active['pro']) unlockApp();

// Web purchase (no store package on the web):
await Purchases.purchaseWeb({ priceId: 'price_123' });

SSR notes

All localStorage access is guarded for typeof window === 'undefined' and falls back to an in-memory store, so calling configure / getCustomerInfo during server render is safe. startWebCheckout skips the window.location redirect on the server and returns the session instead.

License

MIT

Electron / desktop

The SDK runs in three JS contexts with no code change:

  • Website / Electron renderer (Chromium): works out of the box — localStorage persists the anonymous user, startWebCheckout() redirects in-page.
  • Electron main process (Node): there's no window/localStorage, so inject a persistent store and use the desktop checkout flow (open the system browser, then poll).
  • Next.js / SSR: safe to import; storage degrades to in-memory on the server.
// Electron MAIN process
import { app, shell } from 'electron';
import Store from 'electron-store';
import { Purchases, setStorageAdapter } from '@billingbear/purchases-js';

const store = new Store();
setStorageAdapter({
  getItem: (k) => (store.get(k) as string) ?? null,
  setItem: (k, v) => store.set(k, v),
  removeItem: (k) => store.delete(k),
});

await Purchases.configure({ apiKey: 'pk_live_...' });

// Desktop checkout: open the browser, then wait for the purchase to land.
const { url, sessionId } = await Purchases.getCheckoutUrl({ priceId: 'price_123' });
await shell.openExternal(url);
const info = await Purchases.awaitCheckout(sessionId);   // polls until complete/expired
if (info.isPremium) unlockPremium();

You can also pass the adapter inline: Purchases.configure({ apiKey, storage: myAdapter }). For a custom-protocol deep link back into the app, pass successUrl: 'myapp://checkout/done' to getCheckoutUrl.