@openrails/billing-ui 0.234.0 → 0.236.0

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 (63) hide show
  1. package/CHANGELOG.md +142 -2
  2. package/README.md +119 -85
  3. package/dist/account/account-billing.d.ts +6 -5
  4. package/dist/account/change-card-dialog.d.ts +5 -2
  5. package/dist/account/payment-methods-panel.d.ts +6 -5
  6. package/dist/buy-button.d.ts +35 -0
  7. package/dist/cart.d.ts +18 -0
  8. package/dist/checkout.d.ts +1 -3
  9. package/dist/client/checkout.d.ts +28 -0
  10. package/dist/client/client.d.ts +98 -63
  11. package/dist/client/errors.d.ts +5 -4
  12. package/dist/client/generated/error-codes.d.ts +1 -1
  13. package/dist/client/generated/routes.d.ts +142 -99
  14. package/dist/client/generated/wire.d.ts +185 -98
  15. package/dist/client/index.d.ts +2 -2
  16. package/dist/client/types.d.ts +272 -29
  17. package/dist/{client-CWX6QpLa.js → client-BuvolBPT.js} +498 -326
  18. package/dist/client-BuvolBPT.js.map +1 -0
  19. package/dist/client.js +2 -2
  20. package/dist/components/stripe-card.d.ts +8 -2
  21. package/dist/components/stripe-wallets.d.ts +12 -0
  22. package/dist/components/ui/badge.d.ts +1 -1
  23. package/dist/components/ui/button.d.ts +1 -1
  24. package/dist/frame.d.ts +5 -5
  25. package/dist/hooks-2d-XjpuI.js +526 -0
  26. package/dist/hooks-2d-XjpuI.js.map +1 -0
  27. package/dist/i18n/context.d.ts +1 -1
  28. package/dist/i18n/messages.d.ts +1 -1
  29. package/dist/index.d.ts +7 -5
  30. package/dist/index.js +1985 -1667
  31. package/dist/index.js.map +1 -1
  32. package/dist/lib/money.d.ts +1 -0
  33. package/dist/lib/offer-label.d.ts +4 -0
  34. package/dist/locales/de.js +25 -4
  35. package/dist/locales/de.js.map +1 -1
  36. package/dist/locales/en.d.ts +25 -4
  37. package/dist/locales/en.js +25 -4
  38. package/dist/locales/en.js.map +1 -1
  39. package/dist/locales/es.js +25 -4
  40. package/dist/locales/es.js.map +1 -1
  41. package/dist/locales/ja.js +16 -4
  42. package/dist/locales/ja.js.map +1 -1
  43. package/dist/locales/ko.js +16 -4
  44. package/dist/locales/ko.js.map +1 -1
  45. package/dist/locales/zh.js +16 -4
  46. package/dist/locales/zh.js.map +1 -1
  47. package/dist/offers.d.ts +33 -0
  48. package/dist/order-checkout.d.ts +30 -0
  49. package/dist/react/context.d.ts +6 -5
  50. package/dist/react/hooks.d.ts +17 -16
  51. package/dist/react/index.d.ts +1 -1
  52. package/dist/react/provider.d.ts +24 -2
  53. package/dist/react.js +2 -33
  54. package/dist/save-payment-method.d.ts +0 -6
  55. package/dist/styles.css +1 -1
  56. package/dist/types.d.ts +16 -16
  57. package/docs/payment-form-contract.md +6 -6
  58. package/package.json +2 -2
  59. package/dist/client-CWX6QpLa.js.map +0 -1
  60. package/dist/config-DZ97SQG2.js +0 -341
  61. package/dist/config-DZ97SQG2.js.map +0 -1
  62. package/dist/provider.d.ts +0 -18
  63. package/dist/react.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,9 +1,134 @@
1
1
  # Changelog
2
2
 
3
+ ## Apple Pay and Google Pay
4
+
5
+ - `<OrderCheckout wallets>` offers Apple Pay and Google Pay through a Stripe
6
+ PSP's Express Checkout Element, above the saved and new cards. Off by
7
+ default: the page's host must be a payment-method domain registered on that
8
+ Stripe account.
9
+
10
+ ## Renewals are orders
11
+
12
+ - `Order` gains `renewal` (a subscription renewal's period and `due_at`) and
13
+ `voided_at`; `expires_at` is null on a renewal; `status` may be `void`.
14
+ - `PaymentRecovery` gains `order_id`: an engine-owned subscription's unpaid
15
+ renewal, paid through `<OrderCheckout>`.
16
+ - `<OrderCheckout>` offers new card entry only where the order takes one (a
17
+ renewal takes saved cards), and lets the customer authenticate a payment
18
+ their bank challenged while they were away.
19
+
20
+ ## Hosted checkout
21
+
22
+ - `getConfig()` returns `merchant` (`PublicMerchant`: `display_name`, and
23
+ `logo_url` and `support_url` when they are https), null when the request
24
+ resolves no merchant.
25
+ - `Order` carries `checkout` (its hosted checkout: `expires_at`,
26
+ `success_url`, `cancel_url`, `saved_payment_methods`; `url` only on the
27
+ answer that made it), null without one.
28
+
29
+ ## Orders take Solana and hosted pages
30
+
31
+ - `<OrderCheckout>` offers an order's Solana option: one button per token,
32
+ then the Solana Pay QR of the order's `solana_pay` next action, confirmed
33
+ until the transfer lands.
34
+ - A PSP with no fields in the page (Stripe without a publishable key) takes
35
+ the order on its hosted page: `<OrderCheckout>` sends the customer there and
36
+ back to `returnUrl` (default: the current page).
37
+ - `OrderPaymentInput` gains `{ token_symbol, psp_id? }` and
38
+ `{ return_url, psp_id? }`.
39
+
40
+ ## OrderCheckout pays an order
41
+
42
+ - `<OrderCheckout orderId>` shows an order and pays it with a saved card or a
43
+ new card in the PSP's fields, running any `next_action` and `confirm`. It
44
+ takes an optional `client` and offers only cards kept for one-click buys
45
+ (`PaymentMethod.reusable`).
46
+ - The client gains `getOrder`, and `payOrder` and `confirmOrder`, which answer
47
+ `{ order, error? }`: a declined payment (402) is the order with
48
+ `payment.last_payment_error`, not a throw. `BillingError` carries `order`.
49
+ - `StripeCardEntry`'s handle gains `tokenize()` and `handleNextAction()`.
50
+
51
+ ## A card is saved in one call
52
+
53
+ - `addPaymentMethod` takes `token` (was `payment_token`). A Stripe card is a
54
+ `pm_` from Stripe's fields, saved by the same call; a card the bank wants to
55
+ authenticate answers `status: "requires_action"` with `next_action`, and
56
+ `confirmPaymentMethod(id)` finishes it after Stripe.js `handleNextAction`.
57
+ `StripeCardEntry` and `SavePaymentMethod` do this themselves.
58
+ - `createCardSetup`, `getCardSetup`, `confirmCardSetup`, the `CardSetup` type
59
+ and the `returnURL` / `cardSetupReturnURL` props are gone: 3-D Secure runs in
60
+ the page.
61
+
62
+ ## Offers sell products by key
63
+
64
+ - The public catalog is a lookup by product key: `listProducts({ keys })` and
65
+ `useProducts({ keys })` take the keys alone (1 to 100) and read one page;
66
+ `entitlements`, `limit` and `cursor` are gone, and so is
67
+ `ProductsState.nextCursor`. No keys fetch nothing.
68
+ - `<Offers keys>` replaces `<Offers entitlements>`: your server names the
69
+ products, with the Go `Client.ListOffers` when it finds them by
70
+ entitlement.
71
+
72
+ ## Apps buy through BuyButton and Offers
73
+
74
+ - `Checkout` and `CheckoutModal` (and their props), the fixture sources and
75
+ the `CheckoutSource` type are no longer exported; the client no longer has
76
+ `createCheckoutSession` or `checkoutSource`. Buy with `<BuyButton>` (it takes
77
+ `amount`, `autoRenew` and `successUrl`) or `<Offers>`. `CheckoutPage` and
78
+ `CheckoutFrame` stay, for the shared payment page.
79
+ - The main entry also exports `createBillingClient` (with `BillingClient`,
80
+ `BillingClientOptions` and `ProductListOptions`) beside `formatAmount`, so a
81
+ React app imports from `@openrails/billing-ui` alone; `./client` and
82
+ `./react` are unchanged.
83
+
84
+ ## One provider
85
+
86
+ - `BillingProvider` takes what `BillingUiProvider` took (`appearance`,
87
+ `messages`, `t`, `locale`, `navigate`) beside `client` and `onChange`, and
88
+ `BillingUiProvider` is gone. The main entry exports it; `./react` exports the
89
+ same component for hook-only apps, without the stylesheet. `client` is
90
+ optional: leave it out where no customer signs in, such as `CheckoutPage`.
91
+ - Inside a provider with a client, the checkout saves a new card to the
92
+ customer first and pays on the customer surface.
93
+
94
+ ## Offers sell an entitlement
95
+
96
+ - `<BuyButton product price onPaid onSignInRequired>` buys one price, labelled
97
+ from the catalog unless given `label`. The app never touches a checkout
98
+ session: when orders take a new card (#1168) it creates an order instead.
99
+ - `<Offers entitlements onPaid onSignInRequired>` lists every product on sale
100
+ granting any of the entitlements, a `BuyButton` per price; `products` feeds
101
+ it from the host's server instead.
102
+ - `listProducts({ entitlements, keys })` and `useProducts({ entitlements, keys })`
103
+ filter the public catalog. One of them is required: OpenRails no longer
104
+ lists the whole catalog publicly.
105
+
106
+ ## Checkout saves a new card under its PSP
107
+
108
+ - Inside a `BillingProvider`, `Checkout` saved a new card with the session
109
+ option's id as `psp_id`, which `POST /me/payment-methods` refuses
110
+ (`invalid_param`); it now sends the option's `psp_id`.
111
+
112
+ ## A default card per currency
113
+
114
+ - `setDefaultPaymentMethod({currency, paymentMethodId})` (`PUT
115
+ /me/default-payment-methods/{currency}`) replaces
116
+ `setCollectionPaymentMethod`: the default card pays the currency's invoices
117
+ and every subscription in it without its own card. `usePaymentMethods()`
118
+ has `setDefault` for `setCollection`, `PaymentMethod.default_currencies`
119
+ replaces `collection_currencies`, and the `payment_method.default_changed`
120
+ change replaces `payment_method.collection_changed`.
121
+ - `AccountBilling` and `PaymentMethodsPanel` take `defaultCurrency` (was
122
+ `collectionCurrency`) and offer "Make default".
123
+ - `setSubscriptionPaymentMethod(id, null)` and `setPaymentMethod(id, null)`
124
+ make a subscription follow the default card; `Subscription.payment_method_id`
125
+ is its own card, null when it follows. The change-card dialog offers "Your
126
+ default card" and opens on any card subscription (`collection_policy`).
127
+
3
128
  ## The customer's summary
4
129
 
5
- - `getAccount()` reads `GET /me`: the balance per currency, the card that
6
- pays each currency, and the unread notice count.
130
+ - `getAccount()` reads `GET /me`: the balance per currency, the default card
131
+ of each currency, and the unread notice count.
7
132
  - `Invoice.delinquent`: overdue past grace; new usage in its currency is
8
133
  refused until it is paid.
9
134
 
@@ -14,6 +139,21 @@
14
139
  final retry, and whether it waits for a new card. Null unless the
15
140
  subscription is `past_due` or `awaiting_method`.
16
141
 
142
+ ## Subscription changes and seats
143
+
144
+ - `changeTier` and `previewTierChange` are `changeSubscription(id,
145
+ { priceId?, quantity?, idempotencyKey, signature? })` and
146
+ `previewSubscriptionChange(id, { priceId?, quantity? })`, on
147
+ `POST /me/subscriptions/{id}/change[/preview]`. `TierChange` and
148
+ `TierChangePreview` are `SubscriptionChange` and `SubscriptionChangePreview`:
149
+ no `action`, a `quantity`. `useSubscriptions().changeTier` is
150
+ `changeSubscription`, its pending state `change`, and its host event
151
+ `subscription.changed`.
152
+ - `Subscription` has `quantity`: the seats of a per-seat price, null
153
+ otherwise; `scheduled_change.quantity` is the seats the next renewal bills. The `tier_change_*` codes are
154
+ `subscription_change_*`, and `quantity_not_allowed` refuses seats on a price
155
+ without them.
156
+
17
157
  ## The public catalog is under /catalog
18
158
 
19
159
  - `listProducts()` reads `GET /catalog/products` (was `GET /products`).
package/README.md CHANGED
@@ -6,13 +6,13 @@ billing (subscriptions, saved cards, payment history).
6
6
  | Import | Contents |
7
7
  | --------------------------------- | ---------------------------------------------------------------------------------- |
8
8
  | `@openrails/billing-ui/client` | Framework-free typed client for `/billing/v1/me/*` and the catalog, `BillingError` |
9
- | `@openrails/billing-ui/react` | `BillingProvider` and headless hooks |
10
- | `@openrails/billing-ui` | Styled checkout and account components, `BillingUiProvider`, i18n |
9
+ | `@openrails/billing-ui/react` | `BillingProvider` and headless hooks, without styles |
10
+ | `@openrails/billing-ui` | `BillingProvider`, styled checkout and account components, i18n |
11
11
  | `@openrails/billing-ui/locales/*` | `en de es ja ko zh` message bundles |
12
12
 
13
- The checkout owns the browser payment flow. OpenRails holds the checkout
14
- session: the signed-in customer mints one for a price, and its `ocs_` id alone
15
- reads and pays it. Card data is tokenized in NMI-hosted Collect.js iframes and
13
+ The checkout owns the browser payment flow, behind `<BuyButton>` and
14
+ `<Offers>`. OpenRails holds the checkout session: the signed-in customer mints
15
+ one for a price, and its `ocs_` id alone reads and pays it. Card data is tokenized in NMI-hosted Collect.js iframes and
16
16
  never enters the host application, unless the PSP takes cards on OpenRails
17
17
  itself (`card_entry: server`, driver `card`).
18
18
 
@@ -21,41 +21,65 @@ session document, billing fields, browser-autofill behavior, and the checkout
21
21
  request schema.
22
22
 
23
23
  The package lives in the OpenRails repository and shares its version: each
24
- OpenRails release `vX.Y.Z` attaches `openrails-billing-ui-X.Y.Z.tgz`.
24
+ OpenRails release `vX.Y.Z` publishes `@openrails/billing-ui@X.Y.Z` to npm, with
25
+ provenance.
25
26
 
26
27
  ```sh
27
- pnpm add https://github.com/open-rails/openrails/releases/download/vX.Y.Z/openrails-billing-ui-X.Y.Z.tgz
28
+ pnpm add @openrails/billing-ui@X.Y.Z
28
29
  ```
29
30
 
30
- ## Checkout
31
+ ## Buying
32
+
33
+ A React app imports from one place:
31
34
 
32
35
  ```tsx
33
- import { createBillingClient } from "@openrails/billing-ui/client"
34
- import { Checkout, CheckoutFrame } from "@openrails/billing-ui"
35
- import "@openrails/billing-ui/styles.css"
36
-
37
- const billing = createBillingClient({ fetch: auth.authFetch })
38
- const session = await billing.createCheckoutSession({
39
- productKey: "pro",
40
- priceKey: "monthly",
41
- })
36
+ import { BillingProvider, BuyButton, Offers, createBillingClient, formatAmount } from "@openrails/billing-ui"
42
37
 
43
- // A shared payment page (Config.Checkout.PageURL) answers with a url:
44
- <CheckoutFrame url={session.url} theme="dark" onComplete={() => refetchAccess()} />
45
- // Without one, the app renders the checkout itself:
46
- <Checkout source={billing.checkoutSource(session.id)} onComplete={() => refetchAccess()} />
38
+ // auth-ui's authFetch attaches the bearer and retries once after a refresh;
39
+ // `getToken: () => token` works for any other auth.
40
+ const billing = createBillingClient({ baseUrl: "/billing/v1", fetch: auth.authFetch })
41
+
42
+ <BillingProvider client={billing} appearance={{ theme: "auto" }}>
43
+ <App />
44
+ </BillingProvider>
47
45
  ```
48
46
 
49
- `onComplete` is a hint: confirm access from your own authenticated API before
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`.
47
+ `<BuyButton>` buys one price, `<Offers>` the products your server names by
48
+ key (a course, its bundle and the membership that also unlocks it; the Go
49
+ `Client.ListOffers` finds the products granting an entitlement): a
50
+ `BuyButton` per price ("$4.99", "Rent for 3 days, $1.99",
51
+ "$10.00 every 30 days"). The purchase is theirs from start to payment, so the
52
+ app never handles a checkout session: today a button opens one in a checkout
53
+ dialog; when orders take a new card it will create an order instead,
54
+ and apps won't change. Signing in is the host's: with `signedIn={false}`, or
55
+ when OpenRails answers 401, they call `onSignInRequired` instead. `onPaid`
56
+ follows a successful payment only; it is a hint, so confirm access from your
57
+ own API (your gate) before granting anything.
54
58
 
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
+ ```tsx
60
+ <BuyButton
61
+ product="course-101"
62
+ price="rent" // labelled from the catalog unless you pass label
63
+ signedIn={signedIn}
64
+ onSignInRequired={() => openSignIn()}
65
+ onPaid={() => navigate("/courses/css-101")} // your gate now admits them
66
+ />
67
+
68
+ <Offers
69
+ keys={["course-101", "course-bundle", "channel-membership"]} // from your server, or products={...} from its Client.ListOffers
70
+ signedIn={signedIn}
71
+ onSignInRequired={() => openSignIn()}
72
+ onPaid={() => navigate("/courses/css-101")}
73
+ />
74
+ ```
75
+
76
+ `successUrl` returns the buyer from a redirect step (Stripe's hosted page); it
77
+ must be on one of your app's return origins. `autoRenew={false}` buys a
78
+ recurring price's first term only.
79
+
80
+ For prepaid credits, a fixed pack is another product/price. A
81
+ `customer_amount` price takes the buyer's `amount`: collect it on your product
82
+ page, and the checkout charges exactly that:
59
83
 
60
84
  ```tsx
61
85
  import { decimalToAmount } from "@openrails/billing-ui/client"
@@ -64,42 +88,46 @@ import { decimalToAmount } from "@openrails/billing-ui/client"
64
88
  // `enteredAmount` is the text from an <input inputMode="decimal">, e.g. "100".
65
89
  const amount = decimalToAmount(enteredAmount, currency.decimals)
66
90
  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
- })
91
+ const valid = amount && bounds && BigInt(amount) >= BigInt(bounds.min_amount) && BigInt(amount) <= BigInt(bounds.max_amount)
92
+
93
+ <BuyButton product="api-credits" price="deposit" amount={valid ? amount : undefined} label="Add credits" {...handlers} />
79
94
  ```
80
95
 
81
96
  OpenRails validates the bounds and whole currency minor units before accepting
82
97
  payment. Fixed prices reject `amount`; their pack benefit can be greater than
83
98
  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.
99
+ use NMI or Stripe.
85
100
 
86
101
  A rail's own step is the pay result's `next_action`: `redirect_to_url` (an
87
102
  https page, opened in the top window) or `solana_pay` (a `solana:` link shown
88
103
  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.
104
+
105
+ ### A shared payment page
106
+
107
+ Several sites selling for one merchant can share one payment page
108
+ (`Config.Checkout.PageURL`). A server mints the session with the Go
109
+ `Client.CreateCheckoutSession` and hands the browser its `url`, which the site
110
+ frames:
111
+
112
+ ```tsx
113
+ <CheckoutFrame url={session.url} theme="dark" onComplete={() => refetchAccess()} />
114
+ ```
115
+
116
+ Stripe Elements needs the buyer's own client, so the shared page offers token
117
+ rails only.
91
118
 
92
119
  The payment host serves `<CheckoutPage>` from one HTML entry at `PageURL`,
93
120
  behind its adapter's `CheckoutFramePolicy` (only the sites in
94
- `Config.Checkout.EmbedOrigins` may frame it):
121
+ `Config.Checkout.EmbedOrigins` may frame it). No customer signs in there, so
122
+ its `BillingProvider` has no client:
95
123
 
96
124
  ```tsx
97
- import { BillingUiProvider, CheckoutPage } from "@openrails/billing-ui"
125
+ import { BillingProvider, CheckoutPage } from "@openrails/billing-ui"
98
126
 
99
127
  createRoot(root).render(
100
- <BillingUiProvider>
128
+ <BillingProvider>
101
129
  <CheckoutPage appearance={{ variables: brand }} />
102
- </BillingUiProvider>
130
+ </BillingProvider>
103
131
  )
104
132
  ```
105
133
 
@@ -111,25 +139,30 @@ cannot navigate the top window, so the app does); app to page
111
139
 
112
140
  ## Account billing
113
141
 
142
+ One `BillingProvider`, usually at the app's root, gives every component the
143
+ client, the appearance and the words. `@openrails/billing-ui/react` exports the
144
+ same provider for an app that uses only the hooks; it loads no stylesheet.
145
+
114
146
  ```tsx
115
- import { createBillingClient } from "@openrails/billing-ui/client"
116
- import { BillingProvider } from "@openrails/billing-ui/react"
117
- import { AccountBilling, BillingUiProvider } from "@openrails/billing-ui"
147
+ import { AccountBilling, BillingProvider, createBillingClient } from "@openrails/billing-ui"
118
148
  import { de } from "@openrails/billing-ui/locales/de"
119
149
 
120
- // auth-ui's authFetch attaches the bearer and retries once after a refresh;
121
- // `getToken: () => token` works for any other auth.
122
150
  const billing = createBillingClient({ baseUrl: "/billing/v1", fetch: auth.authFetch })
123
151
 
124
- <BillingUiProvider appearance={{ theme: "auto" }} messages={de} locale="de" navigate={navigate}>
125
- <BillingProvider client={billing} onChange={() => queryClient.invalidateQueries({ queryKey: ["billing"] })}>
126
- <AccountBilling
127
- plansHref="/plans"
128
- collectionCurrency="USD" // offers "Use for invoices"
129
- sendSolanaTransaction={(tx) => wallet.signAndSend(tx)} // signs a Solana cancel
130
- />
131
- </BillingProvider>
132
- </BillingUiProvider>
152
+ <BillingProvider
153
+ client={billing}
154
+ appearance={{ theme: "auto" }}
155
+ messages={de}
156
+ locale="de"
157
+ navigate={navigate}
158
+ onChange={() => queryClient.invalidateQueries({ queryKey: ["billing"] })}
159
+ >
160
+ <AccountBilling
161
+ plansHref="/plans"
162
+ defaultCurrency="USD" // offers "Make default"
163
+ sendSolanaTransaction={(tx) => wallet.signAndSend(tx)} // signs a Solana cancel
164
+ />
165
+ </BillingProvider>
133
166
  ```
134
167
 
135
168
  The panels read OpenRails' public configuration (`GET /config`) themselves,
@@ -142,36 +175,38 @@ for host components.
142
175
  ## Catalog and plan changes
143
176
 
144
177
  The client also reads the public catalog and changes a subscription's plan.
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.
178
+ The catalog and Solana reads are public routes, served only when the host
179
+ mounts OpenRails' checkout routes; `GET /config` is always served.
148
180
 
149
181
  | Call | Route |
150
182
  | --------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
151
- | `listProducts()` (each product embeds its prices on sale) | `GET /catalog/products` |
183
+ | `listProducts({ keys })`: the products on sale named by `keys` (1 to 100 product keys), each with its prices, in one page; `useProducts` reads the same | `GET /catalog/products` |
152
184
  | `getConfig()` (capabilities, `currencies`, `payment` with PSPs, `solana.network` and tokens; `client.currencies` is the pinned registry) | `GET /config` |
153
- | `previewTierChange(id, priceId)` | `POST /me/subscriptions/{id}/change-tier/preview` |
154
- | `changeTier(id, { priceId, idempotencyKey, signature? })` | `POST /me/subscriptions/{id}/change-tier` |
185
+ | `previewSubscriptionChange(id, { priceId?, quantity? })` | `POST /me/subscriptions/{id}/change/preview` |
186
+ | `changeSubscription(id, { priceId?, quantity?, idempotencyKey, signature? })` | `POST /me/subscriptions/{id}/change` |
155
187
  | `listSolanaTokens({ priceId, wallet })` | `GET /solana/tokens` |
156
188
 
157
189
  ```ts
158
- const preview = await billing.previewTierChange(sub.id, price.id) // amount_due_now, effective
159
- const change = await billing.changeTier(sub.id, {
160
- priceId: price.id,
190
+ const preview = await billing.previewSubscriptionChange(sub.id, { quantity: 5 }) // amount_due_now, effective
191
+ const change = await billing.changeSubscription(sub.id, {
192
+ quantity: 5,
161
193
  idempotencyKey,
162
194
  })
163
195
  ```
164
196
 
165
- - `changeTier` is a money write. Mint one `idempotencyKey` per attempt and
197
+ - A change moves to another price of the tier group (`priceId`), to other
198
+ seats of a per-seat price (`quantity`), or both. More seats and an upgrade
199
+ charge now; fewer seats and a downgrade apply at the next renewal.
200
+ - `changeSubscription` is a money write. Mint one `idempotencyKey` per attempt and
166
201
  reuse it until the change resolves (`status: "processing"`, a
167
- `tier_change_in_flight` refusal naming `metadata.operation_id`, a 5xx or a
202
+ `subscription_change_in_flight` refusal naming `metadata.operation_id`, a 5xx or a
168
203
  lost response), so OpenRails replays the stored result instead of charging
169
204
  twice. `requires_action` carries `operation_id` for `authenticatePayment`;
170
205
  replay the same key afterwards.
171
206
  - A rail that needs the customer's own step answers `requires_action` with a
172
207
  `next_action`. Solana's is `solana_sign_transactions`:
173
208
  `signWalletAction(change.next_action, send)` signs and sends its
174
- transactions and returns the signature; repeat `changeTier` with the same
209
+ transactions and returns the signature; repeat `changeSubscription` with the same
175
210
  key and `signature`. `cancelSubscription(id, { reason, signature? })` works
176
211
  the same way: its answer is the subscription, unchanged and carrying
177
212
  `next_action` until the signed cancel lands. The hooks run this when given
@@ -191,8 +226,9 @@ provider:
191
226
  Card rails (`collect_js`, `card`, `stripe_elements`) render one panel: saved cards,
192
227
  an inline new card and one Pay/Subscribe button, which is the customer's
193
228
  confirmation of the displayed terms. No provider chooser with one rail.
194
- - Inside a `BillingProvider`, a new card is always saved to the account first
195
- and the source is paid with `payment_method_id`; `requires_action` results
229
+ - Inside a `BillingProvider` with a client, a new card is always saved to the
230
+ account first and the session is paid with `payment_method_id` on the
231
+ customer surface; `requires_action` results
196
232
  carrying an `operation` run 3-D Secure in the page. A `failed` result stays
197
233
  on the panel with `failure.message` next to its `field`, so the buyer can
198
234
  pick another card; the host gives each new attempt a new idempotency key.
@@ -204,10 +240,8 @@ provider:
204
240
  `returnURL(setupId)`; confirm with `client.confirmCardSetup(id)`.
205
241
  - `authenticatePayment(client, operationId, psp)` completes a pending
206
242
  payment's 3-D Secure challenge when `canAuthenticatePayment(psp)`.
207
- - `defaultCountry` (Checkout, SavePaymentMethod, AccountBilling) preselects
208
- the billing country, else the browser locale's region.
209
- - `CheckoutModal gate={<SignIn />}` renders host content (e.g. sign-in) in
210
- the modal instead of the checkout until cleared.
243
+ - `defaultCountry` (CheckoutPage, SavePaymentMethod, AccountBilling)
244
+ preselects the billing country, else the browser locale's region.
211
245
 
212
246
  - Panels: `SubscriptionsPanel` (cancel, resume, change card),
213
247
  `PaymentMethodsPanel`, `PaymentHistory`, `BillingStatusBadge`,
@@ -215,11 +249,11 @@ provider:
215
249
  - `renderSubscriptionFooter={(s) => ...}` adds host content under a
216
250
  subscription row; `useBillingRefresh()` refetches after host-side changes.
217
251
  - Hooks: `useSubscriptions` (`cancel`, `resume`, `setPaymentMethod`,
218
- `changeTier`, per-row `pending`, `nextCursor`), `usePaymentMethods` (`add`,
219
- `remove`, `setCollection`), `usePayments` (cursor pages), `useProducts` (the
220
- catalog).
252
+ `changeSubscription`, per-row `pending`, `nextCursor`), `usePaymentMethods` (`add`,
253
+ `remove`, `setDefault`), `usePayments` (cursor pages), `useProducts` (a
254
+ catalog lookup by product key).
221
255
  Actions resolve to `null` or a `BillingError`; they never throw.
222
- `changeTier` resolves to the `TierChange` instead of `null`. Cancel, resume
256
+ `changeSubscription` resolves to the `SubscriptionChange` instead of `null`. Cancel, resume
223
257
  and the card change answer the subscription, which replaces the row.
224
258
  - Money is exact: `/me` amounts are int64 native-unit strings, scaled by the
225
259
  OpenRails currency registry (`currencies` option to extend it).
@@ -1,11 +1,12 @@
1
1
  import { CheckoutAppearance } from '../appearance';
2
2
  import { SendSolanaTransaction } from '../client/client';
3
- import { PaymentMethodsPanelProps } from './payment-methods-panel';
4
3
  import { SubscriptionsPanelProps } from './subscriptions-panel';
5
4
  export interface AccountBillingProps {
6
- cardSetupReturnURL?: PaymentMethodsPanelProps["cardSetupReturnURL"];
7
- /** Offers to make a card the one that collects this currency's invoices. */
8
- collectionCurrency?: string;
5
+ /**
6
+ * Offers to make a card the default for this currency: it pays the
7
+ * currency's invoices and every subscription in it without its own card.
8
+ */
9
+ defaultCurrency?: string;
9
10
  /** Billing country to preselect when adding a card. */
10
11
  defaultCountry?: string;
11
12
  sendSolanaTransaction?: SendSolanaTransaction;
@@ -16,4 +17,4 @@ export interface AccountBillingProps {
16
17
  className?: string;
17
18
  }
18
19
  /** Subscriptions, payment methods and history in one column. */
19
- export declare function AccountBilling({ cardSetupReturnURL, collectionCurrency, defaultCountry, sendSolanaTransaction, plansHref, renderSubscriptionFooter, historyPageSize, appearance, className, }: AccountBillingProps): import("react").JSX.Element;
20
+ export declare function AccountBilling({ defaultCurrency, defaultCountry, sendSolanaTransaction, plansHref, renderSubscriptionFooter, historyPageSize, appearance, className, }: AccountBillingProps): import("react").JSX.Element;
@@ -7,8 +7,11 @@ export interface ChangeCardDialogProps {
7
7
  name: string;
8
8
  onOpenChange: (open: boolean) => void;
9
9
  pending: boolean;
10
- /** Resolves null on success; the dialog then closes. */
11
- onConfirm: (paymentMethodId: string) => Promise<BillingError | null>;
10
+ /**
11
+ * The subscription's own card, or null to follow the default card. Resolves
12
+ * null on success; the dialog then closes.
13
+ */
14
+ onConfirm: (paymentMethodId: string | null) => Promise<BillingError | null>;
12
15
  appearance?: CheckoutAppearance;
13
16
  }
14
17
  export declare function ChangeCardDialog({ subscription, name, onOpenChange, pending, onConfirm, appearance, }: ChangeCardDialogProps): React.JSX.Element;
@@ -1,10 +1,11 @@
1
1
  import { CheckoutAppearance } from '../appearance';
2
2
  import * as React from "react";
3
3
  export interface PaymentMethodsPanelProps {
4
- /** Return target after off-page card verification; see `SavePaymentMethod`. */
5
- cardSetupReturnURL?: (setupId: string) => string;
6
- /** Offers to make a card the one that collects this currency's invoices. */
7
- collectionCurrency?: string;
4
+ /**
5
+ * Offers to make a card the default for this currency: it pays the
6
+ * currency's invoices and every subscription in it without its own card.
7
+ */
8
+ defaultCurrency?: string;
8
9
  /** Billing country to preselect when adding a card. */
9
10
  defaultCountry?: string;
10
11
  appearance?: CheckoutAppearance;
@@ -14,4 +15,4 @@ export interface PaymentMethodsPanelProps {
14
15
  * The customer's saved cards. "Add card" offers each PSP of the merchant's
15
16
  * payment setup (`GET /config`) that saves a card in the page.
16
17
  */
17
- export declare function PaymentMethodsPanel({ cardSetupReturnURL, collectionCurrency, defaultCountry, appearance, className, }: PaymentMethodsPanelProps): React.JSX.Element;
18
+ export declare function PaymentMethodsPanel({ defaultCurrency, defaultCountry, appearance, className, }: PaymentMethodsPanelProps): React.JSX.Element;
@@ -0,0 +1,35 @@
1
+ import { CheckoutAppearance } from './appearance';
2
+ import { PayResult } from './types';
3
+ import * as React from "react";
4
+ export interface BuyButtonProps {
5
+ /** The product's catalog key. */
6
+ product: string;
7
+ /** The price's key within the product. */
8
+ price: string;
9
+ /** The button's text; by default the price's amount and terms, from the catalog. */
10
+ label?: string;
11
+ /**
12
+ * A customer-chosen deposit, in the price currency's native units, for a
13
+ * price with `customer_amount`; the checkout charges exactly this.
14
+ */
15
+ amount?: string;
16
+ /** false buys a recurring price's first term only; default: renews. */
17
+ autoRenew?: boolean;
18
+ /** Where a redirect rail (Stripe's hosted page) returns the buyer: one of the app's return origins. */
19
+ successUrl?: string;
20
+ /**
21
+ * The purchase succeeded. A hint: the host's own gate decides access, so
22
+ * send the buyer to the content it guards.
23
+ */
24
+ onPaid: (result: PayResult) => void;
25
+ /**
26
+ * Buying needs a signed-in customer: called instead of the checkout when
27
+ * `signedIn` is false or OpenRails answers 401.
28
+ */
29
+ onSignInRequired: () => void;
30
+ /** false when the host knows the visitor is signed out. */
31
+ signedIn?: boolean;
32
+ appearance?: CheckoutAppearance;
33
+ className?: string;
34
+ }
35
+ export declare function BuyButton(props: BuyButtonProps): React.JSX.Element;
package/dist/cart.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ import { CheckoutAppearance } from './appearance';
2
+ import { BillingClient } from './client/client';
3
+ import { Order } from './client/types';
4
+ import * as React from "react";
5
+ export interface CartProps {
6
+ /** The client to read and buy through; default: the provider's. */
7
+ client?: BillingClient;
8
+ /**
9
+ * The order made from the cart. Default: pay it here with OrderCheckout.
10
+ */
11
+ onOrder?: (order: Order) => void;
12
+ /** Passed to OrderCheckout: the order is complete, or processing. */
13
+ onSettled?: (order: Order) => void;
14
+ appearance?: CheckoutAppearance;
15
+ className?: string;
16
+ }
17
+ /** The customer's cart. Requires `BillingProvider`. */
18
+ export declare function Cart({ client: given, onOrder, onSettled, appearance, className, }: CartProps): React.JSX.Element;
@@ -9,8 +9,6 @@ export interface CheckoutProps {
9
9
  layout?: CheckoutLayout;
10
10
  /** Billing country to preselect; default: the browser locale's region. */
11
11
  defaultCountry?: string;
12
- /** Return target after an off-page card verification (Stripe setup). */
13
- cardSetupReturnURL?: (setupId: string) => string;
14
12
  onComplete?: (result: PayResult) => void;
15
13
  /**
16
14
  * Leaves for a redirect rail's page. Default: navigate the top window,
@@ -21,4 +19,4 @@ export interface CheckoutProps {
21
19
  completionMode?: "embedded" | "redirect";
22
20
  className?: string;
23
21
  }
24
- export declare function Checkout({ source, appearance, layout, defaultCountry, cardSetupReturnURL, onComplete, onRedirect, onPhaseChange, completionMode, className, }: CheckoutProps): React.JSX.Element;
22
+ export declare function Checkout({ source, appearance, layout, defaultCountry, onComplete, onRedirect, onPhaseChange, completionMode, className, }: CheckoutProps): React.JSX.Element;
@@ -0,0 +1,28 @@
1
+ import { CheckoutSource } from '../source';
2
+ import { CheckoutSessionLink } from './types';
3
+ export interface CheckoutSourceOptions {
4
+ /**
5
+ * A customer surface's base: its `/me` prefix, such as `/billing/v1/me` or
6
+ * a host's own `/api/v1/merchants/acme/billing/me`. The session is read and
7
+ * paid there with this client's credential, which proves the session's
8
+ * customer, so its saved cards pay. Default: the session id alone, at
9
+ * `{baseUrl}/checkout-sessions`.
10
+ */
11
+ customerBase?: string;
12
+ }
13
+ export interface CheckoutCalls {
14
+ createCheckoutSession(input: {
15
+ priceKey?: string;
16
+ productKey?: string;
17
+ priceId?: string;
18
+ /** Customer-selected deposit in the price currency's native units. */
19
+ amount?: string;
20
+ /** Defaults to true for recurring prices; false buys the initial term only. */
21
+ autoRenew?: boolean;
22
+ successUrl?: string;
23
+ }): Promise<CheckoutSessionLink>;
24
+ checkoutSource(sessionId: string, opts?: CheckoutSourceOptions): CheckoutSource;
25
+ }
26
+ export declare function registerCheckout(client: object, checkout: CheckoutCalls): void;
27
+ /** The checkout calls of a client from createBillingClient. */
28
+ export declare function checkoutOf(client: object): CheckoutCalls;