@umituz/web-payment 0.0.0-stage → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +349 -2
  3. package/dist/billing-adapter.interface-CDkeeowZ.d.mts +94 -0
  4. package/dist/billing-adapter.interface-m7yE50I0.d.ts +94 -0
  5. package/dist/billing-cycle.enum--hJRYlgP.d.mts +10 -0
  6. package/dist/billing-cycle.enum--hJRYlgP.d.ts +10 -0
  7. package/dist/billing-env-D4YjvA3M.d.mts +8 -0
  8. package/dist/billing-env-D4YjvA3M.d.ts +8 -0
  9. package/dist/checkout-metadata-D7Wp_DAD.d.mts +27 -0
  10. package/dist/checkout-metadata-D7Wp_DAD.d.ts +27 -0
  11. package/dist/chunk-2KV7JFYF.mjs +61 -0
  12. package/dist/chunk-2NH5DOLT.mjs +101 -0
  13. package/dist/chunk-3AXPZOCV.mjs +106 -0
  14. package/dist/chunk-4N3BASZ6.mjs +51 -0
  15. package/dist/chunk-NHKK3ENU.mjs +1128 -0
  16. package/dist/chunk-PIWNUAKW.mjs +30 -0
  17. package/dist/chunk-QFB7AOSQ.mjs +33 -0
  18. package/dist/chunk-RQTMBR74.mjs +30 -0
  19. package/dist/chunk-U45FS4FF.mjs +110 -0
  20. package/dist/chunk-X6R76FZT.mjs +0 -0
  21. package/dist/chunk-XQ2HJTTA.mjs +252 -0
  22. package/dist/core.d.mts +17 -0
  23. package/dist/core.d.ts +17 -0
  24. package/dist/core.js +425 -0
  25. package/dist/core.mjs +66 -0
  26. package/dist/http.d.mts +12 -0
  27. package/dist/http.d.ts +12 -0
  28. package/dist/http.js +115 -0
  29. package/dist/http.mjs +88 -0
  30. package/dist/index.d.mts +17 -0
  31. package/dist/index.d.ts +17 -0
  32. package/dist/index.js +1929 -0
  33. package/dist/index.mjs +153 -0
  34. package/dist/memory.d.mts +14 -0
  35. package/dist/memory.d.ts +14 -0
  36. package/dist/memory.js +130 -0
  37. package/dist/memory.mjs +6 -0
  38. package/dist/portal.d.mts +445 -0
  39. package/dist/portal.d.ts +445 -0
  40. package/dist/portal.js +2927 -0
  41. package/dist/portal.mjs +2737 -0
  42. package/dist/product.mapper-CnY1ISOp.d.ts +97 -0
  43. package/dist/product.mapper-noVl-sel.d.mts +97 -0
  44. package/dist/products.d.mts +64 -0
  45. package/dist/products.d.ts +64 -0
  46. package/dist/products.js +370 -0
  47. package/dist/products.mjs +21 -0
  48. package/dist/react.d.mts +182 -0
  49. package/dist/react.d.ts +182 -0
  50. package/dist/react.js +1874 -0
  51. package/dist/react.mjs +105 -0
  52. package/dist/server.d.mts +177 -0
  53. package/dist/server.d.ts +177 -0
  54. package/dist/server.js +622 -0
  55. package/dist/server.mjs +417 -0
  56. package/dist/visual-tokens-BCLVR85G.d.mts +94 -0
  57. package/dist/visual-tokens-BCLVR85G.d.ts +94 -0
  58. package/package.json +122 -4
  59. package/src/core.ts +81 -0
  60. package/src/domain/entities/cancellation.entity.ts +28 -0
  61. package/src/domain/entities/checkout.entity.ts +21 -0
  62. package/src/domain/entities/index.ts +10 -0
  63. package/src/domain/entities/order.entity.ts +15 -0
  64. package/src/domain/entities/product.entity.ts +16 -0
  65. package/src/domain/entities/subscription.entity.ts +24 -0
  66. package/src/domain/entities/sync.entity.ts +4 -0
  67. package/src/domain/enums/amount-type.enum.ts +10 -0
  68. package/src/domain/enums/billing-cycle.enum.ts +30 -0
  69. package/src/domain/enums/index.ts +5 -0
  70. package/src/domain/enums/payment-provider.enum.ts +14 -0
  71. package/src/domain/enums/subscription-status.enum.ts +32 -0
  72. package/src/domain/index.ts +5 -0
  73. package/src/domain/interfaces/billing-adapter.interface.ts +18 -0
  74. package/src/domain/interfaces/index.ts +1 -0
  75. package/src/domain/mappers/product.mapper.ts +176 -0
  76. package/src/domain/utils/minor-units.ts +20 -0
  77. package/src/domain/utils/percent-discount.ts +17 -0
  78. package/src/http.ts +3 -0
  79. package/src/index.ts +12 -0
  80. package/src/infrastructure/browser/hosted-checkout.ts +26 -0
  81. package/src/infrastructure/config/billing-env.ts +31 -0
  82. package/src/infrastructure/constants/billing.constants.ts +10 -0
  83. package/src/infrastructure/http/http-client.ts +83 -0
  84. package/src/infrastructure/pagination/page-iterator.ts +43 -0
  85. package/src/infrastructure/query/query-builder.ts +26 -0
  86. package/src/infrastructure/services/billing.client.ts +300 -0
  87. package/src/infrastructure/services/create-billing-client.ts +23 -0
  88. package/src/infrastructure/services/http/create-http-adapter.ts +123 -0
  89. package/src/infrastructure/services/memory/create-memory-adapter.ts +126 -0
  90. package/src/infrastructure/services/product.repository.ts +198 -0
  91. package/src/infrastructure/utils/checkout-metadata.ts +55 -0
  92. package/src/infrastructure/utils/date-renderer.ts +53 -0
  93. package/src/infrastructure/utils/money-formatter.ts +39 -0
  94. package/src/infrastructure/utils/normalization.util.ts +22 -0
  95. package/src/infrastructure/utils/pagination-clamp.ts +11 -0
  96. package/src/infrastructure/utils/receipt.util.ts +6 -0
  97. package/src/infrastructure/utils/security.util.ts +23 -0
  98. package/src/infrastructure/utils/type-guards.util.ts +23 -0
  99. package/src/infrastructure/utils/validation.util.ts +10 -0
  100. package/src/infrastructure/webhook/parse-event.ts +135 -0
  101. package/src/infrastructure/webhook/verify-webhook.ts +124 -0
  102. package/src/memory.ts +3 -0
  103. package/src/portal.ts +2 -0
  104. package/src/presentation/components/BillingHistoryList.tsx +172 -0
  105. package/src/presentation/components/BillingProvider.tsx +179 -0
  106. package/src/presentation/components/CancelSubscriptionButton.tsx +155 -0
  107. package/src/presentation/components/CheckoutButton.tsx +76 -0
  108. package/src/presentation/components/CustomerPortalLink.tsx +71 -0
  109. package/src/presentation/components/PricingTable.tsx +225 -0
  110. package/src/presentation/components/SubscriptionStatusBadge.tsx +102 -0
  111. package/src/presentation/context/billing-context.ts +30 -0
  112. package/src/presentation/hooks/useBilling.ts +3 -0
  113. package/src/presentation/hooks/useCancelSubscription.ts +47 -0
  114. package/src/presentation/hooks/useCheckoutButton.ts +49 -0
  115. package/src/presentation/hooks/useProducts.ts +76 -0
  116. package/src/presentation/navigation/navigation.strategy.ts +16 -0
  117. package/src/presentation/portal/TurnkeyBillingPage.tsx +144 -0
  118. package/src/presentation/portal/components/BillingLayout.tsx +163 -0
  119. package/src/presentation/portal/components/BillingPage.tsx +22 -0
  120. package/src/presentation/portal/components/BillingPortal.tsx +248 -0
  121. package/src/presentation/portal/components/InvoiceCard.tsx +275 -0
  122. package/src/presentation/portal/components/OverviewTab.tsx +137 -0
  123. package/src/presentation/portal/components/PaymentMethodsList.tsx +268 -0
  124. package/src/presentation/portal/components/PlanComparison.tsx +405 -0
  125. package/src/presentation/portal/components/PlanTab.tsx +197 -0
  126. package/src/presentation/portal/components/PortalButton.tsx +89 -0
  127. package/src/presentation/portal/components/PortalSpinner.tsx +30 -0
  128. package/src/presentation/portal/components/UsageCard.tsx +158 -0
  129. package/src/presentation/portal/components/index.ts +11 -0
  130. package/src/presentation/portal/constants.ts +44 -0
  131. package/src/presentation/portal/create-portal-client.ts +202 -0
  132. package/src/presentation/portal/hooks/index.ts +3 -0
  133. package/src/presentation/portal/hooks/useBilling.ts +240 -0
  134. package/src/presentation/portal/index.ts +97 -0
  135. package/src/presentation/portal/labels.tsx +218 -0
  136. package/src/presentation/portal/styles.ts +70 -0
  137. package/src/presentation/portal/types.ts +213 -0
  138. package/src/presentation/portal/utils/billing.ts +193 -0
  139. package/src/presentation/portal/utils/index.ts +27 -0
  140. package/src/presentation/portal/utils/plans.ts +71 -0
  141. package/src/presentation/theme/visual-tokens.ts +103 -0
  142. package/src/presentation/utils/cancellation-reason-catalog.ts +18 -0
  143. package/src/presentation/utils/checkout-button-visual.ts +50 -0
  144. package/src/presentation/utils/inline-error-visual.ts +21 -0
  145. package/src/presentation/utils/product-pricing.ts +33 -0
  146. package/src/presentation/utils/status-label.ts +23 -0
  147. package/src/presentation/utils/status-tone.ts +25 -0
  148. package/src/products.ts +20 -0
  149. package/src/react.tsx +63 -0
  150. package/src/server.ts +79 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 umituz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,350 @@
1
- # Temporary Holding Version
1
+ # @umituz/web-payment
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Universal web payment and subscription billing engine with multi-provider architecture (Polar.sh, Stripe, etc.), an HTTP adapter, a turnkey billing portal, and React bindings.
4
+
5
+ The SDK is split into an **adapter-agnostic core** (`BillingAdapter` contract, payment models, calculation primitives)
6
+ and a **React presentation layer** (`BillingProvider`, `useBilling`).
7
+ The HTTP adapter binds the contract to your own API; any payment backend can
8
+ be plugged in by implementing the same interface.
9
+
10
+ ## Features
11
+
12
+ - Multi-provider architecture with adapter-agnostic core (`BillingAdapter`, `PaymentServerClient`)
13
+ - Polar.sh client & webhook verification (`PolarBillingClient`, `verifyPolarWebhook`)
14
+ - Extensible for Stripe and future payment gateways
15
+ - HTTP adapter for your own REST backend (`createHttpAdapter`)
16
+ - Common checkout utilities (`buildCheckoutMetadata`, `readCheckoutMetadata`, `formatReceiptNumber`)
17
+ - Polar.sh product catalog (`getProducts`, `getProductById`, `getAllProducts`)
18
+ - React provider + hooks (`BillingProvider`, `useBilling`, `useProducts`)
19
+ - Ready-made UI: pricing table, checkout button, status badge, billing
20
+ history, customer portal link, cancel flow — all localizable
21
+ - Turnkey billing portal page (`TurnkeyBillingPage`)
22
+ - Strongly-typed domain models, abortable I/O
23
+
24
+ ## Installation
25
+
26
+ ```bash
27
+ npm install @umituz/web-payment
28
+ ```
29
+
30
+ Peers are **optional** — install only what you use:
31
+
32
+ | You use… | Also install | Runtime requirement |
33
+ |---|---|---|
34
+ | `…/core` | — | Any JS runtime (zero deps) |
35
+ | `…/products` | — | `fetch` (browsers, Node ≥ 18, workers) |
36
+ | `…/react` | `react` (≥ 18) | React DOM or React Native Web |
37
+ | root (`@umituz/web-polar-payment`) | `react` if you use the React parts | Node ≥ 18 (`engines`) |
38
+
39
+ `react-dom` is **not** required. The package is fully tree-shakeable
40
+ (`sideEffects: false`, ESM + CJS + `.d.ts` via tsup).
41
+
42
+ ## Subpath entries
43
+
44
+ Each surface pulls in only its own dependencies — importing `/react`
45
+ never loads server code, importing `/core` loads nothing at all:
46
+
47
+ ```ts
48
+ // Pure domain — types, enums, mappers, the BillingAdapter contract
49
+ import { BILLING_CYCLE, parseBillingCycle, mapProduct } from '@umituz/web-polar-payment/core';
50
+
51
+ // Polar.sh product catalog over a pluggable HttpClient
52
+ import { ProductRepository, FetchHttpClient } from '@umituz/web-polar-payment/products';
53
+
54
+ // React layer: provider, hooks, components, theme tokens
55
+ import { BillingProvider, PricingTable } from '@umituz/web-polar-payment/react';
56
+
57
+ // Server-side Polar.sh operations: checkout sessions, customer portal,
58
+ // subscription management, webhook signature verification (server only!)
59
+ import { BillingClient, verifyWebhook } from '@umituz/web-polar-payment/server';
60
+
61
+ // Browser BillingAdapter over your own REST endpoints
62
+ import { createHttpAdapter } from '@umituz/web-polar-payment/http';
63
+
64
+ // Turnkey billing portal page over any adapter
65
+ import { TurnkeyBillingPage } from '@umituz/web-polar-payment/portal';
66
+
67
+ // Root entry = everything above (backwards-compatible import paths)
68
+ import { useBilling } from '@umituz/web-polar-payment';
69
+ ```
70
+
71
+ ## Usage
72
+
73
+ ### 1. Wrap the app
74
+
75
+ ```tsx
76
+ import { BillingProvider, createHttpAdapter } from '@umituz/web-polar-payment';
77
+
78
+ const adapter = createHttpAdapter({
79
+ baseUrl: '/api/billing',
80
+ getUserId: () => session.organizationId,
81
+ });
82
+
83
+ function App({ userId, children }) {
84
+ return (
85
+ <BillingProvider adapter={adapter} userId={userId}>
86
+ {children}
87
+ </BillingProvider>
88
+ );
89
+ }
90
+ ```
91
+
92
+ ### 2. Read subscription state
93
+
94
+ ```tsx
95
+ import { useBilling } from '@umituz/web-polar-payment';
96
+
97
+ function PlanBadge() {
98
+ const { status, loading } = useBilling();
99
+ if (loading) return null;
100
+ return <span>{status.plan}</span>;
101
+ }
102
+ ```
103
+
104
+ ### 3. Start a checkout
105
+
106
+ ```tsx
107
+ import { useBilling } from '@umituz/web-polar-payment';
108
+
109
+ function UpgradeButton({ productId }) {
110
+ const { startCheckout } = useBilling();
111
+ return (
112
+ <button onClick={() => startCheckout({ productId, billingCycle: 'monthly' })}>
113
+ Upgrade
114
+ </button>
115
+ );
116
+ }
117
+ ```
118
+
119
+ ### 4. Sync after a checkout redirect
120
+
121
+ ```tsx
122
+ import { useEffect } from 'react';
123
+ import { useBilling } from '@umituz/web-polar-payment';
124
+
125
+ function CheckoutReturn() {
126
+ const { syncSubscription } = useBilling();
127
+ useEffect(() => {
128
+ void syncSubscription();
129
+ }, [syncSubscription]);
130
+ return null;
131
+ }
132
+ ```
133
+
134
+ ### 5. Pricing table — the secure pattern
135
+
136
+ Fetch products **on your server** and pass them down; no access token ever
137
+ reaches the browser bundle:
138
+
139
+ ```tsx
140
+ // server.ts
141
+ import { getAllProducts } from '@umituz/web-polar-payment/products';
142
+
143
+ const products = await getAllProducts(
144
+ { apiToken: process.env.POLAR_OAT_TOKEN! }, // server-side secret
145
+ { isRecurring: true },
146
+ );
147
+
148
+ // PricingPage.tsx
149
+ <PricingTable products={products} ctaLabel="Subscribe" />
150
+ ```
151
+
152
+ > ⚠️ OAT tokens grant full account access — never ship them to a public
153
+ > browser bundle. The `apiToken` prop on `PricingTable` exists for local
154
+ > development only.
155
+
156
+ ### 6. Localize any component
157
+
158
+ Every component takes an optional `labels` subset and `locale`:
159
+
160
+ ```tsx
161
+ <SubscriptionStatusBadge
162
+ loading={false}
163
+ status={status}
164
+ locale="de-DE"
165
+ labels={{ statuses: { active: 'Aktiv' }, endsPrefix: 'endet' }}
166
+ />
167
+ ```
168
+
169
+ ### 7. Own REST backend — `./server` + `./http`
170
+
171
+ When the backend is your own API worker (any runtime with `fetch`), split the
172
+ work with the two transport entry points:
173
+
174
+ ```ts
175
+ // api/billing.ts — server side
176
+ import {
177
+ BillingClient,
178
+ verifyWebhook,
179
+ parseWebhookEvent,
180
+ asCheckoutCompleted,
181
+ asSubscriptionChanged,
182
+ asOrderPaid,
183
+ } from '@umituz/web-polar-payment/server';
184
+
185
+ const billing = new BillingClient({
186
+ apiToken: env.POLAR_ACCESS_TOKEN!, // OAT token, server only
187
+ sandbox: env.POLAR_SANDBOX === 'true', // sandbox-api.polar.sh
188
+ });
189
+
190
+ // Checkout: POST /v1/checkouts with `products: [id]` — metadata is copied
191
+ // onto the order/subscription. Never collect card data yourself: redirect
192
+ // the browser to `checkout.url` (Polar-hosted, PCI scope stays SAQ-A).
193
+ const checkout = await billing.createCheckout({
194
+ productId,
195
+ successUrl: `${appUrl}/billing/return?checkout_id={CHECKOUT_ID}`,
196
+ externalCustomerId: user.id, // links your user to the Polar customer
197
+ metadata: { organizationId: user.orgId },
198
+ });
199
+
200
+ // Dynamic amount (bookings, pay-what-you-want floors): an ad-hoc fixed
201
+ // price is created on the checkout session, catalog product stays intact
202
+ const bookingCheckout = await billing.createCheckout({
203
+ productId: bookingProductId,
204
+ amountCents: 2500,
205
+ currency: 'usd',
206
+ allowDiscountCodes: false,
207
+ metadata: { bookingId },
208
+ });
209
+
210
+ // One-time products (digital goods): POST /v1/products with a fixed price
211
+ const product = await billing.createProduct({
212
+ name: 'Ultralight Gear Course',
213
+ amountCents: 7900, // smallest currency unit
214
+ metadata: { creatorId },
215
+ });
216
+
217
+ // Managed billing portal (payment methods, invoices, cancellation)
218
+ const { url } = await billing.createPortalSession(customerId, appUrl);
219
+
220
+ // Cancel at period end (reversible) vs revoke (immediate)
221
+ await billing.updateSubscription(subscriptionId, { cancelAtPeriodEnd: true });
222
+
223
+ // Webhooks: Standard Webhooks spec — verify the RAW body, before JSON.parse
224
+ const raw = await request.text();
225
+ if (!(await verifyWebhook(raw, request.headers, env.POLAR_WEBHOOK_SECRET))) {
226
+ return new Response('invalid signature', { status: 401 });
227
+ }
228
+ // Then parse the (still raw) body into a typed event view
229
+ const event = parseWebhookEvent(raw);
230
+ const sale = event && asCheckoutCompleted(event); // checkout.completed
231
+ const change = event && asSubscriptionChanged(event); // subscription.* lifecycle
232
+ const paid = event && asOrderPaid(event); // order.paid (renewals)
233
+ ```
234
+
235
+ The browser side binds the same `BillingAdapter` contract to six endpoints under
236
+ one base path — implement them on your worker to match:
237
+
238
+ ```ts
239
+ // app/billing.ts — browser side
240
+ import { createHttpAdapter } from '@umituz/web-polar-payment/http';
241
+
242
+ const adapter = createHttpAdapter({
243
+ baseUrl: '/api/billing',
244
+ getUserId: () => session.organizationId, // used when a call has no id
245
+ });
246
+
247
+ // GET /status?userId= POST /checkout { productId, billingCycle?, ... }
248
+ // POST /sync { checkoutId? } GET /history?userId=
249
+ // POST /cancel { reason? } GET /portal?userId=
250
+ ```
251
+
252
+ Failures return a `{ "error": string | { "message": string } }` envelope with
253
+ a non-2xx status; the adapter surfaces it as `Error("detail (HTTP status)")`.
254
+
255
+ ### 8. Turnkey billing page — `./portal` over any adapter
256
+
257
+ `TurnkeyBillingPage` composes the whole portal (plan comparison, subscription
258
+ tabs, invoices, hosted-portal payment methods) over a `BillingAdapter`. The
259
+ host binds the adapter and the server-fetched catalog; everything else —
260
+ data loading, checkout redirects, cancellation — is wired inside:
261
+
262
+ ```tsx
263
+ import { TurnkeyBillingPage } from '@umituz/web-polar-payment/portal';
264
+ import { createHttpAdapter } from '@umituz/web-polar-payment/http';
265
+
266
+ // products: fetched from your server (metadata.planId tags plan keys)
267
+ <TurnkeyBillingPage
268
+ adapter={createHttpAdapter({ baseUrl: '/api/billing', getUserId: () => orgId })}
269
+ products={products}
270
+ config={{ brandName: 'Acme', supportEmail: 'billing@acme.dev' }}
271
+ />
272
+ ```
273
+
274
+ Prefer composing yourself? `createPortalClient({ adapter, products })`
275
+ returns the portal's `BillingApiClient` seam (plus `selectPlan` /
276
+ `openPortal`) for direct use with `useBilling`.
277
+
278
+ ## Error handling
279
+
280
+ Server-side failures throw `BillingApiError` with the HTTP `status` and the
281
+ raw `body` attached — check with `isBillingApiError(error)`. Browser-side
282
+ adapter failures surface as plain `Error` values carrying the endpoint's
283
+ `{ "error": ... }` envelope detail and the HTTP status.
284
+
285
+ ## Architecture
286
+
287
+ ```
288
+ src/
289
+ ├── core.ts / products.ts / react.tsx # subpath entries (+ server, http, portal)
290
+ ├── domain/ # Pure types + interfaces (no I/O)
291
+ │ ├── entities/ # Subscription, Checkout, Order, ...
292
+ │ ├── enums/ # SubscriptionStatus, BillingCycle, ...
293
+ │ ├── interfaces/ # BillingAdapter contract
294
+ │ ├── utils/ # minor-unit money math
295
+ │ └── mappers/ # snake_case DTO → camelCase domain
296
+ ├── infrastructure/ # Transport, repositories, utilities
297
+ │ ├── http/ # HttpClient (Strategy, AbortSignal-aware)
298
+ │ ├── query/ # QueryBuilder (Builder)
299
+ │ ├── pagination/ # PageIterator (Iterator)
300
+ │ ├── services/ # ProductRepository, BillingClient,
301
+ │ │ └── http/ # createHttpAdapter (six-endpoint contract)
302
+ │ ├── constants/ # SUBSCRIPTION_STATUS, API base URL
303
+ │ └── utils/ # validation, security, type-guards
304
+ └── presentation/ # React layer
305
+ ├── context/ # BillingContext (race-safe refresh)
306
+ ├── hooks/ # useBilling, useProducts
307
+ ├── navigation/ # NavigationStrategy
308
+ ├── portal/ # Turnkey billing portal (page + client)
309
+ ├── theme/ # VisualTokens (self-contained)
310
+ └── components/ # Provider + UI components
311
+ ```
312
+
313
+ ### Patterns in use
314
+
315
+ - **Adapter** — `BillingAdapter` interface, HTTP implementation
316
+ - **Repository** — one repository per business operation
317
+ - **Strategy** — `HttpClient` (swap `fetch` for tests), `NavigationStrategy`
318
+ - **Builder** — `QueryBuilder` for URL search params
319
+ - **Iterator** — `PageIterator` for paginated Polar.sh lists
320
+ - **Error as a value** — `BillingApiError` carries status + raw body
321
+
322
+ ### Design decisions
323
+
324
+ - **Dependency direction points inward.** `presentation` and
325
+ `infrastructure` depend on `domain`; `domain` depends on nothing. This is
326
+ what lets each subpath ship with only its own dependencies.
327
+ - **VisualTokens are self-contained** rather than depending on a shared
328
+ design-system package: a billing SDK must be droppable into any app
329
+ without dragging a UI framework along. Override styles via `labels`,
330
+ `locale`, and component composition instead.
331
+ - **Localization via prop injection, not an i18n framework** — `labels`
332
+ objects with English defaults keep the runtime dependency-free while
333
+ still allowing full translation.
334
+ - **Optimistic-free state**: `BillingProvider` serializes refreshes behind a
335
+ generation counter; a slow earlier response can never overwrite a newer
336
+ one.
337
+
338
+ ## Development
339
+
340
+ ```bash
341
+ npm run typecheck # tsc --noEmit
342
+ npm run lint # eslint (flat config)
343
+ npm test # vitest — 170 tests (domain, infra, presentation)
344
+ npm run build # tsup — CJS + ESM + d.ts for every subpath entry
345
+ npm pack --dry-run # inspect the publishable tarball
346
+ ```
347
+
348
+ ## License
349
+
350
+ MIT
@@ -0,0 +1,94 @@
1
+ import { b as BillingCycle } from './billing-cycle.enum--hJRYlgP.mjs';
2
+
3
+ declare const SUBSCRIPTION_STATUS: {
4
+ readonly ACTIVE: "active";
5
+ readonly CANCELED: "canceled";
6
+ readonly REVOKED: "revoked";
7
+ readonly TRIALING: "trialing";
8
+ readonly PAST_DUE: "past_due";
9
+ readonly INCOMPLETE: "incomplete";
10
+ readonly INCOMPLETE_EXPIRED: "incomplete_expired";
11
+ readonly UNPAID: "unpaid";
12
+ readonly NONE: "none";
13
+ };
14
+ declare const SUBSCRIPTION_STATUS_VALUES: readonly ["active", "canceled", "revoked", "trialing", "past_due", "incomplete", "incomplete_expired", "unpaid", "none"];
15
+ type SubscriptionStatusValue = (typeof SUBSCRIPTION_STATUS_VALUES)[number];
16
+ declare function isKnownSubscriptionStatus(value: unknown): value is SubscriptionStatusValue;
17
+
18
+ declare const CANCELLATION_REASON: Readonly<{
19
+ readonly TOO_EXPENSIVE: "too_expensive";
20
+ readonly MISSING_FEATURES: "missing_features";
21
+ readonly SWITCHED_SERVICE: "switched_service";
22
+ readonly UNUSED: "unused";
23
+ readonly CUSTOMER_SERVICE: "customer_service";
24
+ readonly LOW_QUALITY: "low_quality";
25
+ readonly TOO_COMPLEX: "too_complex";
26
+ readonly OTHER: "other";
27
+ }>;
28
+ declare const CANCELLATION_REASON_VALUES: readonly ["too_expensive", "missing_features", "switched_service", "unused", "customer_service", "low_quality", "too_complex", "other"];
29
+ type CancellationReason = (typeof CANCELLATION_REASON_VALUES)[number];
30
+ interface CancelResult {
31
+ success: boolean;
32
+ endsAt?: string;
33
+ }
34
+
35
+ interface SubscriptionStatus {
36
+ plan: string;
37
+ subscriptionId?: string;
38
+ subscriptionStatus: SubscriptionStatusValue;
39
+ cancelAtPeriodEnd?: boolean;
40
+ currentPeriodEnd?: string;
41
+ billingCycle?: BillingCycle;
42
+ providerCustomerId?: string;
43
+ }
44
+ declare const FREE_SUBSCRIPTION_STATUS: SubscriptionStatus;
45
+ declare function isActiveSubscription(status: SubscriptionStatus): boolean;
46
+
47
+ interface CheckoutParams {
48
+ productId?: string;
49
+ productKey?: string;
50
+ itemId?: string;
51
+ couponCode?: string;
52
+ planKey?: string;
53
+ billingCycle?: BillingCycle;
54
+ successUrl?: string;
55
+ userId?: string;
56
+ customerEmail?: string;
57
+ seats?: number;
58
+ metadata?: Record<string, string | number | boolean>;
59
+ }
60
+ interface CheckoutResult {
61
+ url: string;
62
+ id: string;
63
+ }
64
+
65
+ interface OrderItem {
66
+ id: string;
67
+ createdAt: string;
68
+ amount: number;
69
+ currency: string;
70
+ status: string;
71
+ paid: boolean;
72
+ productName?: string;
73
+ itemType?: string;
74
+ itemId?: string;
75
+ amountUsd?: number;
76
+ receiptNumber?: string;
77
+ invoiceUrl?: string;
78
+ }
79
+
80
+ interface SyncResult {
81
+ synced: boolean;
82
+ plan?: string;
83
+ }
84
+
85
+ interface BillingAdapter {
86
+ getStatus(userId: string): Promise<SubscriptionStatus>;
87
+ createCheckout(params: CheckoutParams): Promise<CheckoutResult>;
88
+ syncSubscription(userId: string, checkoutId?: string): Promise<SyncResult>;
89
+ getBillingHistory(userId: string): Promise<OrderItem[]>;
90
+ cancelSubscription(reason?: CancellationReason): Promise<CancelResult>;
91
+ getPortalUrl(userId: string): Promise<string>;
92
+ }
93
+
94
+ export { type BillingAdapter as B, CANCELLATION_REASON as C, FREE_SUBSCRIPTION_STATUS as F, type OrderItem as O, SUBSCRIPTION_STATUS as S, CANCELLATION_REASON_VALUES as a, type CancelResult as b, type CancellationReason as c, type CheckoutParams as d, type CheckoutResult as e, SUBSCRIPTION_STATUS_VALUES as f, type SubscriptionStatus as g, type SubscriptionStatusValue as h, type SyncResult as i, isActiveSubscription as j, isKnownSubscriptionStatus as k };
@@ -0,0 +1,94 @@
1
+ import { b as BillingCycle } from './billing-cycle.enum--hJRYlgP.js';
2
+
3
+ declare const SUBSCRIPTION_STATUS: {
4
+ readonly ACTIVE: "active";
5
+ readonly CANCELED: "canceled";
6
+ readonly REVOKED: "revoked";
7
+ readonly TRIALING: "trialing";
8
+ readonly PAST_DUE: "past_due";
9
+ readonly INCOMPLETE: "incomplete";
10
+ readonly INCOMPLETE_EXPIRED: "incomplete_expired";
11
+ readonly UNPAID: "unpaid";
12
+ readonly NONE: "none";
13
+ };
14
+ declare const SUBSCRIPTION_STATUS_VALUES: readonly ["active", "canceled", "revoked", "trialing", "past_due", "incomplete", "incomplete_expired", "unpaid", "none"];
15
+ type SubscriptionStatusValue = (typeof SUBSCRIPTION_STATUS_VALUES)[number];
16
+ declare function isKnownSubscriptionStatus(value: unknown): value is SubscriptionStatusValue;
17
+
18
+ declare const CANCELLATION_REASON: Readonly<{
19
+ readonly TOO_EXPENSIVE: "too_expensive";
20
+ readonly MISSING_FEATURES: "missing_features";
21
+ readonly SWITCHED_SERVICE: "switched_service";
22
+ readonly UNUSED: "unused";
23
+ readonly CUSTOMER_SERVICE: "customer_service";
24
+ readonly LOW_QUALITY: "low_quality";
25
+ readonly TOO_COMPLEX: "too_complex";
26
+ readonly OTHER: "other";
27
+ }>;
28
+ declare const CANCELLATION_REASON_VALUES: readonly ["too_expensive", "missing_features", "switched_service", "unused", "customer_service", "low_quality", "too_complex", "other"];
29
+ type CancellationReason = (typeof CANCELLATION_REASON_VALUES)[number];
30
+ interface CancelResult {
31
+ success: boolean;
32
+ endsAt?: string;
33
+ }
34
+
35
+ interface SubscriptionStatus {
36
+ plan: string;
37
+ subscriptionId?: string;
38
+ subscriptionStatus: SubscriptionStatusValue;
39
+ cancelAtPeriodEnd?: boolean;
40
+ currentPeriodEnd?: string;
41
+ billingCycle?: BillingCycle;
42
+ providerCustomerId?: string;
43
+ }
44
+ declare const FREE_SUBSCRIPTION_STATUS: SubscriptionStatus;
45
+ declare function isActiveSubscription(status: SubscriptionStatus): boolean;
46
+
47
+ interface CheckoutParams {
48
+ productId?: string;
49
+ productKey?: string;
50
+ itemId?: string;
51
+ couponCode?: string;
52
+ planKey?: string;
53
+ billingCycle?: BillingCycle;
54
+ successUrl?: string;
55
+ userId?: string;
56
+ customerEmail?: string;
57
+ seats?: number;
58
+ metadata?: Record<string, string | number | boolean>;
59
+ }
60
+ interface CheckoutResult {
61
+ url: string;
62
+ id: string;
63
+ }
64
+
65
+ interface OrderItem {
66
+ id: string;
67
+ createdAt: string;
68
+ amount: number;
69
+ currency: string;
70
+ status: string;
71
+ paid: boolean;
72
+ productName?: string;
73
+ itemType?: string;
74
+ itemId?: string;
75
+ amountUsd?: number;
76
+ receiptNumber?: string;
77
+ invoiceUrl?: string;
78
+ }
79
+
80
+ interface SyncResult {
81
+ synced: boolean;
82
+ plan?: string;
83
+ }
84
+
85
+ interface BillingAdapter {
86
+ getStatus(userId: string): Promise<SubscriptionStatus>;
87
+ createCheckout(params: CheckoutParams): Promise<CheckoutResult>;
88
+ syncSubscription(userId: string, checkoutId?: string): Promise<SyncResult>;
89
+ getBillingHistory(userId: string): Promise<OrderItem[]>;
90
+ cancelSubscription(reason?: CancellationReason): Promise<CancelResult>;
91
+ getPortalUrl(userId: string): Promise<string>;
92
+ }
93
+
94
+ export { type BillingAdapter as B, CANCELLATION_REASON as C, FREE_SUBSCRIPTION_STATUS as F, type OrderItem as O, SUBSCRIPTION_STATUS as S, CANCELLATION_REASON_VALUES as a, type CancelResult as b, type CancellationReason as c, type CheckoutParams as d, type CheckoutResult as e, SUBSCRIPTION_STATUS_VALUES as f, type SubscriptionStatus as g, type SubscriptionStatusValue as h, type SyncResult as i, isActiveSubscription as j, isKnownSubscriptionStatus as k };
@@ -0,0 +1,10 @@
1
+ declare const BILLING_CYCLE: Readonly<{
2
+ readonly MONTHLY: "monthly";
3
+ readonly YEARLY: "yearly";
4
+ }>;
5
+ declare const BILLING_CYCLE_VALUES: readonly ["monthly", "yearly"];
6
+ type BillingCycle = (typeof BILLING_CYCLE_VALUES)[number];
7
+ declare function isKnownBillingCycle(value: unknown): value is BillingCycle;
8
+ declare function parseBillingCycle(value: unknown): BillingCycle | null;
9
+
10
+ export { BILLING_CYCLE as B, BILLING_CYCLE_VALUES as a, type BillingCycle as b, isKnownBillingCycle as i, parseBillingCycle as p };
@@ -0,0 +1,10 @@
1
+ declare const BILLING_CYCLE: Readonly<{
2
+ readonly MONTHLY: "monthly";
3
+ readonly YEARLY: "yearly";
4
+ }>;
5
+ declare const BILLING_CYCLE_VALUES: readonly ["monthly", "yearly"];
6
+ type BillingCycle = (typeof BILLING_CYCLE_VALUES)[number];
7
+ declare function isKnownBillingCycle(value: unknown): value is BillingCycle;
8
+ declare function parseBillingCycle(value: unknown): BillingCycle | null;
9
+
10
+ export { BILLING_CYCLE as B, BILLING_CYCLE_VALUES as a, type BillingCycle as b, isKnownBillingCycle as i, parseBillingCycle as p };
@@ -0,0 +1,8 @@
1
+ declare const CHECKOUT_ID_PARAM = "checkout_id";
2
+ declare const CHECKOUT_ID_PLACEHOLDER = "{CHECKOUT_ID}";
3
+ type ProductIdMap = Map<string, string>;
4
+ declare function parseProductIds(raw: string | undefined): ProductIdMap;
5
+ declare function productIdOf(ids: ProductIdMap, productKey: string): string | null;
6
+ declare function successUrlWithCheckoutId(origin: string, returnPath: string): string;
7
+
8
+ export { CHECKOUT_ID_PARAM as C, type ProductIdMap as P, CHECKOUT_ID_PLACEHOLDER as a, productIdOf as b, parseProductIds as p, successUrlWithCheckoutId as s };
@@ -0,0 +1,8 @@
1
+ declare const CHECKOUT_ID_PARAM = "checkout_id";
2
+ declare const CHECKOUT_ID_PLACEHOLDER = "{CHECKOUT_ID}";
3
+ type ProductIdMap = Map<string, string>;
4
+ declare function parseProductIds(raw: string | undefined): ProductIdMap;
5
+ declare function productIdOf(ids: ProductIdMap, productKey: string): string | null;
6
+ declare function successUrlWithCheckoutId(origin: string, returnPath: string): string;
7
+
8
+ export { CHECKOUT_ID_PARAM as C, type ProductIdMap as P, CHECKOUT_ID_PLACEHOLDER as a, productIdOf as b, parseProductIds as p, successUrlWithCheckoutId as s };
@@ -0,0 +1,27 @@
1
+ declare const PAYMENT_PROVIDER: {
2
+ readonly POLAR: "polar";
3
+ readonly STRIPE: "stripe";
4
+ };
5
+ declare const PAYMENT_PROVIDER_VALUES: ("polar" | "stripe")[];
6
+ type PaymentProvider = (typeof PAYMENT_PROVIDER)[keyof typeof PAYMENT_PROVIDER];
7
+ declare function isKnownPaymentProvider(value: unknown): value is PaymentProvider;
8
+
9
+ declare function formatReceiptNumber(id: string, prefix?: string): string;
10
+
11
+ interface CheckoutOrderMetadata {
12
+ userId: string;
13
+ itemType: string;
14
+ itemId: string;
15
+ couponCode?: string;
16
+ }
17
+ interface BuildCheckoutMetadataInput {
18
+ userId: string;
19
+ itemType: string;
20
+ itemId: string;
21
+ couponCode?: string;
22
+ extra?: Record<string, string>;
23
+ }
24
+ declare function buildCheckoutMetadata(input: BuildCheckoutMetadataInput): Record<string, string>;
25
+ declare function readCheckoutMetadata(metadata: Record<string, unknown> | undefined, allowedItemTypes: readonly string[]): CheckoutOrderMetadata | null;
26
+
27
+ export { type BuildCheckoutMetadataInput as B, type CheckoutOrderMetadata as C, PAYMENT_PROVIDER as P, PAYMENT_PROVIDER_VALUES as a, type PaymentProvider as b, buildCheckoutMetadata as c, formatReceiptNumber as f, isKnownPaymentProvider as i, readCheckoutMetadata as r };
@@ -0,0 +1,27 @@
1
+ declare const PAYMENT_PROVIDER: {
2
+ readonly POLAR: "polar";
3
+ readonly STRIPE: "stripe";
4
+ };
5
+ declare const PAYMENT_PROVIDER_VALUES: ("polar" | "stripe")[];
6
+ type PaymentProvider = (typeof PAYMENT_PROVIDER)[keyof typeof PAYMENT_PROVIDER];
7
+ declare function isKnownPaymentProvider(value: unknown): value is PaymentProvider;
8
+
9
+ declare function formatReceiptNumber(id: string, prefix?: string): string;
10
+
11
+ interface CheckoutOrderMetadata {
12
+ userId: string;
13
+ itemType: string;
14
+ itemId: string;
15
+ couponCode?: string;
16
+ }
17
+ interface BuildCheckoutMetadataInput {
18
+ userId: string;
19
+ itemType: string;
20
+ itemId: string;
21
+ couponCode?: string;
22
+ extra?: Record<string, string>;
23
+ }
24
+ declare function buildCheckoutMetadata(input: BuildCheckoutMetadataInput): Record<string, string>;
25
+ declare function readCheckoutMetadata(metadata: Record<string, unknown> | undefined, allowedItemTypes: readonly string[]): CheckoutOrderMetadata | null;
26
+
27
+ export { type BuildCheckoutMetadataInput as B, type CheckoutOrderMetadata as C, PAYMENT_PROVIDER as P, PAYMENT_PROVIDER_VALUES as a, type PaymentProvider as b, buildCheckoutMetadata as c, formatReceiptNumber as f, isKnownPaymentProvider as i, readCheckoutMetadata as r };