@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.
- package/CHANGELOG.md +142 -2
- package/README.md +119 -85
- package/dist/account/account-billing.d.ts +6 -5
- package/dist/account/change-card-dialog.d.ts +5 -2
- package/dist/account/payment-methods-panel.d.ts +6 -5
- package/dist/buy-button.d.ts +35 -0
- package/dist/cart.d.ts +18 -0
- package/dist/checkout.d.ts +1 -3
- package/dist/client/checkout.d.ts +28 -0
- package/dist/client/client.d.ts +98 -63
- package/dist/client/errors.d.ts +5 -4
- package/dist/client/generated/error-codes.d.ts +1 -1
- package/dist/client/generated/routes.d.ts +142 -99
- package/dist/client/generated/wire.d.ts +185 -98
- package/dist/client/index.d.ts +2 -2
- package/dist/client/types.d.ts +272 -29
- package/dist/{client-CWX6QpLa.js → client-BuvolBPT.js} +498 -326
- package/dist/client-BuvolBPT.js.map +1 -0
- package/dist/client.js +2 -2
- package/dist/components/stripe-card.d.ts +8 -2
- package/dist/components/stripe-wallets.d.ts +12 -0
- package/dist/components/ui/badge.d.ts +1 -1
- package/dist/components/ui/button.d.ts +1 -1
- package/dist/frame.d.ts +5 -5
- package/dist/hooks-2d-XjpuI.js +526 -0
- package/dist/hooks-2d-XjpuI.js.map +1 -0
- package/dist/i18n/context.d.ts +1 -1
- package/dist/i18n/messages.d.ts +1 -1
- package/dist/index.d.ts +7 -5
- package/dist/index.js +1985 -1667
- package/dist/index.js.map +1 -1
- package/dist/lib/money.d.ts +1 -0
- package/dist/lib/offer-label.d.ts +4 -0
- package/dist/locales/de.js +25 -4
- package/dist/locales/de.js.map +1 -1
- package/dist/locales/en.d.ts +25 -4
- package/dist/locales/en.js +25 -4
- package/dist/locales/en.js.map +1 -1
- package/dist/locales/es.js +25 -4
- package/dist/locales/es.js.map +1 -1
- package/dist/locales/ja.js +16 -4
- package/dist/locales/ja.js.map +1 -1
- package/dist/locales/ko.js +16 -4
- package/dist/locales/ko.js.map +1 -1
- package/dist/locales/zh.js +16 -4
- package/dist/locales/zh.js.map +1 -1
- package/dist/offers.d.ts +33 -0
- package/dist/order-checkout.d.ts +30 -0
- package/dist/react/context.d.ts +6 -5
- package/dist/react/hooks.d.ts +17 -16
- package/dist/react/index.d.ts +1 -1
- package/dist/react/provider.d.ts +24 -2
- package/dist/react.js +2 -33
- package/dist/save-payment-method.d.ts +0 -6
- package/dist/styles.css +1 -1
- package/dist/types.d.ts +16 -16
- package/docs/payment-form-contract.md +6 -6
- package/package.json +2 -2
- package/dist/client-CWX6QpLa.js.map +0 -1
- package/dist/config-DZ97SQG2.js +0 -341
- package/dist/config-DZ97SQG2.js.map +0 -1
- package/dist/provider.d.ts +0 -18
- 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
|
|
6
|
-
|
|
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` |
|
|
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
|
|
14
|
-
session: the signed-in customer mints
|
|
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`
|
|
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
|
|
28
|
+
pnpm add @openrails/billing-ui@X.Y.Z
|
|
28
29
|
```
|
|
29
30
|
|
|
30
|
-
##
|
|
31
|
+
## Buying
|
|
32
|
+
|
|
33
|
+
A React app imports from one place:
|
|
31
34
|
|
|
32
35
|
```tsx
|
|
33
|
-
import { createBillingClient } from "@openrails/billing-ui
|
|
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
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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.
|
|
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
|
-
|
|
90
|
-
|
|
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 {
|
|
125
|
+
import { BillingProvider, CheckoutPage } from "@openrails/billing-ui"
|
|
98
126
|
|
|
99
127
|
createRoot(root).render(
|
|
100
|
-
<
|
|
128
|
+
<BillingProvider>
|
|
101
129
|
<CheckoutPage appearance={{ variables: brand }} />
|
|
102
|
-
</
|
|
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
|
|
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
|
-
<
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
146
|
-
|
|
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()` (
|
|
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
|
-
| `
|
|
154
|
-
| `
|
|
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.
|
|
159
|
-
const change = await billing.
|
|
160
|
-
|
|
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
|
-
-
|
|
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
|
-
`
|
|
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 `
|
|
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
|
|
195
|
-
and the
|
|
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` (
|
|
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
|
-
`
|
|
219
|
-
`remove`, `
|
|
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
|
-
`
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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({
|
|
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
|
-
/**
|
|
11
|
-
|
|
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
|
-
/**
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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({
|
|
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;
|
package/dist/checkout.d.ts
CHANGED
|
@@ -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,
|
|
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;
|