@openrails/billing-ui 0.230.0 → 0.233.2

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 (65) hide show
  1. package/CHANGELOG.md +103 -9
  2. package/README.md +88 -39
  3. package/dist/account/account-billing.d.ts +3 -3
  4. package/dist/account/cancel-dialog.d.ts +2 -2
  5. package/dist/account/format.d.ts +4 -4
  6. package/dist/account/payment-methods-panel.d.ts +7 -9
  7. package/dist/account/section.d.ts +3 -1
  8. package/dist/checkout-page.d.ts +6 -1
  9. package/dist/client/client.d.ts +76 -62
  10. package/dist/client/generated/currencies.d.ts +1 -0
  11. package/dist/client/generated/error-codes.d.ts +7 -0
  12. package/dist/client/generated/routes.d.ts +775 -0
  13. package/dist/client/generated/wire.d.ts +682 -0
  14. package/dist/client/index.d.ts +3 -3
  15. package/dist/client/types.d.ts +425 -379
  16. package/dist/client-DXUPzA8C.js +889 -0
  17. package/dist/client-DXUPzA8C.js.map +1 -0
  18. package/dist/client.js +2 -3
  19. package/dist/components/methods.d.ts +3 -3
  20. package/dist/config-BQELtBh4.js +341 -0
  21. package/dist/config-BQELtBh4.js.map +1 -0
  22. package/dist/frame.d.ts +1 -1
  23. package/dist/index.d.ts +4 -4
  24. package/dist/index.js +1447 -1539
  25. package/dist/index.js.map +1 -1
  26. package/dist/lib/billing.d.ts +14 -0
  27. package/dist/lib/money.d.ts +1 -0
  28. package/dist/lib/period.d.ts +0 -2
  29. package/dist/lib/rail-meta.d.ts +4 -4
  30. package/dist/locales/de.js +11 -8
  31. package/dist/locales/de.js.map +1 -1
  32. package/dist/locales/en.d.ts +8 -5
  33. package/dist/locales/en.js +11 -8
  34. package/dist/locales/en.js.map +1 -1
  35. package/dist/locales/es.js +11 -8
  36. package/dist/locales/es.js.map +1 -1
  37. package/dist/locales/ja.js +11 -8
  38. package/dist/locales/ja.js.map +1 -1
  39. package/dist/locales/ko.js +11 -8
  40. package/dist/locales/ko.js.map +1 -1
  41. package/dist/locales/zh.js +11 -8
  42. package/dist/locales/zh.js.map +1 -1
  43. package/dist/psp.d.ts +10 -21
  44. package/dist/react/config.d.ts +34 -0
  45. package/dist/react/context.d.ts +5 -8
  46. package/dist/react/hooks.d.ts +20 -14
  47. package/dist/react/index.d.ts +1 -0
  48. package/dist/react/provider.d.ts +1 -9
  49. package/dist/react/remote.d.ts +0 -1
  50. package/dist/react.js +24 -28
  51. package/dist/react.js.map +1 -1
  52. package/dist/save-payment-method.d.ts +1 -1
  53. package/dist/source.d.ts +0 -9
  54. package/dist/styles.css +1 -1
  55. package/dist/types.d.ts +114 -55
  56. package/docs/payment-form-contract.md +23 -40
  57. package/package.json +3 -3
  58. package/dist/client/generated/openrails-routes.d.ts +0 -2
  59. package/dist/client-DhG2jv34.js +0 -771
  60. package/dist/client-DhG2jv34.js.map +0 -1
  61. package/dist/errors-T-PRm3vV.js +0 -54
  62. package/dist/errors-T-PRm3vV.js.map +0 -1
  63. package/dist/hooks-DLFF-zx5.js +0 -285
  64. package/dist/hooks-DLFF-zx5.js.map +0 -1
  65. package/docs/1055-nmi-host-integration.md +0 -28
package/CHANGELOG.md CHANGED
@@ -1,6 +1,101 @@
1
1
  # Changelog
2
2
 
3
- ## Hosted checkout sessions (openrails#1124)
3
+ ## The public catalog is under /catalog
4
+
5
+ - `listProducts()` reads `GET /catalog/products` (was `GET /products`).
6
+
7
+ ## One public configuration
8
+
9
+ - `getConfig()` reads `GET /config`: the mount's `capabilities`, the currency
10
+ registry (`currencies`) and the merchant's `payment` setup (its PSPs and
11
+ Solana network and tokens; null without a merchant). It replaces
12
+ `getCheckoutConfig()` and `listCurrencies()`: `checkoutConfigSchema` and
13
+ `CheckoutConfig` are `publicConfigSchema` and `PublicConfig` (its `payment`
14
+ is a `PaymentConfig`), and `currencyRegistrySchema` is removed.
15
+ `client.currencies` stays the pinned registry.
16
+ - `BillingProvider` fetches it once and shares it: `useConfig()` reads it,
17
+ `useCurrencyScales()` layers the pinned registry and host overrides over its
18
+ currencies, and the account panels use both.
19
+ - `AccountBilling` and `PaymentMethodsPanel` take their PSPs from it; their
20
+ `psps` prop is removed. "Add card" waits for it, says so when it fails (with
21
+ a retry) or every card PSP is temporarily unavailable, and asks again after
22
+ the PSP's `retry_after`.
23
+ - `PspConfig` has `status` and `retry_after`; a temporarily unavailable PSP
24
+ saves no card (`cardSetupDriver` is null).
25
+
26
+ ## Prices come with their products
27
+
28
+ - `listPrices` is removed with `GET /prices`: `listProducts()` returns every
29
+ product on sale with its current prices. Filter by currency, product or
30
+ renewal in the browser.
31
+
32
+ ## Paying on a customer surface
33
+
34
+ - `client.checkoutSource(id, { customerBase })` reads and pays a checkout
35
+ session as its signed-in customer at a customer surface's
36
+ `/checkout-sessions/{id}` (for example a host's
37
+ `/api/v1/merchants/acme/billing/me`), so its saved cards pay. Without
38
+ `customerBase` the session id alone pays at `/checkout-sessions/{id}` as
39
+ before. `<CheckoutPage customerBase>` passes it through.
40
+
41
+ ## 1.0.0
42
+
43
+ Ships with OpenRails v1.0.0; use the two at the same version. Everything a host
44
+ changes is in [Migrating to v1](../../docs/migrating-to-v1.md).
45
+
46
+ Checkout
47
+
48
+ - A checkout session is the only browser purchase. `createCheckoutSession()`
49
+ mints at `/me/checkout-sessions` and returns a `CheckoutSessionLink` (`url`
50
+ null without a payment page); `client.checkoutSource(id)` is the source of
51
+ `<Checkout>`, `<CheckoutModal>` and `<CheckoutPage>`.
52
+ - The session document's `rails` is `options` (`PaymentOption`, with the
53
+ option's `psp_id`). A rail's own step is one `next_action`
54
+ (`redirect_to_url`, `solana_pay`); a card challenge is `operation`. Saved
55
+ methods carry `card` (`brand`, `last4`, `exp_month`, `exp_year`) and no
56
+ `default`. Fields OpenRails sends as null read as `undefined`.
57
+ - `PayRequest` carries one `billing_details` (`name`, `email`, `phone`,
58
+ `address` with `postal_code` and `country`) in place of the flat
59
+ `name_on_card`, `zip`, `country`, `last_four`, `card_type` and `expiry_date`.
60
+ - Stripe Elements runs on a session inside a `BillingProvider`: the card is
61
+ saved through the customer's client and the session pays it by id.
62
+ - Removed: `createHttpSource`, `CheckoutSourceError`, `checkoutRails`,
63
+ `CheckoutRailOffer`, `PaymentRailOption`.
64
+
65
+ Client
66
+
67
+ - Lists are cursor pages: `listSubscriptions`, `listPaymentMethods`,
68
+ `listPayments` and `listInvoices` take `cursor` and answer `next_cursor`;
69
+ there is no `total`. The hooks page with `nextCursor`.
70
+ - `cancelSubscription`, `resumeSubscription` and
71
+ `setSubscriptionPaymentMethod` resolve to the `Subscription`. A Solana cancel
72
+ or tier change answers `next_action` (`solana_sign_transactions`); sign it
73
+ with `signWalletAction` and repeat the call with `signature`. The separate
74
+ Solana cancel and tier-change methods are removed.
75
+ - `addPaymentMethod` takes `NewCard` with `psp_id`, a `payment_token` or
76
+ `card`, and `billing_details`.
77
+ - There is no default card: `setDefaultPaymentMethod` is
78
+ `setCollectionPaymentMethod`, the hook's `setDefault` is `setCollection`, the
79
+ change event is `payment_method.collection_changed`, and the
80
+ `AccountBilling` / `PaymentMethodsPanel` prop `defaultCurrency` is
81
+ `collectionCurrency`. `PaymentMethod` lists `collection_currencies`.
82
+ - `getSolanaConfig()` is `getCheckoutConfig()` (the network is
83
+ `solana.network`); `listSolanaTokens` takes `{ priceId, wallet }`.
84
+ - `getStatus()` and `BillingStatus` are removed: read `/me/entitlements` and
85
+ `listSubscriptions()`.
86
+ - `listPrices` filters by `productId` and `autoRenew`. A `Price` has
87
+ `product_id`, `archived`, `access_duration_hours` (null: for good) and
88
+ `auto_renew`; `type`, `recurring.interval` and `active` are gone.
89
+ - `Payment` has `kind`, a typed `status` and the `price` it bought; a
90
+ subscription's status is `canceled`.
91
+ - A `TierChange` has no `mode`, `url` or `payment`: a redirect is
92
+ `next_action.url`. Generated list fields are `T[]`, never `null`.
93
+ - A subscription's `access` windows are `starts_at` and `ends_at`.
94
+ - A `Product` has no `catalog_id`. A refusal's code fixes its status:
95
+ `changeTier` answers a registered code for each condition, and a declined
96
+ tier-change charge is `card_declined` with `decline_reason`.
97
+
98
+ ## Hosted checkout sessions
4
99
 
5
100
  - OpenRails serves the checkout session; hosts no longer write
6
101
  `/api/v1/checkout/sessions` routes. `client.createCheckoutSession()` mints
@@ -14,7 +109,7 @@
14
109
  - `<Checkout onRedirect>` replaces the top-window navigation of redirect rails.
15
110
  - The session document gains `embed_origin`.
16
111
 
17
- ## Server card entry (openrails#1129)
112
+ ## Server card entry
18
113
 
19
114
  - Driver `card` (a PSP declared `card_entry: server`, flow `card`): `<Checkout>`
20
115
  and `SavePaymentMethod` render plain card inputs with the Collect.js form's
@@ -26,7 +121,7 @@
26
121
  - `cardSetupDriver` may return `"card"`; `NewCard` takes `payment_token` or
27
122
  `card`, never both.
28
123
 
29
- ## Catalog and plan-change client (openrails#1128)
124
+ ## Catalog and plan-change client
30
125
 
31
126
  - `@openrails/billing-ui/client` gains the public catalog (`listProducts`,
32
127
  `listPrices`, `listCurrencies`), plan change (`previewTierChange`,
@@ -40,7 +135,7 @@
40
135
  `"change_tier"`, `onChange` `subscription.tier_changed`), which resolves to
41
136
  the `TierChange` or a `BillingError`.
42
137
 
43
- ## Explicit payment method, fail-fast errors (openrails#1087, #1088)
138
+ ## Explicit payment method, fail-fast errors
44
139
 
45
140
  - The checkout pre-selects the customer's default card (`PaymentMethod.default`,
46
141
  `SavedPaymentMethod.default`) and always sends the chosen card's id.
@@ -55,13 +150,13 @@
55
150
  (2 s). New `isServerError`; a 5xx without an error envelope has code
56
151
  `server_error`.
57
152
 
58
- ## One-click card subscriptions (openrails#1085)
153
+ ## One-click card subscriptions
59
154
 
60
155
  - A checkout host relays a card subscription's pay request with OpenRails'
61
156
  `CreateCheckoutSessionRequest.Confirm`; a new Collect.js token subscribes in
62
157
  one call. No package API change.
63
158
 
64
- ## Advertised checkout rails (openrails#1078)
159
+ ## Advertised checkout rails
65
160
 
66
161
  - Breaking: `checkoutRails(offers)` takes OpenRails' checkout options alone.
67
162
  Each option carries the `driver` and `public_config` OpenRails derived from
@@ -80,7 +175,7 @@ generated contract.
80
175
  ## 0.9.0
81
176
 
82
177
  One embedded card panel for one-time purchases and subscriptions, on every
83
- card PSP (openrails#1064). Requires OpenRails v0.164.0.
178
+ card PSP. Requires OpenRails v0.164.0.
84
179
 
85
180
  - Stripe Elements checkout rail (`driver: "stripe_elements"`, from a PSP with
86
181
  `flow: "elements"`): saved Stripe cards or a new card in the page, 3-D
@@ -127,8 +222,7 @@ and never branches on Stripe, NMI or any other provider.
127
222
  `getPaymentAuthentication`, `confirmPaymentAuthentication`.
128
223
  - Breaking: `AccountBilling`/`PaymentMethodsPanel` take `psps` (and optional
129
224
  `cardSetupReturnURL`) instead of `cardSetup`; `CardSetupConfig` is removed.
130
- Stripe card setup needs OpenRails serving Stripe's `publishable_key`
131
- (openrails#1062).
225
+ Stripe card setup needs OpenRails serving Stripe's `publishable_key`.
132
226
 
133
227
  ## 0.7.0
134
228
 
package/README.md CHANGED
@@ -35,21 +35,63 @@ import { Checkout, CheckoutFrame } from "@openrails/billing-ui"
35
35
  import "@openrails/billing-ui/styles.css"
36
36
 
37
37
  const billing = createBillingClient({ fetch: auth.authFetch })
38
- const session = await billing.createCheckoutSession({ priceKey: "pro-monthly" })
38
+ const session = await billing.createCheckoutSession({
39
+ productKey: "pro",
40
+ priceKey: "monthly",
41
+ })
39
42
 
40
- // A shared payment page (Config.HTTP.Checkout.PageURL) answers with a url:
43
+ // A shared payment page (Config.Checkout.PageURL) answers with a url:
41
44
  <CheckoutFrame url={session.url} theme="dark" onComplete={() => refetchAccess()} />
42
45
  // Without one, the app renders the checkout itself:
43
46
  <Checkout source={billing.checkoutSource(session.id)} onComplete={() => refetchAccess()} />
44
47
  ```
45
48
 
46
49
  `onComplete` is a hint: confirm access from your own authenticated API before
47
- granting anything. Pass `successUrl` to return the buyer from a redirect rail
48
- (CCBill, Stripe hosted); it must be on one of your app's return origins.
50
+ granting anything. Pass `successUrl` to return the buyer from a redirect step
51
+ (Stripe's hosted page); it must be on one of your app's return origins. A
52
+ server that starts checkout itself mints the session with the Go
53
+ `Client.CreateCheckoutSession` and hands the browser its `id` and `url`.
54
+
55
+ For prepaid credits, a fixed pack is another product/price selection. A
56
+ `customer_amount` price also accepts `amount` when minting. Collect the amount
57
+ on your product page before opening checkout; the resulting session shows and
58
+ charges that exact amount, and its payment form cannot change it:
59
+
60
+ ```tsx
61
+ import { decimalToAmount } from "@openrails/billing-ui/client"
62
+
63
+ // `depositPrice` is in a product from listProducts; `currency` from getConfig().currencies.
64
+ // `enteredAmount` is the text from an <input inputMode="decimal">, e.g. "100".
65
+ const amount = decimalToAmount(enteredAmount, currency.decimals)
66
+ const bounds = depositPrice.customer_amount
67
+ if (
68
+ !amount ||
69
+ !bounds ||
70
+ BigInt(amount) < BigInt(bounds.min_amount) ||
71
+ BigInt(amount) > BigInt(bounds.max_amount)
72
+ ) {
73
+ throw new Error("Choose an amount within the deposit limits")
74
+ }
75
+ const session = await billing.createCheckoutSession({
76
+ priceId: depositPrice.id,
77
+ amount, // USD 100 becomes "100000000"; no floating-point conversion
78
+ })
79
+ ```
80
+
81
+ OpenRails validates the bounds and whole currency minor units before accepting
82
+ payment. Fixed prices reject `amount`; their pack benefit can be greater than
83
+ the purchase price for a bulk discount. Customer-selected deposits currently
84
+ use NMI or Stripe. Changing the selected amount requires minting a new session.
85
+
86
+ A rail's own step is the pay result's `next_action`: `redirect_to_url` (an
87
+ https page, opened in the top window) or `solana_pay` (a `solana:` link shown
88
+ as a QR code). A card challenge is its `operation`, authenticated in the page.
89
+ Stripe Elements needs the buyer's billing client (`<Checkout>` inside a
90
+ `BillingProvider`): the shared payment page offers token rails only.
49
91
 
50
92
  The payment host serves `<CheckoutPage>` from one HTML entry at `PageURL`,
51
93
  behind its adapter's `CheckoutFramePolicy` (only the sites in
52
- `Config.HTTP.Checkout.EmbedOrigins` may frame it):
94
+ `Config.Checkout.EmbedOrigins` may frame it):
53
95
 
54
96
  ```tsx
55
97
  import { BillingUiProvider, CheckoutPage } from "@openrails/billing-ui"
@@ -83,29 +125,34 @@ const billing = createBillingClient({ baseUrl: "/billing/v1", fetch: auth.authFe
83
125
  <BillingProvider client={billing} onChange={() => queryClient.invalidateQueries({ queryKey: ["billing"] })}>
84
126
  <AccountBilling
85
127
  plansHref="/plans"
86
- psps={checkoutConfig.psps} // OpenRails checkout config; enables "Add card"
87
- defaultCurrency="USD" // enables "Make default"
88
- sendSolanaTransaction={(tx) => wallet.signAndSend(tx)} // Solana-rail cancel
128
+ collectionCurrency="USD" // offers "Use for invoices"
129
+ sendSolanaTransaction={(tx) => wallet.signAndSend(tx)} // signs a Solana cancel
89
130
  />
90
131
  </BillingProvider>
91
132
  </BillingUiProvider>
92
133
  ```
93
134
 
135
+ The panels read OpenRails' public configuration (`GET /config`) themselves,
136
+ once per `BillingProvider`: "Add card" offers each PSP of its `payment` that
137
+ saves a card in the page, and says so while the configuration fails or its
138
+ card PSPs are temporarily unavailable; amounts use its currency registry over
139
+ the pinned copy. `useConfig()` and `useCurrencyScales()` read the same copy
140
+ for host components.
141
+
94
142
  ## Catalog and plan changes
95
143
 
96
144
  The client also reads the public catalog and changes a subscription's plan.
97
- No UI ships for these; hosts render their own. The catalog, currency and
98
- Solana reads are public routes, served only when the host mounts OpenRails'
99
- checkout routes.
145
+ No UI ships for these; hosts render their own. The catalog and Solana reads
146
+ are public routes, served only when the host mounts OpenRails' checkout
147
+ routes; `GET /config` is always served.
100
148
 
101
149
  | Call | Route |
102
150
  | --------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
103
- | `listProducts()`, `listPrices({ currency, product, type })` | `GET /products`, `GET /prices` |
104
- | `listCurrencies()` (`client.currencies` is the pinned copy) | `GET /currencies` |
151
+ | `listProducts()` (each product embeds its prices on sale) | `GET /catalog/products` |
152
+ | `getConfig()` (capabilities, `currencies`, `payment` with PSPs, `solana.network` and tokens; `client.currencies` is the pinned registry) | `GET /config` |
105
153
  | `previewTierChange(id, priceId)` | `POST /me/subscriptions/{id}/change-tier/preview` |
106
- | `changeTier(id, { priceId, idempotencyKey })` | `POST /me/subscriptions/{id}/change-tier` |
107
- | `getSolanaConfig()`, `listSolanaTokens({ priceId, wallet })` | `GET /solana/config`, `GET /solana/tokens` |
108
- | `prepareSolanaTierChange(id, newPriceId)`, `confirmSolanaTierChange(id, { signature, newPriceId })` | `POST /me/subscriptions/{id}/solana-tier-change`, `.../confirm` |
154
+ | `changeTier(id, { priceId, idempotencyKey, signature? })` | `POST /me/subscriptions/{id}/change-tier` |
155
+ | `listSolanaTokens({ priceId, wallet })` | `GET /solana/tokens` |
109
156
 
110
157
  ```ts
111
158
  const preview = await billing.previewTierChange(sub.id, price.id) // amount_due_now, effective
@@ -121,30 +168,32 @@ const change = await billing.changeTier(sub.id, {
121
168
  lost response), so OpenRails replays the stored result instead of charging
122
169
  twice. `requires_action` carries `operation_id` for `authenticatePayment`;
123
170
  replay the same key afterwards.
124
- - Solana rail: the wallet signs and sends `prepareSolanaTierChange`'s
125
- `transaction`; pass its signature to `confirmSolanaTierChange`. Keep the
126
- signature until the confirm succeeds; confirming again replays the result.
171
+ - A rail that needs the customer's own step answers `requires_action` with a
172
+ `next_action`. Solana's is `solana_sign_transactions`:
173
+ `signWalletAction(change.next_action, send)` signs and sends its
174
+ transactions and returns the signature; repeat `changeTier` with the same
175
+ key and `signature`. `cancelSubscription(id, { reason, signature? })` works
176
+ the same way: its answer is the subscription, unchanged and carrying
177
+ `next_action` until the signed cancel lands. The hooks run this when given
178
+ `sendTransaction`.
127
179
 
128
180
  ## Provider-neutral flows
129
181
 
130
- Hosts pass OpenRails's browser PSP configs (`GET /checkout-config`, `psps`)
131
- through unchanged; the PSP's `flow` and public `config` pick the browser flow
182
+ OpenRails's browser PSP configs (`GET /config`'s `payment.psps`) pick the
183
+ browser flow from each PSP's `flow` and public `config`
132
184
  (Collect.js, native card inputs, Stripe Elements, redirect, wallet). Hosts never branch on a
133
185
  provider:
134
186
 
135
- - `checkoutRails(offers)` and `savedMethodsFor(methods, rails)` build a
136
- `CheckoutSource`'s `rails` and `saved_methods` (the default card first, pre-selected; paying sends
137
- the chosen card's id explicitly).
138
- `offers` is OpenRails' `ListCheckoutRailOptions` result, passed through: it
139
- lists exactly the armed PSPs whose rail can make this sale (Solana when it is
140
- configured, never CCBill for a new subscription), each with its `driver` and
141
- `public_config`. An offer without a driver is not rendered.
187
+ - A session's `options` are exactly the armed PSPs whose rail can make this
188
+ sale (Solana when it is configured, never CCBill),
189
+ each with its `driver` and `public_config`; `saved_methods` are the buyer's
190
+ cards on them, newest first (paying sends the chosen card's id explicitly).
142
191
  Card rails (`collect_js`, `card`, `stripe_elements`) render one panel: saved cards,
143
- an inline new card and one Pay/Subscribe button, which is the payer's
192
+ an inline new card and one Pay/Subscribe button, which is the customer's
144
193
  confirmation of the displayed terms. No provider chooser with one rail.
145
194
  - Inside a `BillingProvider`, a new card is always saved to the account first
146
195
  and the source is paid with `payment_method_id`; `requires_action` results
147
- carrying `operation_id` run 3-D Secure in the page. A `failed` result stays
196
+ carrying an `operation` run 3-D Secure in the page. A `failed` result stays
148
197
  on the panel with `failure.message` next to its `field`, so the buyer can
149
198
  pick another card; the host gives each new attempt a new idempotency key.
150
199
  - Only PSPs with `checkout !== false` (`checkoutPsps`) take new cards; others
@@ -165,13 +214,13 @@ provider:
165
214
  `CancelSubscriptionDialog`; `AccountBilling` stacks them.
166
215
  - `renderSubscriptionFooter={(s) => ...}` adds host content under a
167
216
  subscription row; `useBillingRefresh()` refetches after host-side changes.
168
- - Hooks: `useSubscriptions` (`cancel`, `cancelOnChain`, `resume`,
169
- `setPaymentMethod`, `changeTier`, per-row `pending`), `usePaymentMethods`
170
- (`add`, `remove`, `setDefault`), `usePayments` (offset pages), `useProducts`
171
- (the catalog). Actions resolve to `null` or a `BillingError`; they never
172
- throw. `changeTier` resolves to the `TierChange` instead of `null`. Cancel
173
- and resume are queued by OpenRails (202); hooks re-read the subscription
174
- until the change shows.
217
+ - Hooks: `useSubscriptions` (`cancel`, `resume`, `setPaymentMethod`,
218
+ `changeTier`, per-row `pending`, `nextCursor`), `usePaymentMethods` (`add`,
219
+ `remove`, `setCollection`), `usePayments` (cursor pages), `useProducts` (the
220
+ catalog).
221
+ Actions resolve to `null` or a `BillingError`; they never throw.
222
+ `changeTier` resolves to the `TierChange` instead of `null`. Cancel, resume
223
+ and the card change answer the subscription, which replaces the row.
175
224
  - Money is exact: `/me` amounts are int64 native-unit strings, scaled by the
176
225
  OpenRails currency registry (`currencies` option to extend it).
177
226
  - Messages: English is complete and the fallback; `messages` layers bundles,
@@ -201,8 +250,8 @@ sale and saved card on a credential-less NMI PSP).
201
250
 
202
251
  ```sh
203
252
  pnpm test:e2e # Playwright against the real server
204
- pnpm contract # regenerate src/client/generated from the in-repo OpenRails
205
- pnpm contract:check # fail if the generated contract is stale
253
+ pnpm contract # regenerate OpenRails' contract files, src/client/generated among them
254
+ pnpm contract:check # fail if a generated contract file is stale
206
255
  ```
207
256
 
208
257
  `e2e/openrails/account.spec.ts` drives the packaged `AccountBilling` (built from
@@ -3,9 +3,9 @@ import { SendSolanaTransaction } from '../client/client';
3
3
  import { PaymentMethodsPanelProps } from './payment-methods-panel';
4
4
  import { SubscriptionsPanelProps } from './subscriptions-panel';
5
5
  export interface AccountBillingProps {
6
- psps?: PaymentMethodsPanelProps["psps"];
7
6
  cardSetupReturnURL?: PaymentMethodsPanelProps["cardSetupReturnURL"];
8
- defaultCurrency?: string;
7
+ /** Offers to make a card the one that collects this currency's invoices. */
8
+ collectionCurrency?: string;
9
9
  /** Billing country to preselect when adding a card. */
10
10
  defaultCountry?: string;
11
11
  sendSolanaTransaction?: SendSolanaTransaction;
@@ -16,4 +16,4 @@ export interface AccountBillingProps {
16
16
  className?: string;
17
17
  }
18
18
  /** Subscriptions, payment methods and history in one column. */
19
- export declare function AccountBilling({ psps, cardSetupReturnURL, defaultCurrency, defaultCountry, sendSolanaTransaction, plansHref, renderSubscriptionFooter, historyPageSize, appearance, className, }: AccountBillingProps): import("react").JSX.Element;
19
+ export declare function AccountBilling({ cardSetupReturnURL, collectionCurrency, defaultCountry, sendSolanaTransaction, plansHref, renderSubscriptionFooter, historyPageSize, appearance, className, }: AccountBillingProps): import("react").JSX.Element;
@@ -7,12 +7,12 @@ export interface CancelSubscriptionDialogProps {
7
7
  onOpenChange: (open: boolean) => void;
8
8
  /** Subscription display name. */
9
9
  name: string;
10
- /** On-chain (Solana) cancel: no feedback, the wallet signs. */
10
+ /** On-chain (Solana) cancel: the wallet signs after the reason. */
11
11
  onChain?: boolean;
12
12
  /** In-flight stage, when cancelling. */
13
13
  pending?: SubscriptionAction;
14
14
  /** Resolves null on success; the dialog then closes. */
15
- onConfirm: (feedback: string) => Promise<BillingError | null>;
15
+ onConfirm: (reason: string) => Promise<BillingError | null>;
16
16
  appearance?: CheckoutAppearance;
17
17
  }
18
18
  export declare function CancelSubscriptionDialog({ open, onOpenChange, name, onChain, pending, onConfirm, appearance, }: CancelSubscriptionDialogProps): React.JSX.Element;
@@ -1,10 +1,10 @@
1
- import { CardSummary, CurrencyScales, Payment, Subscription, SubscriptionPrice } from '../client/types';
1
+ import { CardSummary, CurrencyScales, Payment, Price, Subscription } from '../client/types';
2
2
  import { Translator } from '../i18n/messages';
3
3
  export declare function formatDate(value: string | null | undefined, locale?: string): string | null;
4
4
  /** Exact money, or null while the currency's scale is unknown. */
5
5
  export declare function formatMoney(amount: string | null | undefined, currency: string, scales: CurrencyScales, locale?: string): string | null;
6
6
  /** "every 30 days", "30 days of access" or "one-time". */
7
- export declare function intervalLabel(price: SubscriptionPrice | null | undefined, m: Translator): string | null;
7
+ export declare function intervalLabel(price: Pick<Price, "access_duration_hours" | "billing_interval_hours"> | null | undefined, m: Translator): string | null;
8
8
  export declare function subscriptionName(s: Subscription, { t }: Translator): string;
9
9
  /** What a payment bought: the product's name, else its kind, plus its cadence. */
10
10
  export declare function paymentItem(p: Payment, m: Translator): {
@@ -16,9 +16,9 @@ export declare function brandName(brand: string | null | undefined, fallback: st
16
16
  export declare function cardText(card: CardSummary | null | undefined, m: Translator): string;
17
17
  export declare function expiry(card: CardSummary | null | undefined): string | null;
18
18
  export declare const LIVE_STATUSES: Set<string>;
19
- /** Still grants access: live, or cancelled with the paid period running. */
19
+ /** A current subscription, or a canceled subscription retaining paid access. */
20
20
  export declare const isLive: (s: Subscription) => boolean;
21
- /** Access ends at period end and nothing renews it. */
21
+ /** Future billing has stopped or cancellation is scheduled. */
22
22
  export declare const isEnding: (s: Subscription) => boolean;
23
23
  export type StatusTone = "success" | "warning" | "destructive" | "neutral";
24
24
  export declare const statusTone: (status: string) => StatusTone;
@@ -1,19 +1,17 @@
1
1
  import { CheckoutAppearance } from '../appearance';
2
- import { PspConfig } from '../psp';
3
2
  import * as React from "react";
4
3
  export interface PaymentMethodsPanelProps {
5
- /**
6
- * OpenRails's browser PSP configs (checkout config). "Add card" offers
7
- * each one a card can be saved with in the page.
8
- */
9
- psps?: readonly PspConfig[];
10
4
  /** Return target after off-page card verification; see `SavePaymentMethod`. */
11
5
  cardSetupReturnURL?: (setupId: string) => string;
12
- /** Enables "Make default" for this currency's invoice collection. */
13
- defaultCurrency?: string;
6
+ /** Offers to make a card the one that collects this currency's invoices. */
7
+ collectionCurrency?: string;
14
8
  /** Billing country to preselect when adding a card. */
15
9
  defaultCountry?: string;
16
10
  appearance?: CheckoutAppearance;
17
11
  className?: string;
18
12
  }
19
- export declare function PaymentMethodsPanel({ psps, cardSetupReturnURL, defaultCurrency, defaultCountry, appearance, className, }: PaymentMethodsPanelProps): React.JSX.Element;
13
+ /**
14
+ * The customer's saved cards. "Add card" offers each PSP of the merchant's
15
+ * payment setup (`GET /config`) that saves a card in the page.
16
+ */
17
+ export declare function PaymentMethodsPanel({ cardSetupReturnURL, collectionCurrency, defaultCountry, appearance, className, }: PaymentMethodsPanelProps): React.JSX.Element;
@@ -19,7 +19,9 @@ export declare function EmptyState({ title, description, action, }: {
19
19
  description?: string;
20
20
  action?: React.ReactNode;
21
21
  }): React.JSX.Element;
22
- export declare function ErrorState({ error, onRetry, }: {
22
+ export declare function ErrorState({ error, message, onRetry, }: {
23
23
  error: BillingError;
24
+ /** Replaces the error's own message. */
25
+ message?: string;
24
26
  onRetry: () => void;
25
27
  }): React.JSX.Element;
@@ -7,10 +7,15 @@ export interface CheckoutPageProps {
7
7
  baseUrl?: string;
8
8
  /** Replaces the client built from `baseUrl`. */
9
9
  client?: BillingClient;
10
+ /**
11
+ * Pay as the signed-in customer on this customer surface (its `/me`
12
+ * prefix) instead of with the session id alone; see `checkoutSource`.
13
+ */
14
+ customerBase?: string;
10
15
  /** This host's branding; the framing app chooses the theme. */
11
16
  appearance?: CheckoutAppearance;
12
17
  layout?: CheckoutLayout;
13
18
  defaultCountry?: string;
14
19
  className?: string;
15
20
  }
16
- export declare function CheckoutPage({ baseUrl, client, appearance, layout, defaultCountry, className, }: CheckoutPageProps): React.JSX.Element;
21
+ export declare function CheckoutPage({ baseUrl, client, customerBase, appearance, layout, defaultCountry, className, }: CheckoutPageProps): React.JSX.Element;