{"_id":"@billingbear/purchases-js","_rev":"4-038f54c7bb41db79f2df9eb0b41980e0","name":"@billingbear/purchases-js","dist-tags":{"latest":"0.7.0"},"versions":{"0.1.0":{"name":"@billingbear/purchases-js","version":"0.1.0","keywords":["web","subscriptions","stripe","checkout","entitlements","paywall","billingbear"],"author":{"name":"BillingBear"},"license":"MIT","_id":"@billingbear/purchases-js@0.1.0","maintainers":[{"name":"spicy_sparks","email":"admin@spicysparks.com"},{"name":"marf95","email":"fontana.marco@hotmail.com"}],"dist":{"shasum":"f9797f5191c508a262377a7a3dba753256a775a4","tarball":"https://registry.npmjs.org/@billingbear/purchases-js/-/purchases-js-0.1.0.tgz","fileCount":21,"integrity":"sha512-mDvorMwbRC/G147GXE3FE7ptF0ZeKffnfwliGUrc2ivKZgVvvRPlnwzC0bwsmSPtyMT1EawHdO086aZyM8HQug==","signatures":[{"sig":"MEYCIQC01gAQRY2N4xGll5F94iK/+DCfsGEPx5IMDo8Vo/HD7wIhAP3qGr/oUDBPcd7zrW7LF87TKFKZt+bMjnEXamimWPZe","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":71504},"jest":{"roots":["<rootDir>/src"],"preset":"ts-jest","testMatch":["**/__tests__/**/*.test.ts"],"testEnvironment":"node"},"main":"lib/index.js","types":"lib/index.d.ts","exports":{".":{"types":"./lib/index.d.ts","default":"./lib/index.js"},"./revenuecat":{"types":"./lib/revenuecat.d.ts","default":"./lib/revenuecat.js"}},"gitHead":"eee1141e1346f234e977c5ee2965b3605155d66d","scripts":{"test":"jest","build":"tsc","prepare":"tsc"},"_npmUser":{"name":"spicy_sparks","email":"admin@spicysparks.com"},"_npmVersion":"11.12.1","description":"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).","directories":{},"_nodeVersion":"25.9.0","_hasShrinkwrap":false,"devDependencies":{"jest":"^29.7.0","ts-jest":"^29.1.0","typescript":"^5.0.0","@types/jest":"^29.5.0"},"_npmOperationalInternal":{"tmp":"tmp/purchases-js_0.1.0_1780579446547_0.8782639875202649","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@billingbear/purchases-js","version":"0.2.0","keywords":["web","subscriptions","stripe","checkout","entitlements","paywall","billingbear"],"author":{"name":"BillingBear"},"license":"MIT","_id":"@billingbear/purchases-js@0.2.0","maintainers":[{"name":"spicy_sparks","email":"admin@spicysparks.com"},{"name":"marf95","email":"fontana.marco@hotmail.com"}],"dist":{"shasum":"5fdcdcea9a5d5889168fe4fd8dcfb5994817efc8","tarball":"https://registry.npmjs.org/@billingbear/purchases-js/-/purchases-js-0.2.0.tgz","fileCount":22,"integrity":"sha512-2w6MetO1ga/x2GlilnOd2BxcsXkFQg6ig02b3gHAFFdYjS5kfB9zSsaRTXflF6JSaY+FvehKHL1a5bcDqeUwdg==","signatures":[{"sig":"MEYCIQDQTmVdWiCS7uyVKVhYWUvHq33YtNpPq7Ry2aywGJHkkQIhAKneion6EmWhptWoQDvKBOsVg4sgBjqoG9uHC+7esOPR","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":87092},"jest":{"roots":["<rootDir>/src"],"preset":"ts-jest","testMatch":["**/__tests__/**/*.test.ts"],"testEnvironment":"node"},"main":"lib/index.js","types":"lib/index.d.ts","exports":{".":{"types":"./lib/index.d.ts","default":"./lib/index.js"},"./revenuecat":{"types":"./lib/revenuecat.d.ts","default":"./lib/revenuecat.js"}},"gitHead":"509e7bc339aad43e72bb977faebb405f394e68de","scripts":{"test":"jest","build":"tsc","prepare":"tsc"},"_npmUser":{"name":"spicy_sparks","email":"admin@spicysparks.com"},"_npmVersion":"11.12.1","description":"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).","directories":{},"_nodeVersion":"25.9.0","_hasShrinkwrap":false,"devDependencies":{"jest":"^29.7.0","ts-jest":"^29.1.0","typescript":"^5.0.0","@types/jest":"^29.5.0"},"_npmOperationalInternal":{"tmp":"tmp/purchases-js_0.2.0_1780579780952_0.3307120682888631","host":"s3://npm-registry-packages-npm-production"}},"0.4.0":{"name":"@billingbear/purchases-js","version":"0.4.0","keywords":["web","subscriptions","stripe","checkout","entitlements","paywall","billingbear"],"author":{"name":"BillingBear"},"license":"MIT","_id":"@billingbear/purchases-js@0.4.0","maintainers":[{"name":"spicy_sparks","email":"admin@spicysparks.com"},{"name":"marf95","email":"fontana.marco@hotmail.com"}],"dist":{"shasum":"c61097b9b9dc267c839de8bc4836f75a64139aad","tarball":"https://registry.npmjs.org/@billingbear/purchases-js/-/purchases-js-0.4.0.tgz","fileCount":25,"integrity":"sha512-XQOVsfpJvx2xfgRJdG5Bms+jJ5sELzOGUbEL3PrrCI1YJL83doLITUr0PbyCtbQ10gO/56p7RBayEMiIqaL/iQ==","signatures":[{"sig":"MEUCIQCNHiPQSZkLmq8Rk3TltiOxhlepukMRLp9MVB65GRUIXwIgRXfG3QVa+t80WVeb0oqbOsmoOszF5SL4R33VVvzSxlU=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":136898},"jest":{"roots":["<rootDir>/src"],"preset":"ts-jest","testMatch":["**/__tests__/**/*.test.ts"],"testEnvironment":"node"},"main":"lib/index.js","types":"lib/index.d.ts","exports":{".":{"types":"./lib/index.d.ts","default":"./lib/index.js"},"./revenuecat":{"types":"./lib/revenuecat.d.ts","default":"./lib/revenuecat.js"}},"gitHead":"4c1df0b86df0d138cdfa571cac94c062611033be","scripts":{"test":"jest","build":"tsc","prepare":"tsc"},"_npmUser":{"name":"spicy_sparks","email":"admin@spicysparks.com"},"_npmVersion":"11.12.1","description":"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).","directories":{},"_nodeVersion":"25.9.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"jest":"^29.7.0","ts-jest":"^29.1.0","typescript":"^5.0.0","@types/jest":"^29.5.0"},"_npmOperationalInternal":{"tmp":"tmp/purchases-js_0.4.0_1780682398563_0.7299162027894861","host":"s3://npm-registry-packages-npm-production"}},"0.7.0":{"name":"@billingbear/purchases-js","version":"0.7.0","description":"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).","main":"lib/index.js","types":"lib/index.d.ts","scripts":{"build":"tsc","prepare":"tsc","test":"jest"},"jest":{"preset":"ts-jest","testEnvironment":"node","roots":["<rootDir>/src"],"testMatch":["**/__tests__/**/*.test.ts","**/__tests__/**/*.test.tsx"],"moduleNameMapper":{"^lottie-web/build/player/lottie_light$":"<rootDir>/src/__tests__/__mocks__/lottie-web.ts"}},"keywords":["web","subscriptions","stripe","checkout","entitlements","paywall","customer-center","react","billingbear"],"author":{"name":"BillingBear"},"license":"MIT","publishConfig":{"access":"public"},"peerDependencies":{"react":">=17.0.0","lottie-web":"^5.12.0"},"peerDependenciesMeta":{"react":{"optional":true},"lottie-web":{"optional":true}},"optionalDependencies":{"lottie-web":"^5.12.0"},"devDependencies":{"@testing-library/react":"^14.3.1","@types/jest":"^29.5.0","@types/react":"^18.3.0","@types/react-dom":"^18.3.0","jest":"^29.7.0","jest-environment-jsdom":"^29.7.0","lottie-web":"^5.12.2","react":"^18.3.1","react-dom":"^18.3.1","ts-jest":"^29.1.0","typescript":"^5.0.0"},"exports":{".":{"types":"./lib/index.d.ts","default":"./lib/index.js"},"./revenuecat":{"types":"./lib/revenuecat.d.ts","default":"./lib/revenuecat.js"}},"gitHead":"2ffce25608ff82430117b59cfbc794ee3354c961","_id":"@billingbear/purchases-js@0.7.0","_nodeVersion":"25.9.0","_npmVersion":"11.12.1","dist":{"integrity":"sha512-TpqbdKyF6d8C3OAkTPINQXQCiLEAYzysVZFcq7A1+6FROR0NMciKE3VbvKpUWfog5JZyz54QsIfi4XsaGrByYA==","shasum":"18e3f48488e901d2e1c941a484b8c7166096db8f","tarball":"https://registry.npmjs.org/@billingbear/purchases-js/-/purchases-js-0.7.0.tgz","fileCount":111,"unpackedSize":3160776,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQD6lb4Unk4Obi4dZcjCHaCkCoOWbtee1F6hiHrqpE31YwIgGnKijwhMarX9ybsNO/XGQt/iUgmmUUlB6RruoetNCuc="}]},"_npmUser":{"name":"spicy_sparks","email":"admin@spicysparks.com"},"directories":{},"maintainers":[{"name":"spicy_sparks","email":"admin@spicysparks.com"},{"name":"marf95","email":"fontana.marco@hotmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/purchases-js_0.7.0_1780993958632_0.599170885329706"},"_hasShrinkwrap":false}},"time":{"created":"2026-06-04T13:24:06.334Z","modified":"2026-06-09T08:32:38.957Z","0.1.0":"2026-06-04T13:24:06.704Z","0.2.0":"2026-06-04T13:29:41.093Z","0.4.0":"2026-06-05T17:59:58.690Z","0.7.0":"2026-06-09T08:32:38.834Z"},"author":{"name":"BillingBear"},"license":"MIT","keywords":["web","subscriptions","stripe","checkout","entitlements","paywall","customer-center","react","billingbear"],"description":"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).","maintainers":[{"name":"spicy_sparks","email":"admin@spicysparks.com"},{"name":"marf95","email":"fontana.marco@hotmail.com"}],"readme":"# @billingbear/purchases-js\n\nWeb subscription SDK for [BillingBear](https://billingbear.io) — gate premium\nfeatures in any web app and take payment through Stripe Checkout. Same REST\nbackend as the React Native SDK (`@billingbear/purchases`), but built on plain\n`fetch` + `localStorage` with SSR guards. No `react-native-iap`, no\n`AsyncStorage`, no native modules — it runs in the browser and during\nNext.js / SSR server render without throwing.\n\n```bash\nnpm install @billingbear/purchases-js\n```\n\n## Quick start\n\n```typescript\nimport { Purchases } from '@billingbear/purchases-js';\n\n// Call once at app startup. Resolves (and persists) an anonymous appUserId\n// in localStorage if you don't pass one.\nconst info = await Purchases.configure({ apiKey: 'pk_live_...' });\nif (info.isPremium) unlockApp();\n```\n\n## Gating premium\n\n```typescript\nif (await Purchases.isPremium()) { /* ... */ }\n\n// Or check a specific entitlement identifier:\nif (await Purchases.checkEntitlement('pro')) { /* ... */ }\n\n// Latest snapshot (re-fetches entitlements):\nconst info = await Purchases.getCustomerInfo();\n```\n\n## Stripe web checkout\n\n```typescript\n// Creates a Checkout session and redirects the browser to Stripe.\n// successUrl / cancelUrl default to the current page.\nawait Purchases.startWebCheckout({ priceId: 'price_123' });\n\n// Or create the session yourself and control the redirect:\nconst { url } = await Purchases.createCheckoutSession({\n  priceId: 'price_123',\n  successUrl: 'https://app.example.com/welcome',\n  cancelUrl: 'https://app.example.com/pricing',\n});\nwindow.open(url, '_blank');\n```\n\n## Offerings → checkout\n\nFetch your offerings and check out the right package without hard-coding the\nStripe price id. For the `stripe` provider a product's `productId` IS the Stripe\nPrice id; the `stripePriceId` helper pulls it out for you:\n\n```typescript\nimport { Purchases, stripePriceId } from '@billingbear/purchases-js';\n\nconst { offerings } = await Purchases.getOfferings();\nconst current = offerings.find((o) => o.isCurrent) ?? offerings[0];\n\nconst monthly = current?.packages.find((p) => p.identifier === 'monthly');\nconst priceId = stripePriceId(monthly);          // -> \"price_123\" | null\nif (priceId) {\n  await Purchases.startWebCheckout({ priceId }); // redirect to Stripe Checkout\n}\n```\n\nEach `OfferingProduct` also carries display price metadata when the backend has\nit (`priceString`, `price`, `currencyCode`, `billingPeriod`, `trialPeriod`,\n`localizedPrices`) so you can render a paywall with the real price.\n\n## Attribution\n\nThe same attribution / MMP setters as the React Native SDK are available — one\nline each, writing the reserved `$`-prefixed subscriber attributes:\n\n```typescript\nawait Purchases.setAdjustID('adjust-id');\nawait Purchases.setAppsflyerID('appsflyer-id');\nawait Purchases.setCampaign('spring_sale');\n// also: setFBAnonymousID, setMparticleID, setOnesignalID, setAirshipChannelID,\n// setCleverTapID, setMixpanelDistinctID, setFirebaseAppInstanceID,\n// setMediaSource, setAdGroup, setAd, setKeyword, setCreative\n```\n\n## Identity\n\n```typescript\n// Link the anonymous user to your own user id (merges entitlements server-side).\nawait Purchases.login('your-user-id');\n\n// Reset to a fresh anonymous user.\nawait Purchases.logout();\n```\n\n## Paywalls\n\nThe SDK ships a **built-in paywall renderer** — compiled-to-vanilla DOM with\ninline styles, **zero framework dependencies** (no React, no Tailwind, no CSS to\nimport). Fetch a paywall and present it as a modal:\n\n```typescript\nimport { Purchases, presentPaywall } from '@billingbear/purchases-js';\n\nconst paywall = await Purchases.getPaywall('main');\nif (paywall) {\n  const result = await presentPaywall({\n    paywall,\n    variables: { 'user.firstName': 'Marco' }, // fills {{ user.firstName }} in the copy\n    // The chosen product identifier IS the Stripe Price id by default; map it\n    // explicitly if your dashboard product identifiers differ:\n    // resolvePriceId: (productId) => priceMap[productId],\n  });\n  // { outcome: 'purchased' | 'dismissed' | 'restored', productId? }\n  if (result.outcome === 'purchased') unlock();\n}\n```\n\n`presentPaywall` mounts a fullscreen overlay (high z-index, body scroll-lock),\nrenders all five layouts (`vertical-features`, `horizontal-products`,\n`comparison-table`, `minimal`, `hero-image`) with solid/gradient/image/video/\nlottie backgrounds, the trial badge, urgency countdown, social proof, and the\noptional survey overlay — faithful to the dashboard's hosted page.\n\n#### Lottie backgrounds (optional)\n\nPaywalls with a **lottie background** animate via [`lottie-web`](https://www.npmjs.com/package/lottie-web),\nwhich the SDK loads **on-demand** (a guarded dynamic import) so it stays a\nzero-required-dependency package. `lottie-web` is declared as an **optional\ndependency / optional peer** — install it only if you ship lottie paywalls:\n\n```bash\nnpm install lottie-web\n```\n\nWhen the module isn't present (or the animation fails to load), the renderer\nfalls back to the paywall's configured **poster image**, so a lottie paywall\nalways looks intentional without the extra dependency.\n\n> **Redirect note:** the CTA calls `Purchases.startWebCheckout`, which redirects\n> the browser to Stripe. When that happens the page navigates away, so the\n> promise may never resolve — the user lands on your `successUrl` instead. Pass\n> `successUrl` / `cancelUrl` to control where they return.\n\n### Inline embed\n\nTo embed the paywall inside your own page instead of a modal, use\n`renderPaywall` — same options, no overlay, outcome delivered via `onOutcome`:\n\n```typescript\nimport { renderPaywall } from '@billingbear/purchases-js';\n\nconst controller = renderPaywall(document.getElementById('paywall')!, {\n  paywall,\n  onOutcome: (r) => { if (r.outcome === 'purchased') unlock(); },\n});\n// controller.dismiss() / controller.destroy()\n```\n\n### Events\n\nBoth entry points emit the standard paywall event stream (identical to the\nReact Native SDK) — subscribe for funnel analytics:\n\n```typescript\nimport { addPaywallEventListener } from '@billingbear/purchases-js';\n\nconst off = addPaywallEventListener((e) => analytics.track(e.type, e));\n// paywall_impression | paywall_dismiss | paywall_purchase_started/completed/failed\n// | paywall_custom_action | paywall_survey_response\n```\n\nExposures are recorded automatically: `viewed` at mount, `dismissed` / `purchased`\nat the outcome. Survey answers also write the `$survey_<id>` subscriber attribute\nfor audience targeting.\n\n### Headless / custom renderer\n\n`getPaywall` tags the request as `platform: 'web'` and passes the current\n`appUserId` for A/B variant assignment, recording a best-effort `viewed`\nexposure. If you render the paywall yourself, opt out and record the funnel\nmanually:\n\n```typescript\nconst paywall = await Purchases.getPaywall('main', { recordExposure: false });\nif (paywall) {\n  // …your renderer…\n  Purchases.recordPaywallExposure(paywall, 'viewed');\n  Purchases.recordPaywallExposure(paywall, 'dismissed'); // or 'purchased'\n}\n```\n\n`recordPaywallExposure` is fire-and-forget — it never awaits or throws, and\nforwards the paywall's assigned experiment id + variant automatically. The\nlow-level building block `renderPaywallDocument(container, document, options)`\nis also exported if you want the DOM without the modal/telemetry shell.\n\n## Customer Center\n\nA drop-in, themeable **in-app subscription-management** screen for your end users\n— the web counterpart of the React Native `<CustomerCenter />`. Compiled-to-vanilla\nDOM with inline styles, **zero framework dependencies**. Present it as a modal:\n\n```typescript\nimport { presentCustomerCenter } from '@billingbear/purchases-js';\n\nconst result = await presentCustomerCenter();\n// { outcome: 'closed', lastAction?: { outcome, subscriptionId } }\n```\n\nOn open it loads the Customer Center payload (subscriptions + the themeable\nconfig) for the tracked `appUserId`, renders each subscription with its\nserver-driven action buttons, and wires them to the SDK:\n\n- **manage_in_store** → opens the store / billing-portal URL in a new tab\n- **cancel** / **cancel_at_period_end** → optional cancellation survey, then a\n  win-back **offer** (managed Stripe/Paddle subs) or a **retention** prompt,\n  before the cancel POST\n- **reactivate** → re-enables a period-end cancellation\n- **request_refund** → confirm, then request a refund\n- **change_plan** → switches to a target product, or falls back to support\n\nSupport links, custom sections and deep links from the config render below, and\nvirtual-currency balances (best-effort) show in a \"Balances\" group. Theming and\ncopy (localizations) come entirely from the dashboard config; pass a\n`themeOverride` to force a mode/accent per mount. After every mutation the\npayload is re-fetched so the cards reflect the new state.\n\n```typescript\nawait presentCustomerCenter({\n  appUserId: 'user-123',          // defaults to the tracked Purchases.appUserId\n  themeOverride: { mode: 'dark' },\n});\n```\n\n### Inline embed\n\nTo embed the Customer Center inside your own page instead of a modal, use\n`renderCustomerCenterInline` — same options, no overlay, returns a controller:\n\n```typescript\nimport { renderCustomerCenterInline } from '@billingbear/purchases-js';\n\nconst controller = renderCustomerCenterInline(document.getElementById('cc')!, {\n  onClose: () => hide(),\n  onAction: (outcome, subId) => analytics.track('cc_action', { outcome, subId }),\n});\n// controller.destroy()\n```\n\nThe lower-level building blocks are also exported: `Purchases.getCustomerCenter()`\nreturns the raw payload, and `renderCustomerCenter(container, data, options)`\nrenders the DOM with your own transport if you want to drive it headlessly.\n\n## Rendered components\n\nThe SDK also ships **real, embeddable React components** — the same renderer the\nhosted `/p/{slug}` page uses, packaged for any web app (and `react-native-web` /\nElectron, since they use only DOM + inline styles — no Tailwind, no CSS file, no\nNext.js). `react` is an **optional** peer dependency: install it only if you use\nthese components.\n\n```bash\nnpm install react react-dom        # peer deps for the components\nnpm install lottie-web             # only if you use a Lottie background\n```\n\n### `<Paywall>`\n\nRenders the full `PaywallDocument` with every background (solid / gradient /\nimage / video / lottie) and component block (features, social proof, \"how it\nworks\" timeline, tier tabs, free-trial switch, comparison table, products,\nurgency countdown), and wires the purchase button to Stripe checkout.\n\n```tsx\nimport { Paywall } from '@billingbear/purchases-js';\n\n// Fetch + render by identifier (calls Purchases.getPaywall under the hood):\n<Paywall identifier=\"main\" projectName=\"Acme\" onClose={() => setOpen(false)} />\n\n// Or render a document you already have, with real Stripe prices merged in:\n<Paywall\n  document={doc}\n  prices={[\n    { productIdentifier: 'yearly', priceId: 'price_123', formattedPrice: '$79.99', recurringInterval: 'year' },\n  ]}\n  collectEmail\n  onCheckout={async (productId, email) => { /* your checkout */ }}\n/>\n```\n\nThe default checkout opens a Stripe Checkout session via\n`Purchases.startWebCheckout` using the matched price's `priceId` (or the product\nidentifier itself when no `prices` are supplied). Pass `onCheckout` to override.\nVideo backgrounds render with a native `<video>`; Lottie backgrounds load\n`lottie-web` lazily (poster fallback if it isn't installed).\n\n### `<CustomerCenter>`\n\nA drop-in, themeable subscription-management screen for end users (RevenueCat\nCustomer Center parity) built from the data the SDK fetches. Renders each\nsubscription with its server-driven action buttons and wires them to the SDK:\nmanage-in-store, cancel (with optional survey + win-back offer + retention\nstep), reactivate, request-refund and change-plan, plus support links and\nvirtual-currency balances.\n\n```tsx\nimport { CustomerCenter } from '@billingbear/purchases-js';\n\n<CustomerCenter onClose={() => setOpen(false)} />\n```\n\nBoth components are framework-agnostic React — drop them in a modal, a route, or\na side panel.\n\n## RevenueCat migration\n\nSwap the import and most code keeps working:\n\n```typescript\n// - import Purchases from '@revenuecat/purchases-js';\nimport Purchases from '@billingbear/purchases-js/revenuecat';\n\nawait Purchases.configure({ apiKey: 'pk_live_...', appUserID: 'user-123' });\nconst info = await Purchases.getCustomerInfo();\nif (info.entitlements.active['pro']) unlockApp();\n\n// Web purchase (no store package on the web):\nawait Purchases.purchaseWeb({ priceId: 'price_123' });\n```\n\n## SSR notes\n\nAll `localStorage` access is guarded for `typeof window === 'undefined'` and\nfalls back to an in-memory store, so calling `configure` / `getCustomerInfo`\nduring server render is safe. `startWebCheckout` skips the `window.location`\nredirect on the server and returns the session instead.\n\n## License\n\nMIT\n\n## Electron / desktop\n\nThe SDK runs in three JS contexts with no code change:\n\n- **Website / Electron renderer (Chromium):** works out of the box — `localStorage` persists the anonymous user, `startWebCheckout()` redirects in-page.\n- **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).\n- **Next.js / SSR:** safe to import; storage degrades to in-memory on the server.\n\n```ts\n// Electron MAIN process\nimport { app, shell } from 'electron';\nimport Store from 'electron-store';\nimport { Purchases, setStorageAdapter } from '@billingbear/purchases-js';\n\nconst store = new Store();\nsetStorageAdapter({\n  getItem: (k) => (store.get(k) as string) ?? null,\n  setItem: (k, v) => store.set(k, v),\n  removeItem: (k) => store.delete(k),\n});\n\nawait Purchases.configure({ apiKey: 'pk_live_...' });\n\n// Desktop checkout: open the browser, then wait for the purchase to land.\nconst { url, sessionId } = await Purchases.getCheckoutUrl({ priceId: 'price_123' });\nawait shell.openExternal(url);\nconst info = await Purchases.awaitCheckout(sessionId);   // polls until complete/expired\nif (info.isPremium) unlockPremium();\n```\n\nYou can also pass the adapter inline: `Purchases.configure({ apiKey, storage: myAdapter })`.\nFor a custom-protocol deep link back into the app, pass `successUrl: 'myapp://checkout/done'` to `getCheckoutUrl`.\n","readmeFilename":"README.md"}