@openrails/billing-ui 0.0.0-stage → 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.
- package/CHANGELOG.md +351 -0
- package/LICENSE +21 -0
- package/README.md +260 -2
- package/dist/account/account-billing.d.ts +19 -0
- package/dist/account/cancel-dialog.d.ts +18 -0
- package/dist/account/change-card-dialog.d.ts +14 -0
- package/dist/account/format.d.ts +25 -0
- package/dist/account/notice.d.ts +3 -0
- package/dist/account/payment-history.d.ts +8 -0
- package/dist/account/payment-methods-panel.d.ts +17 -0
- package/dist/account/section.d.ts +27 -0
- package/dist/account/status-badge.d.ts +8 -0
- package/dist/account/subscriptions-panel.d.ts +19 -0
- package/dist/appearance.d.ts +23 -0
- package/dist/authenticate.d.ts +8 -0
- package/dist/checkout-frame.d.ts +17 -0
- package/dist/checkout-page.d.ts +21 -0
- package/dist/checkout.d.ts +24 -0
- package/dist/client/client.d.ts +181 -0
- package/dist/client/errors.d.ts +26 -0
- package/dist/client/generated/currencies.d.ts +1 -0
- package/dist/client/generated/error-codes.d.ts +7 -0
- package/dist/client/generated/routes.d.ts +775 -0
- package/dist/client/generated/wire.d.ts +682 -0
- package/dist/client/index.d.ts +4 -0
- package/dist/client/types.d.ts +800 -0
- package/dist/client-DXUPzA8C.js +889 -0
- package/dist/client-DXUPzA8C.js.map +1 -0
- package/dist/client.js +2 -0
- package/dist/components/billing-fields.d.ts +39 -0
- package/dist/components/card-brands.d.ts +6 -0
- package/dist/components/card-fields.d.ts +29 -0
- package/dist/components/ccbill-fields.d.ts +8 -0
- package/dist/components/methods.d.ts +13 -0
- package/dist/components/pay-button.d.ts +6 -0
- package/dist/components/qr.d.ts +6 -0
- package/dist/components/saved-methods.d.ts +9 -0
- package/dist/components/states.d.ts +10 -0
- package/dist/components/stripe-card.d.ts +17 -0
- package/dist/components/summary.d.ts +12 -0
- package/dist/components/ui/alert-dialog.d.ts +18 -0
- package/dist/components/ui/badge.d.ts +7 -0
- package/dist/components/ui/button.d.ts +8 -0
- package/dist/components/ui/card.d.ts +11 -0
- package/dist/components/ui/dialog.d.ts +17 -0
- package/dist/components/ui/input.d.ts +3 -0
- package/dist/components/ui/label.d.ts +3 -0
- package/dist/components/ui/radio-group.d.ts +5 -0
- package/dist/components/ui/separator.d.ts +3 -0
- package/dist/components/ui/skeleton.d.ts +2 -0
- package/dist/components/ui/spinner.d.ts +3 -0
- package/dist/components/ui/textarea.d.ts +3 -0
- package/dist/config-BQELtBh4.js +341 -0
- package/dist/config-BQELtBh4.js.map +1 -0
- package/dist/dist-CslWupfi.js +3311 -0
- package/dist/dist-CslWupfi.js.map +1 -0
- package/dist/fixtures.d.ts +9 -0
- package/dist/frame.d.ts +47 -0
- package/dist/i18n/context.d.ts +4 -0
- package/dist/i18n/index.d.ts +2 -0
- package/dist/i18n/messages.d.ts +58 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +4090 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/billing.d.ts +39 -0
- package/dist/lib/card-brands.d.ts +2 -0
- package/dist/lib/card-entry.d.ts +46 -0
- package/dist/lib/ccbill.d.ts +8 -0
- package/dist/lib/collect.d.ts +51 -0
- package/dist/lib/money.d.ts +10 -0
- package/dist/lib/period.d.ts +13 -0
- package/dist/lib/rail-meta.d.ts +15 -0
- package/dist/lib/redirect.d.ts +2 -0
- package/dist/lib/stripe-appearance.d.ts +2 -0
- package/dist/lib/stripe.d.ts +2 -0
- package/dist/locales/de.d.ts +2 -0
- package/dist/locales/de.js +207 -0
- package/dist/locales/de.js.map +1 -0
- package/dist/locales/en.d.ts +204 -0
- package/dist/locales/en.js +207 -0
- package/dist/locales/en.js.map +1 -0
- package/dist/locales/es.d.ts +2 -0
- package/dist/locales/es.js +207 -0
- package/dist/locales/es.js.map +1 -0
- package/dist/locales/ja.d.ts +2 -0
- package/dist/locales/ja.js +198 -0
- package/dist/locales/ja.js.map +1 -0
- package/dist/locales/ko.d.ts +2 -0
- package/dist/locales/ko.js +198 -0
- package/dist/locales/ko.js.map +1 -0
- package/dist/locales/zh.d.ts +2 -0
- package/dist/locales/zh.js +198 -0
- package/dist/locales/zh.js.map +1 -0
- package/dist/modal.d.ts +12 -0
- package/dist/provider.d.ts +18 -0
- package/dist/psp.d.ts +35 -0
- package/dist/react/config.d.ts +34 -0
- package/dist/react/context.d.ts +35 -0
- package/dist/react/hooks.d.ts +82 -0
- package/dist/react/index.d.ts +4 -0
- package/dist/react/provider.d.ts +10 -0
- package/dist/react/remote.d.ts +10 -0
- package/dist/react.js +33 -0
- package/dist/react.js.map +1 -0
- package/dist/save-payment-method.d.ts +26 -0
- package/dist/scope-context.d.ts +17 -0
- package/dist/scope.d.ts +6 -0
- package/dist/source.d.ts +5 -0
- package/dist/styles.css +3 -0
- package/dist/tokenized-card-form.d.ts +24 -0
- package/dist/types.d.ts +250 -0
- package/docs/payment-form-contract.md +136 -0
- package/package.json +112 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
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
|
|
99
|
+
|
|
100
|
+
- OpenRails serves the checkout session; hosts no longer write
|
|
101
|
+
`/api/v1/checkout/sessions` routes. `client.createCheckoutSession()` mints
|
|
102
|
+
one for the signed-in customer (`priceKey` or `priceId`, `successUrl`) and
|
|
103
|
+
`client.checkoutSource(id)` reads and pays it with the id alone (no bearer).
|
|
104
|
+
A refusal the buyer can act on resolves as a `failed`, `blocked` or
|
|
105
|
+
`expired` result. A `card` rail pays through the shared page too.
|
|
106
|
+
- `<CheckoutFrame url onComplete theme>` frames the shared payment page and
|
|
107
|
+
`<CheckoutPage>` is that page; they speak an origin-checked frame protocol
|
|
108
|
+
(`ready`, `resize`, `complete`, `redirect`; `init {theme}`).
|
|
109
|
+
- `<Checkout onRedirect>` replaces the top-window navigation of redirect rails.
|
|
110
|
+
- The session document gains `embed_origin`.
|
|
111
|
+
|
|
112
|
+
## Server card entry
|
|
113
|
+
|
|
114
|
+
- Driver `card` (a PSP declared `card_entry: server`, flow `card`): `<Checkout>`
|
|
115
|
+
and `SavePaymentMethod` render plain card inputs with the Collect.js form's
|
|
116
|
+
layout and field errors, load no gateway script, and send
|
|
117
|
+
`card: {number, exp_month, exp_year, cvc}`. Inside a `BillingProvider` the
|
|
118
|
+
card is saved first (`addPaymentMethod({ provider, card })`) and charged by
|
|
119
|
+
id; a page without one sends `card` in `PayRequest`. The inputs are cleared
|
|
120
|
+
once the card is sent.
|
|
121
|
+
- `cardSetupDriver` may return `"card"`; `NewCard` takes `payment_token` or
|
|
122
|
+
`card`, never both.
|
|
123
|
+
|
|
124
|
+
## Catalog and plan-change client
|
|
125
|
+
|
|
126
|
+
- `@openrails/billing-ui/client` gains the public catalog (`listProducts`,
|
|
127
|
+
`listPrices`, `listCurrencies`), plan change (`previewTierChange`,
|
|
128
|
+
`changeTier`) and Solana (`getSolanaConfig`, `listSolanaTokens`,
|
|
129
|
+
`prepareSolanaTierChange`, `confirmSolanaTierChange`) calls, with the types
|
|
130
|
+
`Product`, `Price`, `Currency`, `TierChangePreview`, `TierChange`,
|
|
131
|
+
`SolanaConfig`, `SolanaToken`, `SolanaTierChangeTx` and `SolanaTierChange`.
|
|
132
|
+
`changeTier` requires an `idempotencyKey`; reuse it until the change
|
|
133
|
+
resolves.
|
|
134
|
+
- React: `useProducts`, and `useSubscriptions().changeTier` (per-row `pending`
|
|
135
|
+
`"change_tier"`, `onChange` `subscription.tier_changed`), which resolves to
|
|
136
|
+
the `TierChange` or a `BillingError`.
|
|
137
|
+
|
|
138
|
+
## Explicit payment method, fail-fast errors
|
|
139
|
+
|
|
140
|
+
- The checkout pre-selects the customer's default card (`PaymentMethod.default`,
|
|
141
|
+
`SavedPaymentMethod.default`) and always sends the chosen card's id.
|
|
142
|
+
OpenRails refuses a charge that names no method with
|
|
143
|
+
`payment_method_required` (400).
|
|
144
|
+
- A server error (5xx) from `pay` is shown at once and the panel returns to
|
|
145
|
+
ready; hosts keep their idempotency key so paying again replays. A host
|
|
146
|
+
source signals it by throwing an error with a numeric `status`
|
|
147
|
+
(`CheckoutSourceError`, or any error carrying `status`).
|
|
148
|
+
- The client never retries a write. A GET retries once, only on a network
|
|
149
|
+
error or 502/503/504 (429/503 with `Retry-After`), within `RETRY_BUDGET_MS`
|
|
150
|
+
(2 s). New `isServerError`; a 5xx without an error envelope has code
|
|
151
|
+
`server_error`.
|
|
152
|
+
|
|
153
|
+
## One-click card subscriptions
|
|
154
|
+
|
|
155
|
+
- A checkout host relays a card subscription's pay request with OpenRails'
|
|
156
|
+
`CreateCheckoutSessionRequest.Confirm`; a new Collect.js token subscribes in
|
|
157
|
+
one call. No package API change.
|
|
158
|
+
|
|
159
|
+
## Advertised checkout rails
|
|
160
|
+
|
|
161
|
+
- Breaking: `checkoutRails(offers)` takes OpenRails' checkout options alone.
|
|
162
|
+
Each option carries the `driver` and `public_config` OpenRails derived from
|
|
163
|
+
the armed PSP (Solana: `token_symbol`, `token_name`, `network`); the package
|
|
164
|
+
no longer derives drivers from `psps`. `CheckoutRailOffer` gains `selector`,
|
|
165
|
+
`driver` and `public_config`.
|
|
166
|
+
|
|
167
|
+
## Moved into OpenRails
|
|
168
|
+
|
|
169
|
+
The package now lives in `open-rails/openrails` under `sdk/billing-ui` and
|
|
170
|
+
takes the OpenRails version: each OpenRails release `vX.Y.Z` attaches
|
|
171
|
+
`openrails-billing-ui-X.Y.Z.tgz`. The e2e server and generated contract build
|
|
172
|
+
the OpenRails at the same commit. `OPENRAILS_VERSION` is gone from the
|
|
173
|
+
generated contract.
|
|
174
|
+
|
|
175
|
+
## 0.9.0
|
|
176
|
+
|
|
177
|
+
One embedded card panel for one-time purchases and subscriptions, on every
|
|
178
|
+
card PSP. Requires OpenRails v0.164.0.
|
|
179
|
+
|
|
180
|
+
- Stripe Elements checkout rail (`driver: "stripe_elements"`, from a PSP with
|
|
181
|
+
`flow: "elements"`): saved Stripe cards or a new card in the page, 3-D
|
|
182
|
+
Secure via `authenticatePayment`; no redirect to hosted Checkout. The
|
|
183
|
+
Payment Element is cards only (no Link bank tab or wallets).
|
|
184
|
+
- One screen: saved cards (brand •••• last4 · MM/YY, most recent first),
|
|
185
|
+
inline "Use a new card", one "Pay $X" / "Subscribe for $X every <period>"
|
|
186
|
+
button with the agreement line. No provider chooser for a single rail.
|
|
187
|
+
- New cards are always saved to the account (no consent checkbox) before
|
|
188
|
+
paying by id; Collect.js display metadata (`last_four`, `card_type`,
|
|
189
|
+
`expiry_date`) is sent with every token.
|
|
190
|
+
- Declines stay on the panel: `PayResult.failure` / `CheckoutSession.failure`
|
|
191
|
+
(`{ reason, message, field }`) render next to the card field; retry with
|
|
192
|
+
another card. `requires_action` + `operation_id` authenticates in the page.
|
|
193
|
+
- Collect.js `validationCallback`: inline number/expiry/CVC errors; the
|
|
194
|
+
button waits for three valid fields. Stripe Elements and Collect.js fields
|
|
195
|
+
are themed from the page tokens.
|
|
196
|
+
- `defaultCountry` on Checkout / SavePaymentMethod / AccountBilling /
|
|
197
|
+
TokenizedCardForm; fallback: the browser locale's region
|
|
198
|
+
(`initialCountry`, `browserCountry`).
|
|
199
|
+
- `PspConfig.checkout`; `checkoutPsps(psps)` — only checkout PSPs take new
|
|
200
|
+
cards. `isCardRail`, `useOptionalBillingContext`.
|
|
201
|
+
- `CheckoutModal`: always closable; `gate` renders host content (sign-in)
|
|
202
|
+
in place of the checkout.
|
|
203
|
+
- Account: card labels read "Visa •••• 4242 · 12/30"; failed payments show
|
|
204
|
+
`payment.failure.message`.
|
|
205
|
+
- Breaking: `paymentMethods.consent`/`enterCard` messages are replaced by
|
|
206
|
+
`paymentMethods.saveNotice`; `cardLabel` is "{brand} •••• {last4}".
|
|
207
|
+
|
|
208
|
+
## 0.8.0
|
|
209
|
+
|
|
210
|
+
Provider-neutral hosts: a host passes OpenRails's browser PSP configs through
|
|
211
|
+
and never branches on Stripe, NMI or any other provider.
|
|
212
|
+
|
|
213
|
+
- `checkoutRails(offers, psps)` / `savedMethodsFor(methods, rails)` derive the
|
|
214
|
+
checkout rails and chargeable saved cards from each PSP's `flow` and public
|
|
215
|
+
config.
|
|
216
|
+
- `SavePaymentMethod`: consented in-page card saving with any PSP — Collect.js
|
|
217
|
+
tokenization or Stripe Elements card setup (Stripe.js is loaded on first
|
|
218
|
+
use). `canSavePaymentMethod`, `cardSetupDriver`.
|
|
219
|
+
- `authenticatePayment(client, operationId, psp)`: 3-D Secure for a pending
|
|
220
|
+
payment operation. `canAuthenticatePayment`.
|
|
221
|
+
- Client: `createCardSetup`, `getCardSetup`, `confirmCardSetup`,
|
|
222
|
+
`getPaymentAuthentication`, `confirmPaymentAuthentication`.
|
|
223
|
+
- Breaking: `AccountBilling`/`PaymentMethodsPanel` take `psps` (and optional
|
|
224
|
+
`cardSetupReturnURL`) instead of `cardSetup`; `CardSetupConfig` is removed.
|
|
225
|
+
Stripe card setup needs OpenRails serving Stripe's `publishable_key`.
|
|
226
|
+
|
|
227
|
+
## 0.7.0
|
|
228
|
+
|
|
229
|
+
- Periods are exact. OpenRails windows are hours, so a 720h price reads
|
|
230
|
+
"every 30 days" (was "every month") and 8760h "every 365 days"; only exact
|
|
231
|
+
weeks, days or hours are named, in the account panels and the checkout
|
|
232
|
+
summary, in all six locales.
|
|
233
|
+
- Plural messages: `interval.every|per|access.<hour|day|week>` are CLDR plural
|
|
234
|
+
nodes (`{ one, other }`, exact `"1"` for 毎日-style forms) chosen by the
|
|
235
|
+
provider's `locale`; `Translator.plural(key, count)`. Breaking for hosts
|
|
236
|
+
overriding the removed `interval.day|week|month|year|days|hours` keys.
|
|
237
|
+
- Payment history names what was bought: the product's `display_name` from
|
|
238
|
+
OpenRails (`payment.product`, OpenRails after v0.160.0) and the renewal
|
|
239
|
+
period; older servers fall back to "Subscription"/"Purchase".
|
|
240
|
+
- Checkout summary copy ("Renews {period}") is a message: `checkout.renews`.
|
|
241
|
+
- The e2e harness and route contract pin OpenRails master `78039ea` (first
|
|
242
|
+
commit serving `payment.product`; untagged, after v0.160.0).
|
|
243
|
+
|
|
244
|
+
## 0.6.0
|
|
245
|
+
|
|
246
|
+
- "Add card" works against real OpenRails, which requires the PSP that
|
|
247
|
+
tokenized the card: `CardSetupConfig.provider` (the PSP key, e.g. `"nmi"`)
|
|
248
|
+
is required and sent as `NewCard.provider`. Before, every add was refused
|
|
249
|
+
with 400 `provider is required`.
|
|
250
|
+
|
|
251
|
+
## 0.5.2
|
|
252
|
+
|
|
253
|
+
- `renderSubscriptionFooter` (on `SubscriptionsPanel` and `AccountBilling`)
|
|
254
|
+
renders host content under a subscription row, e.g. a plan-change control.
|
|
255
|
+
- `useBillingRefresh()` (from `./react`) refetches every hook after a
|
|
256
|
+
host-side change such as a plan switch.
|
|
257
|
+
- The cancel and change-card dialogs use the panel's `appearance`; they read
|
|
258
|
+
only the provider's, so `AccountBilling appearance` missed them.
|
|
259
|
+
|
|
260
|
+
## 0.5.1
|
|
261
|
+
|
|
262
|
+
- `appearance.theme: "inherit"`: no bundled palette; components use the host
|
|
263
|
+
page's shadcn tokens and follow its `.dark` class.
|
|
264
|
+
- `SubscriptionsPanel`: "Change card" switches the saved card an active NMI
|
|
265
|
+
subscription renews on (`useSubscriptions().setPaymentMethod`), which now
|
|
266
|
+
notifies `subscription.payment_method_changed`.
|
|
267
|
+
|
|
268
|
+
## 0.5.0
|
|
269
|
+
|
|
270
|
+
Account billing, alongside checkout. No change to checkout's API.
|
|
271
|
+
|
|
272
|
+
- `@openrails/billing-ui/client`: `createBillingClient` for the OpenRails
|
|
273
|
+
customer surface (`/billing/v1/me/*`): subscriptions (list, get, cancel with
|
|
274
|
+
feedback, resume, payment method, Solana on-chain cancel), payment methods
|
|
275
|
+
(list, add tokenized card, remove, per-currency default), payments, invoices,
|
|
276
|
+
status. zod-validated responses, `BillingError` from the error envelope, the
|
|
277
|
+
pinned currency registry for exact money.
|
|
278
|
+
- `@openrails/billing-ui/react`: `BillingProvider` (`onChange` for host cache
|
|
279
|
+
invalidation), `useSubscriptions`, `usePaymentMethods`, `usePayments`.
|
|
280
|
+
- Styled: `AccountBilling`, `SubscriptionsPanel` with cancel/resume dialog and
|
|
281
|
+
provider-portal links, `PaymentMethodsPanel` (adding a card uses
|
|
282
|
+
`TokenizedCardForm`), `PaymentHistory`, `BillingStatusBadge`,
|
|
283
|
+
`BillingUiProvider`, `BillingUiRoot`.
|
|
284
|
+
- Messages: typed bundles with `en de es ja ko zh` under `./locales/*`.
|
|
285
|
+
- `formatAmount` takes an optional locale.
|
|
286
|
+
- The route contract and e2e suite run against real OpenRails v0.159.0.
|
|
287
|
+
|
|
288
|
+
## 0.4.2
|
|
289
|
+
|
|
290
|
+
No API or visual change.
|
|
291
|
+
|
|
292
|
+
- Components import `cn` from the [`cn`](https://github.com/shadcn-ui/cn)
|
|
293
|
+
package (pinned `0.4.0`), replacing `clsx` and `tailwind-merge`.
|
|
294
|
+
|
|
295
|
+
## 0.4.1
|
|
296
|
+
|
|
297
|
+
Visual only; no API change.
|
|
298
|
+
|
|
299
|
+
- UI primitives regenerated as shadcn `base-vega` on a zinc palette.
|
|
300
|
+
- Theme tokens now map to Tailwind colors, so token utilities (`bg-primary`,
|
|
301
|
+
`text-muted-foreground`, `border-input`, …) render; in 0.4.0 they emitted no
|
|
302
|
+
CSS, leaving the pay button and selected radios unstyled.
|
|
303
|
+
- `dark:` variants follow `appearance.theme` instead of the OS alone.
|
|
304
|
+
- `CheckoutModal` now paints its popover panel; buttons inside `.orck` drop the
|
|
305
|
+
UA button face.
|
|
306
|
+
|
|
307
|
+
## 0.4.0
|
|
308
|
+
|
|
309
|
+
Breaking: the package is renamed `openrails-checkout` → `@openrails/billing-ui`.
|
|
310
|
+
|
|
311
|
+
- Import from `@openrails/billing-ui` and `@openrails/billing-ui/styles.css`.
|
|
312
|
+
- The injected stylesheet is `<style id="billing-ui-styles" data-billing-ui>`.
|
|
313
|
+
- Releases attach the packed tarball to the GitHub release instead of
|
|
314
|
+
publishing to npm:
|
|
315
|
+
`https://github.com/open-rails/billing-ui/releases/download/v0.4.0/openrails-billing-ui-0.4.0.tgz`.
|
|
316
|
+
|
|
317
|
+
## 0.3.0
|
|
318
|
+
|
|
319
|
+
Breaking: the session document carries exact money.
|
|
320
|
+
|
|
321
|
+
- Accepted `processing` outcomes poll the same source without repeating payment
|
|
322
|
+
or tokenization. Ambiguous pay errors retain that pending attempt; local expiry
|
|
323
|
+
cannot turn it into a failure. Pending sessions may omit/null `expires_at`.
|
|
324
|
+
- Export `TokenizedCardForm` for separately consented NMI card setup, reusing the
|
|
325
|
+
existing hosted fields without presenting setup as a payment.
|
|
326
|
+
- Source changes fence late payment callbacks from the previous checkout.
|
|
327
|
+
|
|
328
|
+
- `plan.unit_amount`, `line_items[].amount`, `tax` and `due_today` are int64
|
|
329
|
+
decimal strings of `plan.currency`'s native unit; `plan.unit_decimals` (the
|
|
330
|
+
currency's registered scale) is required. Numeric amounts are refused as an
|
|
331
|
+
unavailable session. Replaces `unit_amount_micros`, `amount_micros`,
|
|
332
|
+
`tax_micros` and `due_today_micros`, which were JSON numbers at an assumed
|
|
333
|
+
six decimals.
|
|
334
|
+
- Rendering never passes money through a JS number (`BigInt` scaling, exact
|
|
335
|
+
Intl decimal formatting, refusal instead of rounding). `formatAmount`,
|
|
336
|
+
`amountToDecimal`, `addAmounts`, `isAmount`, `amountSchema` and
|
|
337
|
+
`unitDecimalsSchema` are exported.
|
|
338
|
+
- Hosts build the plan with `openrails.NewHostedCheckoutPlan` (OpenRails
|
|
339
|
+
`HostedCheckoutSession` types); the package decodes OpenRails' canonical
|
|
340
|
+
`hosted_checkout_session.json` fixture.
|
|
341
|
+
|
|
342
|
+
## 0.2.5
|
|
343
|
+
|
|
344
|
+
- A Solana option is named after the host-bound token and network; the former
|
|
345
|
+
`USDC` default is removed.
|
|
346
|
+
- Releases publish to npm through the GitHub release workflow.
|
|
347
|
+
|
|
348
|
+
## 0.2.4
|
|
349
|
+
|
|
350
|
+
- Hosts must accept `name_on_card` on the pay endpoint; `first_name` and
|
|
351
|
+
`last_name` are no longer emitted.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 OpenRails and Paul Fidika
|
|
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,261 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @openrails/billing-ui
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Embeddable OpenRails billing UI: the checkout flow and the customer's account
|
|
4
|
+
billing (subscriptions, saved cards, payment history).
|
|
5
|
+
|
|
6
|
+
| Import | Contents |
|
|
7
|
+
| --------------------------------- | ---------------------------------------------------------------------------------- |
|
|
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 |
|
|
11
|
+
| `@openrails/billing-ui/locales/*` | `en de es ja ko zh` message bundles |
|
|
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
|
|
16
|
+
never enters the host application, unless the PSP takes cards on OpenRails
|
|
17
|
+
itself (`card_entry: server`, driver `card`).
|
|
18
|
+
|
|
19
|
+
See [Payment form contract](docs/payment-form-contract.md) for the exact-money
|
|
20
|
+
session document, billing fields, browser-autofill behavior, and the checkout
|
|
21
|
+
request schema.
|
|
22
|
+
|
|
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`.
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
pnpm add https://github.com/open-rails/openrails/releases/download/vX.Y.Z/openrails-billing-ui-X.Y.Z.tgz
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Checkout
|
|
31
|
+
|
|
32
|
+
```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
|
+
})
|
|
42
|
+
|
|
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()} />
|
|
47
|
+
```
|
|
48
|
+
|
|
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`.
|
|
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.
|
|
91
|
+
|
|
92
|
+
The payment host serves `<CheckoutPage>` from one HTML entry at `PageURL`,
|
|
93
|
+
behind its adapter's `CheckoutFramePolicy` (only the sites in
|
|
94
|
+
`Config.Checkout.EmbedOrigins` may frame it):
|
|
95
|
+
|
|
96
|
+
```tsx
|
|
97
|
+
import { BillingUiProvider, CheckoutPage } from "@openrails/billing-ui"
|
|
98
|
+
|
|
99
|
+
createRoot(root).render(
|
|
100
|
+
<BillingUiProvider>
|
|
101
|
+
<CheckoutPage appearance={{ variables: brand }} />
|
|
102
|
+
</BillingUiProvider>
|
|
103
|
+
)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The page reads the session id from its URL fragment and talks to its frame
|
|
107
|
+
with `postMessage`, origin-checked both ways: page to app `ready`,
|
|
108
|
+
`resize {height}`, `complete {status}` and `redirect {url}` (a nested frame
|
|
109
|
+
cannot navigate the top window, so the app does); app to page
|
|
110
|
+
`init {theme}`. The page messages only the app origin recorded on its session.
|
|
111
|
+
|
|
112
|
+
## Account billing
|
|
113
|
+
|
|
114
|
+
```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"
|
|
118
|
+
import { de } from "@openrails/billing-ui/locales/de"
|
|
119
|
+
|
|
120
|
+
// auth-ui's authFetch attaches the bearer and retries once after a refresh;
|
|
121
|
+
// `getToken: () => token` works for any other auth.
|
|
122
|
+
const billing = createBillingClient({ baseUrl: "/billing/v1", fetch: auth.authFetch })
|
|
123
|
+
|
|
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>
|
|
133
|
+
```
|
|
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
|
+
|
|
142
|
+
## Catalog and plan changes
|
|
143
|
+
|
|
144
|
+
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.
|
|
148
|
+
|
|
149
|
+
| Call | Route |
|
|
150
|
+
| --------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
|
|
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` |
|
|
153
|
+
| `previewTierChange(id, priceId)` | `POST /me/subscriptions/{id}/change-tier/preview` |
|
|
154
|
+
| `changeTier(id, { priceId, idempotencyKey, signature? })` | `POST /me/subscriptions/{id}/change-tier` |
|
|
155
|
+
| `listSolanaTokens({ priceId, wallet })` | `GET /solana/tokens` |
|
|
156
|
+
|
|
157
|
+
```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,
|
|
161
|
+
idempotencyKey,
|
|
162
|
+
})
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
- `changeTier` is a money write. Mint one `idempotencyKey` per attempt and
|
|
166
|
+
reuse it until the change resolves (`status: "processing"`, a
|
|
167
|
+
`tier_change_in_flight` refusal naming `metadata.operation_id`, a 5xx or a
|
|
168
|
+
lost response), so OpenRails replays the stored result instead of charging
|
|
169
|
+
twice. `requires_action` carries `operation_id` for `authenticatePayment`;
|
|
170
|
+
replay the same key afterwards.
|
|
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`.
|
|
179
|
+
|
|
180
|
+
## Provider-neutral flows
|
|
181
|
+
|
|
182
|
+
OpenRails's browser PSP configs (`GET /config`'s `payment.psps`) pick the
|
|
183
|
+
browser flow from each PSP's `flow` and public `config`
|
|
184
|
+
(Collect.js, native card inputs, Stripe Elements, redirect, wallet). Hosts never branch on a
|
|
185
|
+
provider:
|
|
186
|
+
|
|
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).
|
|
191
|
+
Card rails (`collect_js`, `card`, `stripe_elements`) render one panel: saved cards,
|
|
192
|
+
an inline new card and one Pay/Subscribe button, which is the customer's
|
|
193
|
+
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
|
|
196
|
+
carrying an `operation` run 3-D Secure in the page. A `failed` result stays
|
|
197
|
+
on the panel with `failure.message` next to its `field`, so the buyer can
|
|
198
|
+
pick another card; the host gives each new attempt a new idempotency key.
|
|
199
|
+
- Only PSPs with `checkout !== false` (`checkoutPsps`) take new cards; others
|
|
200
|
+
stay listed so existing cards and subscriptions keep working.
|
|
201
|
+
- `<SavePaymentMethod psp={psp} onSaved={(id) => ...} />` saves a card in the
|
|
202
|
+
page (needs `BillingProvider`); `canSavePaymentMethod(psp)` filters PSPs
|
|
203
|
+
that support it. After an off-page verification the provider returns to
|
|
204
|
+
`returnURL(setupId)`; confirm with `client.confirmCardSetup(id)`.
|
|
205
|
+
- `authenticatePayment(client, operationId, psp)` completes a pending
|
|
206
|
+
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.
|
|
211
|
+
|
|
212
|
+
- Panels: `SubscriptionsPanel` (cancel, resume, change card),
|
|
213
|
+
`PaymentMethodsPanel`, `PaymentHistory`, `BillingStatusBadge`,
|
|
214
|
+
`CancelSubscriptionDialog`; `AccountBilling` stacks them.
|
|
215
|
+
- `renderSubscriptionFooter={(s) => ...}` adds host content under a
|
|
216
|
+
subscription row; `useBillingRefresh()` refetches after host-side changes.
|
|
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.
|
|
224
|
+
- Money is exact: `/me` amounts are int64 native-unit strings, scaled by the
|
|
225
|
+
OpenRails currency registry (`currencies` option to extend it).
|
|
226
|
+
- Messages: English is complete and the fallback; `messages` layers bundles,
|
|
227
|
+
`t` lets the host's i18n win. `useMessages().error(err)` maps error codes.
|
|
228
|
+
Count-dependent messages are CLDR plural nodes (`{ one, other }`, plus an
|
|
229
|
+
exact `"1"` like ICU `=1`), selected with `locale`.
|
|
230
|
+
- Periods are exact: OpenRails windows are hours, so 720h reads "every 30
|
|
231
|
+
days", never "monthly"; only exact weeks, days or hours are named.
|
|
232
|
+
- Styles are scoped under `.orck`; `appearance.theme` is `light`, `dark`,
|
|
233
|
+
`auto` or `inherit`. `inherit` bundles no palette: the host page's shadcn
|
|
234
|
+
tokens (`--background`, `--primary`, ...) and its `.dark` class apply.
|
|
235
|
+
|
|
236
|
+
## UI primitives
|
|
237
|
+
|
|
238
|
+
`src/components/ui/*` is shadcn (`base-vega`, zinc; see `components.json`),
|
|
239
|
+
managed with `pnpm dlx shadcn@4.21.0 add <name> --overwrite`, importing `cn`
|
|
240
|
+
from the [`cn`](https://github.com/shadcn-ui/cn) package.
|
|
241
|
+
|
|
242
|
+
## E2E against real OpenRails
|
|
243
|
+
|
|
244
|
+
`e2e/server` is a Go module that builds the OpenRails at this commit (`replace`
|
|
245
|
+
to the repository root) with AuthKit. It serves the
|
|
246
|
+
embedded `/billing/v1` API and `/auth/v1` on a throwaway `postgres:18-alpine`
|
|
247
|
+
container (Docker and Go required), with test-only `POST /__test/users` (user +
|
|
248
|
+
access token) and `POST /__test/users/{id}/billing` (imported subscription,
|
|
249
|
+
sale and saved card on a credential-less NMI PSP).
|
|
250
|
+
|
|
251
|
+
```sh
|
|
252
|
+
pnpm test:e2e # Playwright against the real server
|
|
253
|
+
pnpm contract # regenerate OpenRails' contract files, src/client/generated among them
|
|
254
|
+
pnpm contract:check # fail if a generated contract file is stale
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
`e2e/openrails/account.spec.ts` drives the packaged `AccountBilling` (built from
|
|
258
|
+
`dist/`) through list, cancel, resume, the in-use card refusal and history. Set
|
|
259
|
+
`BILLING_UI_SCREENSHOTS=<dir>` to capture light, dark and mobile screenshots.
|
|
260
|
+
Card deletion calls NMI, which the harness has no credentials for; jsdom tests
|
|
261
|
+
cover it.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { CheckoutAppearance } from '../appearance';
|
|
2
|
+
import { SendSolanaTransaction } from '../client/client';
|
|
3
|
+
import { PaymentMethodsPanelProps } from './payment-methods-panel';
|
|
4
|
+
import { SubscriptionsPanelProps } from './subscriptions-panel';
|
|
5
|
+
export interface AccountBillingProps {
|
|
6
|
+
cardSetupReturnURL?: PaymentMethodsPanelProps["cardSetupReturnURL"];
|
|
7
|
+
/** Offers to make a card the one that collects this currency's invoices. */
|
|
8
|
+
collectionCurrency?: string;
|
|
9
|
+
/** Billing country to preselect when adding a card. */
|
|
10
|
+
defaultCountry?: string;
|
|
11
|
+
sendSolanaTransaction?: SendSolanaTransaction;
|
|
12
|
+
plansHref?: string;
|
|
13
|
+
renderSubscriptionFooter?: SubscriptionsPanelProps["renderSubscriptionFooter"];
|
|
14
|
+
historyPageSize?: number;
|
|
15
|
+
appearance?: CheckoutAppearance;
|
|
16
|
+
className?: string;
|
|
17
|
+
}
|
|
18
|
+
/** 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;
|