{"_id":"@codesyncr/cashier-cloud","name":"@codesyncr/cashier-cloud","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@codesyncr/cashier-cloud","version":"0.1.0","description":"Cashier Cloud web SDK — subscriptions and entitlements across Apple, Google and Stripe, from the browser.","type":"module","main":"./dist/index.cjs","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"scripts":{"build":"tsup src/index.ts --format esm,cjs --dts --clean","type-check":"tsc --noEmit","test":"node --test test/*.test.mjs","dev":"tsup src/index.ts --format esm,cjs --dts --watch"},"author":{"name":"CodeSyncr"},"license":"MIT","engines":{"node":">=18"},"devDependencies":{"@types/node":"^26.5.0","tsup":"^8.0.0","typescript":"^7.0.2"},"module":"./dist/index.js","keywords":["cashier","cashier-cloud","subscriptions","entitlements","paywall","in-app-purchase","storekit","play-billing","stripe"],"publishConfig":{"access":"public"},"_id":"@codesyncr/cashier-cloud@0.1.0","gitHead":"3cbe8de879ad4254c1f894ca7624bad900139320","_nodeVersion":"22.22.3","_npmVersion":"10.9.8","dist":{"integrity":"sha512-gLOOPBoxgXj8N+3xByamObgvfDKzJvuUnZvvSO1p9Q03KEpGUcqNFkpzcU5YYZTwLKV/9OeuoEBZr9VLsQeezA==","shasum":"a366195ad334bf64507f1d67473fc174775c0b4c","tarball":"https://registry.npmjs.org/@codesyncr/cashier-cloud/-/cashier-cloud-0.1.0.tgz","fileCount":6,"unpackedSize":77832,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQD98PtmqJuVqYXW6RwM+d4HGkX5YwhnEdaCPgznbyUudgIhAI1eoiFPJ3idc8P4BHYwheu11wFN/AH0Zc+FG1wQ4IvG"}]},"_npmUser":{"name":"codesyncr","email":"yashkumar12125@gmail.com"},"directories":{},"maintainers":[{"name":"codesyncr","email":"yashkumar12125@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/cashier-cloud_0.1.0_1788886084046_0.12395755715945111"},"_hasShrinkwrap":false}},"time":{"created":"2026-09-08T16:48:03.856Z","0.1.0":"2026-09-08T16:48:04.221Z","modified":"2026-09-08T16:48:04.489Z"},"maintainers":[{"name":"codesyncr","email":"yashkumar12125@gmail.com"}],"description":"Cashier Cloud web SDK — subscriptions and entitlements across Apple, Google and Stripe, from the browser.","keywords":["cashier","cashier-cloud","subscriptions","entitlements","paywall","in-app-purchase","storekit","play-billing","stripe"],"author":{"name":"CodeSyncr"},"license":"MIT","readme":"# @codesyncr/cashier-cloud\n\nThe web SDK for **Cashier Cloud** — one subscription state across Apple, Google\nand Stripe, read from the browser.\n\nThis package is the client and nothing else. It carries a **public** key, so\neverything it can do is safe to ship in a page: read what the paywall should\nsell, read what the current subscriber owns, and start a checkout. Receipt\nverification, metering and the entitlement ledger run on Cloud, because a\ncredential that ships to a browser is not a credential.\n\n```bash\nnpm install @codesyncr/cashier-cloud\n```\n\nWorks in any browser bundler and in Node 18+ (SSR-safe — it falls back to\nin-memory storage where `localStorage` is absent).\n\n---\n\n## Quick start\n\n```typescript\nimport { CashierCloud } from '@codesyncr/cashier-cloud'\n\nconst cashier = new CashierCloud({ apiKey: 'cshr_web_…' })\n\n// 1. What should the paywall present?\nconst { packages } = await cashier.offerings()\n\n// 2. Send them to pay.\nawait cashier.purchase(packages[0], {\n  successUrl: 'https://example.com/welcome',\n  cancelUrl: 'https://example.com/pricing',\n})\n\n// 3. On the success page, refetch and unlock.\nconst info = await cashier.customerInfo({ force: true })\nif (info.entitlements.premium?.active) unlockPro()\n```\n\n---\n\n## Configuration\n\nConfigure once for the page:\n\n```typescript\nimport { CashierCloud } from '@codesyncr/cashier-cloud'\n\nCashierCloud.configure({ apiKey: 'cshr_web_…', appUserId: user?.id })\n\n// anywhere else\nconst cashier = CashierCloud.getSharedInstance()\n```\n\n`new CashierCloud(…)` works too, but identity and the CustomerInfo cache live on\nthe instance — two of them would disagree about who is signed in.\n\n| Option | Type | Default |\n|---|---|---|\n| `apiKey` | `string` | — **required** |\n| `appUserId` | `string` | a persisted anonymous id |\n| `headers` | `Record<string, string>` | `{}` |\n| `fetch` | `FetchLike` | global `fetch` |\n| `storage` | `Storage` | `localStorage`, else memory |\n\n**Only ever pass the public key.** It is scoped to reading this subscriber and\nstarting a checkout. The secret `sk_…` key can grant entitlements and belongs on\nyour own server, never in a page.\n\n**There is no `baseURL`.** The API origin is fixed at `https://nimbusgo.space`,\nbecause the public key is scoped to that host — a page that could repoint the\nSDK could hand the key, and every subscriber read made with it, to whoever owns\nthe other host. Tests and proxies inject `fetch`, which is narrower and\nexplicit.\n\n---\n\n## Identity\n\nA person often buys **before** they sign up, and that purchase has to survive the\ngap. So a subscriber always has an id: either yours, or an anonymous one the SDK\ngenerates and persists.\n\n```typescript\nconst cashier = new CashierCloud({ apiKey })\ncashier.appUserId    // \"$anon:9f2c…\"  — generated once, kept in localStorage\ncashier.isAnonymous  // true\n\nawait cashier.purchase(pkg)      // bought while anonymous\nawait cashier.logIn(user.id)     // merges that purchase into the real account\ncashier.isAnonymous              // false\n```\n\n### `logIn(appUserId)`\n\nAliases the current subject onto your user id and returns the merged\n`CustomerInfo`. Idempotent — calling it again for the same user is a no-op, not\na second alias. Once the merge lands the anonymous id is forgotten, because\nkeeping it would resurrect an empty subscriber on the next visit.\n\nCall it as soon as you know who someone is — after sign-in, and on every app\nload for an already-signed-in user.\n\n### `logOut()`\n\nRotates to a fresh anonymous subscriber. Entitlements stay with the account they\nwere bought under; this only changes who *this browser* is reading as. Call it\nwhen your own session ends.\n\n---\n\n## Reading subscriber state\n\n### `customerInfo(options?)`\n\nThe one aggregate answering *what does this person have right now*.\n\n```typescript\nconst info = await cashier.customerInfo()\n\ninfo.entitlements.premium?.active      // boolean\ninfo.entitlements.premium?.willRenew   // false once cancelled\ninfo.entitlements.premium?.periodType  // trial | intro | normal | promotional | grace\ninfo.entitlements.premium?.expiresAt   // Date | null  (null = never expires)\ninfo.activeEntitlementIds              // [\"premium\"]\ninfo.activeProductIds                  // [\"pro_monthly\"]\ninfo.activeSubscriptions               // store subscriptions currently granting\ninfo.latestExpiresAt                   // Date | null\n```\n\nCached after the first call. Pass `{ force: true }` to refetch — do that when\nyou land on your success URL, and after `logIn`.\n\nTwo behaviours are worth knowing because they will otherwise look like bugs:\n\n- **A cancelled subscription is still active.** Cancelling sets `willRenew` to\n  `false`; access continues until `expiresAt`. Do not gate on `willRenew`.\n- **A failed payment does not revoke immediately.** The entitlement enters a\n  grace period — `active` stays `true` and `periodType` becomes `'grace'` — while\n  the store retries. If you want to nudge the customer, that flag is the signal.\n\n### `isEntitled(id)`\n\nA synchronous read of the cached `CustomerInfo`. Convenient for rendering:\n\n```typescript\nif (cashier.isEntitled('premium')) showProBadge()\n```\n\nIt is only as fresh as the last fetch, and it runs in a browser the user\ncontrols. **Never gate anything that matters on it** — have your own server ask\nCloud instead.\n\n### `onCustomerInfoUpdate(listener)`\n\nFires on every refresh. Returns an unsubscribe function.\n\n```typescript\nconst off = cashier.onCustomerInfoUpdate((info) => {\n  setPro(info.entitlements.premium?.active ?? false)\n})\n```\n\n### `current` and `invalidate()`\n\n`current` is the cached `CustomerInfo` or `null` before the first fetch.\n`invalidate()` drops the cache so the next read refetches.\n\n---\n\n## The paywall\n\n### `offerings()`\n\nReturns the current offering with every package's product resolved, so a paywall\nasks *what should I sell right now* instead of hard-coding product ids — which\nmeans you can reprice or swap plans from the Cloud dashboard without shipping\na release.\n\n```typescript\nconst { currentOffering, packages, metadata } = await cashier.offerings()\n\npackages.forEach((pkg) => {\n  pkg.id                    // \"monthly\" | \"annual\"\n  pkg.product.id            // \"pro_monthly\"\n  pkg.product.name          // \"Pro\"\n  pkg.product.amount        // 149900 — smallest unit, always\n  pkg.product.currency      // \"INR\"\n  pkg.product.periodMonths  // 1  (0 = lifetime / non-renewing)\n  pkg.product.trialDays     // 7\n  pkg.product.entitlements  // [\"premium\"]\n})\n```\n\n`packages()` is a shortcut when you only need the list. For the common cases,\nthe offering is addressable by billing period rather than by index — derived\nfrom each product's `periodMonths`, so a package called `starter` still resolves:\n\n```typescript\nconst { monthly, annual, lifetime } = await cashier.offerings()\n```\n\nPass a currency when you already know where the visitor is, rather than quoting\ndollars first and correcting yourself:\n\n```typescript\nawait cashier.offerings({ currency: 'EUR' })\n```\n\n> **Amounts are in the smallest currency unit** — paise, cents. `149900` is\n> ₹1,499.00. Format with `Intl.NumberFormat`, dividing by 100.\n\n### `purchase(target, options?)`\n\nStarts a web checkout and sends the browser to it. Web purchases settle through\nStripe.\n\n```typescript\nawait cashier.purchase(packages[0], {\n  successUrl: 'https://example.com/welcome',\n  cancelUrl: 'https://example.com/pricing',\n})\n```\n\n`target` may be a package, a product, or a bare product id. `successUrl` and\n`cancelUrl` default to the current page.\n\n| Option | Effect |\n|---|---|\n| `customerEmail` | Pre-fills checkout, skipping the email step |\n| `locale` | BCP-47 tag for checkout's language, e.g. `hi-IN` |\n| `metadata` | Carried through to your webhook on the resulting event |\n| `redirect: false` | Returns the URL instead of navigating to it |\n\nThis **navigates away**, so treat the call as the end of the page's life. To\nhandle the redirect yourself, pass `redirect: false` and use the returned URL:\n\n```typescript\nconst { url } = await cashier.purchase(pkg, { redirect: false })\n```\n\nNothing is unlocked when the customer returns to your success URL — it is\nunlocked when Stripe tells Cloud the money moved. Always refetch:\n\n```typescript\nconst info = await cashier.customerInfo({ force: true })\n```\n\n---\n\n## Errors\n\nEvery failure throws `CashierCloudError` carrying a stable `code`. **Branch on\nthe code, not the status or the message** — statuses get reused and messages get\nreworded; neither is a contract.\n\n```typescript\nimport { CashierCloudError, CashierErrorCode } from '@codesyncr/cashier-cloud'\n\ntry {\n  await cashier.purchase(pkg)\n} catch (err) {\n  if (!(err instanceof CashierCloudError)) throw err\n\n  switch (err.code) {\n    case CashierErrorCode.UserCancelled:  break                    // not worth surfacing\n    case CashierErrorCode.Network:        return showRetry()\n    case CashierErrorCode.PaymentRequired: return showUpgrade()\n    default:                              return showGenericError(err.message)\n  }\n}\n```\n\n| Code | When |\n|---|---|\n| `network_error` | The request never reached Cloud. `status` is `0` |\n| `invalid_api_key` | Missing, malformed, revoked, or a secret key in a browser |\n| `invalid_app_user_id` | Empty or reserved app user id |\n| `offering_not_found` | No offering configured, or the named one is gone |\n| `product_not_available` | Not purchasable right now |\n| `subscriber_not_found` | Never seen this subscriber |\n| `payment_required` | The account is over its plan |\n| `user_cancelled` | The customer walked away from checkout |\n| `checkout_failed` | Checkout could not be started |\n| `rate_limited` | Too many requests |\n| `server_error` | Cloud faulted |\n| `unknown` | Anything else |\n\n`err.paymentRequired` and `err.userCancelled` are shortcuts for the two you will\nbranch on most. A network failure is a typed error too — a raw `TypeError` from\n`fetch` would be indistinguishable from a bug in your own code.\n\n---\n\n## React\n\n```tsx\nimport { createContext, useContext, useEffect, useState } from 'react'\nimport { CashierCloud, type CustomerInfo } from '@codesyncr/cashier-cloud'\n\nconst cashier = new CashierCloud({ apiKey: import.meta.env.VITE_CASHIER_KEY })\nconst Ctx = createContext<CustomerInfo | null>(null)\n\nexport function CashierProvider({ userId, children }: { userId?: string; children: React.ReactNode }) {\n  const [info, setInfo] = useState<CustomerInfo | null>(null)\n\n  useEffect(() => {\n    const off = cashier.onCustomerInfoUpdate(setInfo)\n    // logIn merges anything bought before sign-up; plain read otherwise.\n    ;(userId ? cashier.logIn(userId) : cashier.customerInfo()).catch(console.error)\n    return off\n  }, [userId])\n\n  return <Ctx.Provider value={info}>{children}</Ctx.Provider>\n}\n\nexport function useEntitlement(id: string) {\n  return useContext(Ctx)?.entitlements[id]?.active ?? false\n}\n```\n\n---\n\n## Types\n\nEverything is exported: `CustomerInfo`, `EntitlementInfo`, `Product`, `Package`,\n`Offering`, `ResolvedPackage`, `OfferingsResponse`, `Subscription`,\n`SubscriptionStatus`, `PeriodType`, `EntitlementSource`, `Entitlement`.\n\n`SubscriberEvent` is also exported — it is the payload Cloud posts to your\nbackend's webhook, so you can type that handler with the same package:\n\n```typescript\nimport type { SubscriberEvent } from '@codesyncr/cashier-cloud'\n\nexport function handleCashierWebhook(event: SubscriberEvent) {\n  switch (event.type) {\n    case 'initial_purchase':\n    case 'renewal':          return grant(event.subject, event.entitlementIds)\n    case 'billing_issue':    return emailAboutPayment(event.subject)\n    case 'expiration':       return revoke(event.subject, event.entitlementIds)\n  }\n}\n```\n\n---\n\n## Not in this package\n\n- **Native in-app purchases** — the iOS and Android SDKs, which wrap StoreKit 2\n  and Play Billing.\n- **Receipt verification and metering** — Cloud's, and they run on Cloud.\n- **Razorpay, PayU, PhonePe, Cashfree, Paytm, PayPal** — the multi-gateway layer\n  is `plugins/cashier` in the Nimbus framework: free, self-hosted, Go, and\n  unrelated to Cloud.\n\n---\n\n## Development\n\n```bash\nnpm install && npm run build && npm test\n```\n\n## Status\n\nPrivate until Cloud's endpoints are live. The API surface below is settled.\n","readmeFilename":"README.md","_rev":"1-8585f3f3b59132fda27aec1f1b2cebff"}