@molecule/app-billing-react 1.0.0 → 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 (2) hide show
  1. package/README.md +494 -0
  2. package/package.json +16 -10
package/README.md ADDED
@@ -0,0 +1,494 @@
1
+ <!--
2
+ AUTO-GENERATED — DO NOT EDIT THIS FILE.
3
+ Generated by `mlcl sync-docs` from the package's src/index.ts JSDoc + mlcl/registry.json.
4
+ Edits here are overwritten on the next commit (molecule's pre-commit hook regenerates).
5
+ To change this document, edit the module-level JSDoc in src/index.ts.
6
+ Generated: 2026-08-04T01:50:09.341Z
7
+ -->
8
+
9
+ # @molecule/app-billing-react
10
+
11
+ > **Auto-generated, AI-first package reference** for the [molecule.dev](https://molecule.dev) ecosystem.
12
+ > It is written to be read by coding agents as much as by people, and is generated from this
13
+ > package's source — edit `src/index.ts` JSDoc, not this file.
14
+
15
+ React pricing page + checkout flow for the molecule.dev billing kit.
16
+
17
+ Components:
18
+ `<PricingPage />` — public pricing table that fetches `/api/billing/tiers`
19
+ and posts to `/api/billing/checkout` when the user clicks Upgrade.
20
+
21
+ `<BillingStatusBadge />` — compact account-page status display that
22
+ shows the current tier and offers a cancel-subscription button.
23
+
24
+ `<LimitsList>` / `<LimitsItem>` — building blocks for the
25
+ `renderLimits` prop: a stacked checklist row with check / dash icon,
26
+ e.g. `renderLimits={(l) => (
27
+ <LimitsList>
28
+ <LimitsItem>{l.maxAccounts} accounts</LimitsItem>
29
+ <LimitsItem included={l.canExport}>Data export</LimitsItem>
30
+ </LimitsList>
31
+ )}`
32
+
33
+ Hooks:
34
+ `usePricingTiers<TLimits>()` → `UseHttpResult<PricingTiersResponse<TLimits>>`
35
+ `useBillingStatus<TLimits>()` → `UseHttpResult<BillingStatus<TLimits>>`
36
+ `useStartCheckout()` → `{ data, loading, error, start(priceId) }`
37
+ `useCancelSubscription()` → `{ data, loading, error, cancel() }`
38
+
39
+ The API side of this kit lives in `@molecule/api-entitlements` +
40
+ `@molecule/api-payments-stripe`. Wire those into your project (any
41
+ mlcl flagship template that includes `@molecule/api-entitlements`
42
+ already exposes the `/api/billing/*` routes), then drop `<PricingPage />`
43
+ onto a `/pricing` route.
44
+
45
+ ## Quick Start
46
+
47
+ ```tsx
48
+ import { PricingPage } from '@molecule/app-billing-react'
49
+ import type { PersonalFinanceLimits } from '../tiers'
50
+
51
+ const Pricing = () => (
52
+ <PricingPage<PersonalFinanceLimits>
53
+ period="month"
54
+ renderLimits={(l) => (
55
+ <ul>
56
+ <li>{l.maxAccounts} accounts</li>
57
+ <li>{l.maxTransactionsPerMonth} transactions / month</li>
58
+ </ul>
59
+ )}
60
+ />
61
+ )
62
+ ```
63
+
64
+ ## Type
65
+
66
+ `feature`
67
+
68
+ ## Installation
69
+
70
+ ```bash
71
+ npm install @molecule/app-billing-react @molecule/app-http @molecule/app-react @molecule/app-ui @molecule/app-ui-react react
72
+ npm install -D @types/react
73
+ ```
74
+
75
+ ## API
76
+
77
+ ### Interfaces
78
+
79
+ #### `BillingActionState`
80
+
81
+ Async-state shape returned by `useStartCheckout` / `useCancelSubscription`.
82
+
83
+ ```typescript
84
+ interface BillingActionState<T> {
85
+ /** The most recent response from the action, or `null` before the first call. */
86
+ data: T | null
87
+
88
+ /** True while a request is in flight. */
89
+ loading: boolean
90
+
91
+ /** Last error thrown by the action, or `null` on success. */
92
+ error: Error | null
93
+ }
94
+ ```
95
+
96
+ #### `BillingStatus`
97
+
98
+ Snapshot of the signed-in user's current billing state. Returned by
99
+ `GET /api/billing/status` once the user is authenticated.
100
+
101
+ ```typescript
102
+ interface BillingStatus<TLimits = unknown> {
103
+ /** The user's `users.planKey` (`'free'`, `'stripeMonthly'`, etc.). */
104
+ planKey: string
105
+
106
+ /** Tier category — `'free'`, `'pro'`, `'team'`, or any app-defined extension. */
107
+ category: string
108
+
109
+ /** Display name (matches `PricingTierEntry.name` for the active tier). */
110
+ name: string
111
+
112
+ /** Tier-specific limits the user is currently entitled to. */
113
+ limits: TLimits
114
+
115
+ /** True when the user is on the registry default (free) tier. */
116
+ isFree: boolean
117
+ }
118
+ ```
119
+
120
+ #### `BillingStatusBadgeProps`
121
+
122
+ Props for `<BillingStatusBadge />`.
123
+
124
+ ```typescript
125
+ interface BillingStatusBadgeProps {
126
+ /**
127
+ * Optional callback invoked after a successful cancel. Most apps will
128
+ * navigate the user back to settings or refresh the page from here.
129
+ */
130
+ onCanceled?: () => void
131
+
132
+ /** Optional className applied to the outer wrapper. */
133
+ className?: string
134
+
135
+ /**
136
+ * Whether to render the cancel-subscription button when the user is
137
+ * on a paid tier. Defaults to `true`.
138
+ */
139
+ showCancel?: boolean
140
+ }
141
+ ```
142
+
143
+ #### `CancelResponse`
144
+
145
+ Response from `POST /api/billing/cancel`.
146
+
147
+ ```typescript
148
+ interface CancelResponse {
149
+ /** True when the cancellation request was accepted. */
150
+ canceled?: boolean
151
+
152
+ /** Localized error message when cancellation failed. */
153
+ error?: string
154
+ }
155
+ ```
156
+
157
+ #### `CheckoutResponse`
158
+
159
+ Response from `POST /api/billing/checkout`. Either `checkoutUrl` or `updated` is set.
160
+
161
+ ```typescript
162
+ interface CheckoutResponse {
163
+ /** Stripe Checkout URL — set when the user has no active subscription yet. */
164
+ checkoutUrl?: string
165
+
166
+ /** True when the user already had a subscription that was updated in place. */
167
+ updated?: boolean
168
+
169
+ /** Updated subscription metadata (only when `updated === true`). */
170
+ subscription?: {
171
+ expiresAt?: string
172
+ autoRenews?: boolean
173
+ }
174
+ }
175
+ ```
176
+
177
+ #### `LimitsItemProps`
178
+
179
+ Props for `<LimitsItem>`.
180
+
181
+ ```typescript
182
+ interface LimitsItemProps {
183
+ /** Row content (typically a translated label + number). */
184
+ children: ReactNode
185
+ /**
186
+ * Whether this feature is included in the tier. When `false`, the row
187
+ * renders with a muted line-through and a dash glyph instead of the
188
+ * check icon. Defaults to `true`.
189
+ */
190
+ included?: boolean
191
+ }
192
+ ```
193
+
194
+ #### `PricingPageProps`
195
+
196
+ Props for `<PricingPage />`.
197
+
198
+ ```typescript
199
+ interface PricingPageProps<TLimits = unknown> {
200
+ /**
201
+ * Optional billing-period selector. Defaults to `'month'`. Pass `'year'`
202
+ * to render the yearly column. The component falls back to whatever the
203
+ * tier provides when the requested period is missing.
204
+ */
205
+ period?: PricingTierPrice['period']
206
+
207
+ /**
208
+ * Optional render function for the tier-specific limits column. Defaults
209
+ * to a stacked checklist rendering numeric / boolean values with a green
210
+ * check glyph. Apps with rich limit shapes can supply a custom renderer.
211
+ */
212
+ renderLimits?: (limits: TLimits) => React.ReactNode
213
+
214
+ /**
215
+ * Optional override for the page-level heading translation key.
216
+ * Defaults to `'billing.pricing.heading'`.
217
+ */
218
+ headingKey?: string
219
+
220
+ /** Optional English fallback for the heading. Defaults to `'Choose your plan'`. */
221
+ headingDefault?: string
222
+
223
+ /**
224
+ * Optional sub-heading shown under the page heading. Pass `null` to
225
+ * suppress. Defaults to a translated "Pick the plan that fits…" line.
226
+ */
227
+ subheadingKey?: string | null
228
+ /** English fallback for the sub-heading. */
229
+ subheadingDefault?: string
230
+
231
+ /**
232
+ * Optional className applied to the outer wrapper, useful when embedding
233
+ * the page in an existing layout.
234
+ */
235
+ className?: string
236
+
237
+ /**
238
+ * Path the browser is sent to when an anonymous visitor clicks a paid
239
+ * tier's upgrade CTA. Defaults to `/login`. Set to `null` to disable
240
+ * the redirect (e.g. when the app handles the auth gate at a higher
241
+ * level via routing guards).
242
+ */
243
+ unauthenticatedRedirect?: string | null
244
+
245
+ /**
246
+ * Optional tier key to highlight as "most popular" — receives the
247
+ * elevated card variant, a popular-badge in the header, and a
248
+ * primary-tone CTA. When omitted (default), the highest-priced tier
249
+ * with a real stripePriceId for the selected period is auto-selected.
250
+ * Pass `null` to disable highlighting entirely.
251
+ */
252
+ popularTierKey?: string | null
253
+
254
+ /**
255
+ * Optional font-family stack applied inline to the page heading,
256
+ * tier names, and hero price. Defaults to a system-serif cascade
257
+ * (Georgia, Iowan Old Style, …) matching the flagship-app
258
+ * convention. Pass `null` to disable inline font and inherit from
259
+ * the theme.
260
+ */
261
+ headlineFontFamily?: string | null
262
+ }
263
+ ```
264
+
265
+ #### `PricingTierEntry`
266
+
267
+ One row of the public pricing table. Apps can declare any number of
268
+ `period` variants per tier (typically a `month` and `year` pair).
269
+
270
+ ```typescript
271
+ interface PricingTierEntry<TLimits = unknown> {
272
+ /** Stable slug used as a row key (`'free'`, `'pro'`, `'team'`). */
273
+ key: string
274
+
275
+ /** Display name shown in the page heading and CTA. */
276
+ name: string
277
+
278
+ /** Price variants for the tier. Order is presentation order. */
279
+ prices: PricingTierPrice[]
280
+
281
+ /** Tier-specific limits — render any/all of these on the comparison row. */
282
+ limits: TLimits
283
+
284
+ /** Whether the tier is billed per seat (rendered as a footnote). */
285
+ perSeat?: boolean
286
+ }
287
+ ```
288
+
289
+ #### `PricingTierPrice`
290
+
291
+ One billing period offered for a tier — typically `month` or `year` —
292
+ with the human-readable price string and the Stripe price ID. The price
293
+ ID may be `null` for the free tier (no Stripe product).
294
+
295
+ ```typescript
296
+ interface PricingTierPrice {
297
+ /** Billing cadence. */
298
+ period: 'month' | 'year'
299
+
300
+ /** Display string (e.g. `'$19/mo'`, `'$190/yr'`, `'$0'`). */
301
+ price: string
302
+
303
+ /**
304
+ * Stripe Price ID used as the line item when the user clicks Upgrade.
305
+ * `null` for free tiers and during local dev when env vars are unset.
306
+ */
307
+ stripePriceId: string | null
308
+
309
+ /** Optional savings tag on yearly variants (e.g. `'2 months free'`). */
310
+ savings?: string
311
+ }
312
+ ```
313
+
314
+ #### `PricingTiersResponse`
315
+
316
+ Response envelope for `GET /api/billing/tiers`.
317
+
318
+ ```typescript
319
+ interface PricingTiersResponse<TLimits = unknown> {
320
+ /** Tiers ordered as the API returns them (typically free → pro → team). */
321
+ data: PricingTierEntry<TLimits>[]
322
+ }
323
+ ```
324
+
325
+ ### Functions
326
+
327
+ #### `BillingStatusBadge(props)`
328
+
329
+ Compact billing-status display for the user's account/settings page.
330
+ Shows the current tier name and offers a cancel button on paid tiers.
331
+
332
+ ```typescript
333
+ function BillingStatusBadge(
334
+ props: BillingStatusBadgeProps,
335
+ ): ReactElement<unknown, string | JSXElementConstructor<any>> | null
336
+ ```
337
+
338
+ - `props` — Component props.
339
+
340
+ **Returns:** The rendered status badge.
341
+
342
+ #### `LimitsItem(props)`
343
+
344
+ Single row in a tier's feature list — a check (or em-dash) icon
345
+ followed by the row label. Use inside `<LimitsList>`.
346
+
347
+ ```typescript
348
+ function LimitsItem({
349
+ children,
350
+ included = true,
351
+ }: LimitsItemProps): ReactElement<unknown, string | JSXElementConstructor<any>>
352
+ ```
353
+
354
+ #### `LimitsList(props)`
355
+
356
+ Container for a tier's feature list. Apps use this in their
357
+ `renderLimits` prop with `<LimitsItem>` children to get the polished
358
+ stacked-checklist layout (check icon prefix, muted/primary colors,
359
+ spacing) that matches the rest of `<PricingPage />`.
360
+
361
+ ```typescript
362
+ function LimitsList({
363
+ children,
364
+ }: {
365
+ children: ReactNode
366
+ }): ReactElement<unknown, string | JSXElementConstructor<any>>
367
+ ```
368
+
369
+ #### `PricingPage(props)`
370
+
371
+ Renders the public pricing page. Fetches `/api/billing/tiers` on mount
372
+ and lays out one card per tier with the price for the selected period
373
+ and a CTA that starts a Stripe Checkout session via
374
+ `/api/billing/checkout`. Tiers without a Stripe priceId (e.g. the free
375
+ tier or local-dev) render a disabled CTA so users still see the row.
376
+
377
+ The highest-priced paid tier is highlighted as "most popular" by
378
+ default — the card uses the elevated variant, the header carries a
379
+ star badge, and its CTA renders in the primary color. Apps that want
380
+ a different tier in the spotlight can pass `popularTierKey` (or
381
+ `null` to suppress).
382
+
383
+ ```typescript
384
+ function PricingPage(
385
+ props: PricingPageProps<TLimits>,
386
+ ): ReactElement<unknown, string | JSXElementConstructor<any>>
387
+ ```
388
+
389
+ - `props` — Component props (see `PricingPageProps`).
390
+
391
+ **Returns:** The rendered pricing page.
392
+
393
+ #### `useBillingStatus()`
394
+
395
+ Fetch the signed-in user's current subscription state once on mount.
396
+ Returns 401 from the API when the user is not authenticated; consumers
397
+ should treat the absence of `data` as "anonymous → free tier".
398
+
399
+ ```typescript
400
+ function useBillingStatus(): UseHttpResult<BillingStatus<TLimits>>
401
+ ```
402
+
403
+ **Returns:** Async-state for the user's billing status.
404
+
405
+ #### `useCancelSubscription()`
406
+
407
+ Cancel the user's active subscription at the end of the current
408
+ billing period. Returns the response from the bonded payment provider.
409
+
410
+ ```typescript
411
+ function useCancelSubscription(): BillingActionState<CancelResponse> & {
412
+ cancel: () => Promise<CancelResponse | null>
413
+ }
414
+ ```
415
+
416
+ **Returns:** Async-state plus a `cancel` function.
417
+
418
+ #### `usePricingTiers()`
419
+
420
+ Fetch the public pricing data once on mount. Useful inside a
421
+ pricing/upgrade page where the tiers are needed before render.
422
+
423
+ ```typescript
424
+ function usePricingTiers(): UseHttpResult<PricingTiersResponse<TLimits>>
425
+ ```
426
+
427
+ **Returns:** Async-state for the pricing tiers response.
428
+
429
+ #### `useStartCheckout()`
430
+
431
+ Start a Stripe Checkout session for a given Stripe price ID. The
432
+ returned `start(priceId)` posts to `/api/billing/checkout`; the
433
+ response is either `{ checkoutUrl }` (for new subscribers — redirect
434
+ the browser) or `{ updated: true }` (for existing subscribers —
435
+ refresh the page).
436
+
437
+ ```typescript
438
+ function useStartCheckout(): BillingActionState<CheckoutResponse> & {
439
+ start: (priceId: string) => Promise<CheckoutResponse | null>
440
+ }
441
+ ```
442
+
443
+ **Returns:** Async-state plus a `start` function.
444
+
445
+ ## Injection Notes
446
+
447
+ ### Requirements
448
+
449
+ Peer dependencies:
450
+
451
+ - `@molecule/app-http` ^1.0.1
452
+ - `@molecule/app-react` ^1.0.1
453
+ - `@molecule/app-ui` ^1.0.1
454
+ - `@molecule/app-ui-react` ^1.0.1
455
+ - `react` ^18.0.0 || ^19.0.0
456
+
457
+ ### Runtime Dependencies
458
+
459
+ - `@molecule/app-http`
460
+ - `@molecule/app-react`
461
+ - `@molecule/app-ui`
462
+ - `@molecule/app-ui-react`
463
+ - `react`
464
+
465
+ **Name collision:** `PricingPage` is also exported by
466
+ `@molecule/app-pricing-page-react` (a tier-card grid with a monthly/yearly
467
+ toggle driven by `usePricingTiers()`). THIS package's `<PricingPage>` is the
468
+ entitlements-kit table with a `renderLimits` prop + compound
469
+ `<LimitsList>`/`<LimitsItem>` and a built-in `<BillingStatusBadge>` — import
470
+ from `@molecule/app-billing-react` when you are wiring
471
+ `@molecule/api-entitlements`. If you import both packages, alias one to avoid
472
+ the clash.
473
+
474
+ ## E2E Tests
475
+
476
+ Integration checklist — drive the real UI (live preview, no mocks), adapt
477
+ each item to this app's actual screens/flows, and check every box off one
478
+ by one. A box you can't check is an integration bug to fix — not a skip:
479
+
480
+ - [ ] The pricing route renders every tier from `/api/billing/tiers` with name,
481
+ price, and per-tier limits — no empty table, no `undefined` cells.
482
+ - [ ] The signed-in user's CURRENT tier is visibly marked (highlighted / "current
483
+ plan") and its Upgrade button is disabled or absent.
484
+ - [ ] Clicking Upgrade on another tier posts to `/api/billing/checkout` and the
485
+ page follows the returned checkout handoff (button is not a dead click).
486
+ - [ ] `<BillingStatusBadge />` on the account screen shows the live tier, and its
487
+ cancel action updates the shown status after confirmation.
488
+ - [ ] A signed-out visitor can still view the public pricing table.
489
+ - [ ] If the tiers endpoint fails, the page shows a visible error state — not a
490
+ blank page or spinner forever.
491
+
492
+ ## Translations
493
+
494
+ Translation strings are provided by `@molecule/app-locales-billing`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@molecule/app-billing-react",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "React pricing page + checkout flow that consumes /api/billing/* (provided by @molecule/api-entitlements + @molecule/api-payments-stripe)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -17,7 +17,8 @@
17
17
  }
18
18
  },
19
19
  "files": [
20
- "dist"
20
+ "dist",
21
+ "README.md"
21
22
  ],
22
23
  "keywords": [
23
24
  "molecule",
@@ -27,18 +28,23 @@
27
28
  "react"
28
29
  ],
29
30
  "license": "Apache-2.0",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/molecule-dev/molecule.git",
34
+ "directory": "packages/app/features/billing-react"
35
+ },
30
36
  "peerDependencies": {
31
- "@molecule/app-http": "^1.0.0",
32
- "@molecule/app-react": "^1.0.0",
33
- "@molecule/app-ui": "^1.0.0",
34
- "@molecule/app-ui-react": "^1.0.0",
37
+ "@molecule/app-http": "^1.0.1",
38
+ "@molecule/app-react": "^1.0.1",
39
+ "@molecule/app-ui": "^1.0.1",
40
+ "@molecule/app-ui-react": "^1.0.1",
35
41
  "react": "^18.0.0 || ^19.0.0"
36
42
  },
37
43
  "devDependencies": {
38
- "@molecule/app-http": "1.0.0",
39
- "@molecule/app-react": "1.0.0",
40
- "@molecule/app-ui": "1.0.0",
41
- "@molecule/app-ui-react": "1.0.0",
44
+ "@molecule/app-http": "1.0.1",
45
+ "@molecule/app-react": "1.0.1",
46
+ "@molecule/app-ui": "1.0.1",
47
+ "@molecule/app-ui-react": "1.0.1",
42
48
  "@types/node": "26.1.2",
43
49
  "@types/react": "19.2.17",
44
50
  "react": "19.2.8",