@openrails/billing-ui 0.235.0 → 0.237.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 +155 -0
- package/README.md +103 -103
- package/dist/account/account-billing.d.ts +2 -4
- package/dist/account/payment-methods-panel.d.ts +2 -4
- package/dist/buy-button.d.ts +40 -0
- package/dist/cart.d.ts +18 -0
- package/dist/client/client.d.ts +95 -59
- package/dist/client/errors.d.ts +4 -0
- package/dist/client/generated/error-codes.d.ts +1 -1
- package/dist/client/generated/routes.d.ts +103 -272
- package/dist/client/generated/wire.d.ts +117 -246
- package/dist/client/index.d.ts +2 -2
- package/dist/client/types.d.ts +286 -142
- package/dist/client-D9G1Tgox.js +888 -0
- package/dist/client-D9G1Tgox.js.map +1 -0
- package/dist/client.js +2 -2
- package/dist/components/eyebrow.d.ts +5 -0
- 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/embedded-checkout.d.ts +22 -0
- package/dist/hooks-DYepYVMX.js +526 -0
- package/dist/hooks-DYepYVMX.js.map +1 -0
- package/dist/i18n/context.d.ts +1 -1
- package/dist/index.d.ts +9 -11
- package/dist/index.js +1470 -2178
- 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 +20 -0
- package/dist/locales/de.js.map +1 -1
- package/dist/locales/en.d.ts +20 -0
- package/dist/locales/en.js +20 -0
- package/dist/locales/en.js.map +1 -1
- package/dist/locales/es.js +20 -0
- package/dist/locales/es.js.map +1 -1
- package/dist/locales/ja.js +11 -0
- package/dist/locales/ja.js.map +1 -1
- package/dist/locales/ko.js +11 -0
- package/dist/locales/ko.js.map +1 -1
- package/dist/locales/zh.js +11 -0
- package/dist/locales/zh.js.map +1 -1
- package/dist/modal.d.ts +3 -9
- package/dist/offers.d.ts +32 -0
- package/dist/order-checkout.d.ts +30 -0
- package/dist/psp.d.ts +0 -8
- package/dist/react/hooks.d.ts +8 -7
- 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/tokenized-card-form.d.ts +5 -1
- package/dist/types.d.ts +12 -250
- package/docs/payment-form-contract.md +31 -74
- package/package.json +1 -2
- package/dist/checkout-frame.d.ts +0 -17
- package/dist/checkout-page.d.ts +0 -21
- package/dist/checkout.d.ts +0 -24
- package/dist/client-BpzCZes3.js +0 -931
- package/dist/client-BpzCZes3.js.map +0 -1
- package/dist/components/ccbill-fields.d.ts +0 -8
- package/dist/components/methods.d.ts +0 -13
- package/dist/components/summary.d.ts +0 -12
- package/dist/config-qLcHvK0Q.js +0 -341
- package/dist/config-qLcHvK0Q.js.map +0 -1
- package/dist/fixtures.d.ts +0 -9
- package/dist/frame.d.ts +0 -47
- package/dist/lib/rail-meta.d.ts +0 -15
- package/dist/lib/redirect.d.ts +0 -2
- package/dist/provider.d.ts +0 -18
- package/dist/react.js.map +0 -1
- package/dist/source.d.ts +0 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,160 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Running charges
|
|
4
|
+
|
|
5
|
+
- `Balance` gains `accrued_amount`, `accrual_rate_per_hour`, `as_of` and
|
|
6
|
+
`depletes_at`: what the customer's running charges have charged and not yet
|
|
7
|
+
posted, the rate they draw the balance at, and when it runs out.
|
|
8
|
+
`available_amount` is net of the accrual.
|
|
9
|
+
|
|
10
|
+
## Orders replace checkout sessions
|
|
11
|
+
|
|
12
|
+
- Breaking: `<BuyButton>` and `<Offers>` create the customer's order and pay
|
|
13
|
+
it in `<OrderCheckout>`; `onPaid` receives the completed `Order`.
|
|
14
|
+
`returnUrl` replaces `successUrl`, and `autoRenew` is gone.
|
|
15
|
+
- Breaking: `<CheckoutPage>`, `<CheckoutFrame>` and the shared payment page
|
|
16
|
+
are removed with checkout sessions; so are `CheckoutLayout`,
|
|
17
|
+
`CheckoutFrameTheme`, `CheckoutSession`, `CheckoutSessionStatus`,
|
|
18
|
+
`CheckoutPlan`, `CheckoutLineItem`, `CheckoutPhase`, `PaymentOption`,
|
|
19
|
+
`PayRequest`, `PayResult`, their schemas, `amountSchema`,
|
|
20
|
+
`unitDecimalsSchema`, `isCardRail`, `savedMethodsFor` and the client's
|
|
21
|
+
`CheckoutSessionLink`. `NextAction` and `PaymentFailure` are the order's,
|
|
22
|
+
from `@openrails/billing-ui/client`.
|
|
23
|
+
- A page that paid a server-minted session pays the server's order instead:
|
|
24
|
+
the Go `Client.CreateOrder` creates it, and `<OrderCheckout orderId>` pays
|
|
25
|
+
it, a subscription line included.
|
|
26
|
+
- `SavedPaymentMethod` names its `psp_id` (was `option_id`).
|
|
27
|
+
|
|
28
|
+
## Usage orders
|
|
29
|
+
|
|
30
|
+
- Breaking: `listInvoices`, `getInvoice` and `Invoice` are removed. Usage
|
|
31
|
+
billed in arrears is an order (`origin: usage`): `listOrders({ origin:
|
|
32
|
+
"usage" })` lists them, and `<OrderCheckout>` pays one with a saved card.
|
|
33
|
+
- `Order` gains `usage` (its period, `due_at`, `amount_paid`, `amount_due`,
|
|
34
|
+
collection and `delinquent`) and `uncollectible_at`; `status` may be
|
|
35
|
+
`uncollectible`. A usage line names its `source`; its `price_id` and
|
|
36
|
+
`product_id` are null.
|
|
37
|
+
- `Order.checkout.expires_at` is read again.
|
|
38
|
+
|
|
39
|
+
## Embedded checkout
|
|
40
|
+
|
|
41
|
+
- `<EmbeddedCheckout url onComplete?>` frames a merchant order's embedded
|
|
42
|
+
checkout (`checkout.ui_mode: embedded`): it follows the page's height and
|
|
43
|
+
hears its completion only from its own frame on the checkout host's origin;
|
|
44
|
+
without `onComplete` it goes to the checkout's `success_url`.
|
|
45
|
+
- `Order.checkout.ui_mode`; `PublicMerchant.embed_origins`;
|
|
46
|
+
`PublicConfig.network` (the checkout page's sign-in to the customer network).
|
|
47
|
+
- `createNetworkLinkChallenge()` and `createNetworkLink({challengeId, idToken})`
|
|
48
|
+
link the customer to a customer-network account.
|
|
49
|
+
|
|
50
|
+
## No Solana token list
|
|
51
|
+
|
|
52
|
+
- `listSolanaTokens` and `SolanaToken` are gone with `GET /v1/solana/tokens`:
|
|
53
|
+
the accepted tokens are `getConfig()`'s `payment.solana.tokens`.
|
|
54
|
+
|
|
55
|
+
## Apple Pay and Google Pay
|
|
56
|
+
|
|
57
|
+
- `<OrderCheckout wallets>` offers Apple Pay and Google Pay through a Stripe
|
|
58
|
+
PSP's Express Checkout Element, above the saved and new cards. Off by
|
|
59
|
+
default: the page's host must be a payment-method domain registered on that
|
|
60
|
+
Stripe account.
|
|
61
|
+
|
|
62
|
+
## Renewals are orders
|
|
63
|
+
|
|
64
|
+
- `Order` gains `renewal` (a subscription renewal's period and `due_at`) and
|
|
65
|
+
`voided_at`; `expires_at` is null on a renewal; `status` may be `void`.
|
|
66
|
+
- `PaymentRecovery` gains `order_id`: an engine-owned subscription's unpaid
|
|
67
|
+
renewal, paid through `<OrderCheckout>`.
|
|
68
|
+
- `<OrderCheckout>` offers new card entry only where the order takes one (a
|
|
69
|
+
renewal takes saved cards), and lets the customer authenticate a payment
|
|
70
|
+
their bank challenged while they were away.
|
|
71
|
+
|
|
72
|
+
## Hosted checkout
|
|
73
|
+
|
|
74
|
+
- `getConfig()` returns `merchant` (`PublicMerchant`: `display_name`, and
|
|
75
|
+
`logo_url` and `support_url` when they are https), null when the request
|
|
76
|
+
resolves no merchant.
|
|
77
|
+
- `Order` carries `checkout` (its hosted checkout: `expires_at`,
|
|
78
|
+
`success_url`, `cancel_url`, `saved_payment_methods`; `url` only on the
|
|
79
|
+
answer that made it), null without one.
|
|
80
|
+
|
|
81
|
+
## Orders take Solana and hosted pages
|
|
82
|
+
|
|
83
|
+
- `<OrderCheckout>` offers an order's Solana option: one button per token,
|
|
84
|
+
then the Solana Pay QR of the order's `solana_pay` next action, confirmed
|
|
85
|
+
until the transfer lands.
|
|
86
|
+
- A PSP with no fields in the page (Stripe without a publishable key) takes
|
|
87
|
+
the order on its hosted page: `<OrderCheckout>` sends the customer there and
|
|
88
|
+
back to `returnUrl` (default: the current page).
|
|
89
|
+
- `OrderPaymentInput` gains `{ token_symbol, psp_id? }` and
|
|
90
|
+
`{ return_url, psp_id? }`.
|
|
91
|
+
|
|
92
|
+
## OrderCheckout pays an order
|
|
93
|
+
|
|
94
|
+
- `<OrderCheckout orderId>` shows an order and pays it with a saved card or a
|
|
95
|
+
new card in the PSP's fields, running any `next_action` and `confirm`. It
|
|
96
|
+
takes an optional `client` and offers only cards kept for one-click buys
|
|
97
|
+
(`PaymentMethod.reusable`).
|
|
98
|
+
- The client gains `getOrder`, and `payOrder` and `confirmOrder`, which answer
|
|
99
|
+
`{ order, error? }`: a declined payment (402) is the order with
|
|
100
|
+
`payment.last_payment_error`, not a throw. `BillingError` carries `order`.
|
|
101
|
+
- `StripeCardEntry`'s handle gains `tokenize()` and `handleNextAction()`.
|
|
102
|
+
|
|
103
|
+
## A card is saved in one call
|
|
104
|
+
|
|
105
|
+
- `addPaymentMethod` takes `token` (was `payment_token`). A Stripe card is a
|
|
106
|
+
`pm_` from Stripe's fields, saved by the same call; a card the bank wants to
|
|
107
|
+
authenticate answers `status: "requires_action"` with `next_action`, and
|
|
108
|
+
`confirmPaymentMethod(id)` finishes it after Stripe.js `handleNextAction`.
|
|
109
|
+
`StripeCardEntry` and `SavePaymentMethod` do this themselves.
|
|
110
|
+
- `createCardSetup`, `getCardSetup`, `confirmCardSetup`, the `CardSetup` type
|
|
111
|
+
and the `returnURL` / `cardSetupReturnURL` props are gone: 3-D Secure runs in
|
|
112
|
+
the page.
|
|
113
|
+
|
|
114
|
+
## Offers sell products by key
|
|
115
|
+
|
|
116
|
+
- The public catalog is a lookup by product key: `listProducts({ keys })` and
|
|
117
|
+
`useProducts({ keys })` take the keys alone (1 to 100) and read one page;
|
|
118
|
+
`entitlements`, `limit` and `cursor` are gone, and so is
|
|
119
|
+
`ProductsState.nextCursor`. No keys fetch nothing.
|
|
120
|
+
- `<Offers keys>` replaces `<Offers entitlements>`: your server names the
|
|
121
|
+
products, with the Go `Client.ListOffers` when it finds them by
|
|
122
|
+
entitlement.
|
|
123
|
+
|
|
124
|
+
## Apps buy through BuyButton and Offers
|
|
125
|
+
|
|
126
|
+
- `Checkout` and `CheckoutModal` (and their props), the fixture sources and
|
|
127
|
+
the `CheckoutSource` type are no longer exported; the client no longer has
|
|
128
|
+
`createCheckoutSession` or `checkoutSource`. Buy with `<BuyButton>` (it takes
|
|
129
|
+
`amount`, `autoRenew` and `successUrl`) or `<Offers>`. `CheckoutPage` and
|
|
130
|
+
`CheckoutFrame` stay, for the shared payment page.
|
|
131
|
+
- The main entry also exports `createBillingClient` (with `BillingClient`,
|
|
132
|
+
`BillingClientOptions` and `ProductListOptions`) beside `formatAmount`, so a
|
|
133
|
+
React app imports from `@openrails/billing-ui` alone; `./client` and
|
|
134
|
+
`./react` are unchanged.
|
|
135
|
+
|
|
136
|
+
## One provider
|
|
137
|
+
|
|
138
|
+
- `BillingProvider` takes what `BillingUiProvider` took (`appearance`,
|
|
139
|
+
`messages`, `t`, `locale`, `navigate`) beside `client` and `onChange`, and
|
|
140
|
+
`BillingUiProvider` is gone. The main entry exports it; `./react` exports the
|
|
141
|
+
same component for hook-only apps, without the stylesheet. `client` is
|
|
142
|
+
optional: leave it out where no customer signs in, such as `CheckoutPage`.
|
|
143
|
+
- Inside a provider with a client, the checkout saves a new card to the
|
|
144
|
+
customer first and pays on the customer surface.
|
|
145
|
+
|
|
146
|
+
## Offers sell an entitlement
|
|
147
|
+
|
|
148
|
+
- `<BuyButton product price onPaid onSignInRequired>` buys one price, labelled
|
|
149
|
+
from the catalog unless given `label`. The app never touches a checkout
|
|
150
|
+
session: when orders take a new card (#1168) it creates an order instead.
|
|
151
|
+
- `<Offers entitlements onPaid onSignInRequired>` lists every product on sale
|
|
152
|
+
granting any of the entitlements, a `BuyButton` per price; `products` feeds
|
|
153
|
+
it from the host's server instead.
|
|
154
|
+
- `listProducts({ entitlements, keys })` and `useProducts({ entitlements, keys })`
|
|
155
|
+
filter the public catalog. One of them is required: OpenRails no longer
|
|
156
|
+
lists the whole catalog publicly.
|
|
157
|
+
|
|
3
158
|
## Checkout saves a new card under its PSP
|
|
4
159
|
|
|
5
160
|
- Inside a `BillingProvider`, `Checkout` saved a new card with the session
|
package/README.md
CHANGED
|
@@ -6,19 +6,17 @@ 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
|
-
|
|
15
|
-
|
|
16
|
-
never enters the host application
|
|
17
|
-
itself (`card_entry: server`, driver `card`).
|
|
13
|
+
The checkout owns the browser payment flow, behind `<BuyButton>`, `<Offers>`
|
|
14
|
+
and `<OrderCheckout>`: the signed-in customer's order, read and paid with
|
|
15
|
+
their own credential. Card data is tokenized in the PSP's own fields
|
|
16
|
+
(Collect.js, Stripe Elements) and never enters the host application.
|
|
18
17
|
|
|
19
|
-
See [Payment form contract](docs/payment-form-contract.md) for
|
|
20
|
-
|
|
21
|
-
request schema.
|
|
18
|
+
See [Payment form contract](docs/payment-form-contract.md) for exact money,
|
|
19
|
+
billing fields, browser-autofill behavior and the payment request.
|
|
22
20
|
|
|
23
21
|
The package lives in the OpenRails repository and shares its version: each
|
|
24
22
|
OpenRails release `vX.Y.Z` publishes `@openrails/billing-ui@X.Y.Z` to npm, with
|
|
@@ -28,35 +26,57 @@ provenance.
|
|
|
28
26
|
pnpm add @openrails/billing-ui@X.Y.Z
|
|
29
27
|
```
|
|
30
28
|
|
|
31
|
-
##
|
|
29
|
+
## Buying
|
|
30
|
+
|
|
31
|
+
A React app imports from one place:
|
|
32
32
|
|
|
33
33
|
```tsx
|
|
34
|
-
import { createBillingClient } from "@openrails/billing-ui
|
|
35
|
-
import { Checkout, CheckoutFrame } from "@openrails/billing-ui"
|
|
36
|
-
import "@openrails/billing-ui/styles.css"
|
|
37
|
-
|
|
38
|
-
const billing = createBillingClient({ fetch: auth.authFetch })
|
|
39
|
-
const session = await billing.createCheckoutSession({
|
|
40
|
-
productKey: "pro",
|
|
41
|
-
priceKey: "monthly",
|
|
42
|
-
})
|
|
34
|
+
import { BillingProvider, BuyButton, Offers, createBillingClient, formatAmount } from "@openrails/billing-ui"
|
|
43
35
|
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
36
|
+
// auth-ui's authFetch attaches the bearer and retries once after a refresh;
|
|
37
|
+
// `getToken: () => token` works for any other auth.
|
|
38
|
+
const billing = createBillingClient({ baseUrl: "/billing/v1", fetch: auth.authFetch })
|
|
39
|
+
|
|
40
|
+
<BillingProvider client={billing} appearance={{ theme: "auto" }}>
|
|
41
|
+
<App />
|
|
42
|
+
</BillingProvider>
|
|
48
43
|
```
|
|
49
44
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
45
|
+
`<BuyButton>` buys one price, `<Offers>` the products your server names by
|
|
46
|
+
key (a course, its bundle and the membership that also unlocks it; the Go
|
|
47
|
+
`Client.ListOffers` finds the products granting an entitlement): a
|
|
48
|
+
`BuyButton` per price ("$4.99", "Rent for 3 days, $1.99",
|
|
49
|
+
"$10.00 every 30 days"). The purchase is theirs from start to payment: a
|
|
50
|
+
button creates the customer's order and pays it in a dialog
|
|
51
|
+
(`<OrderCheckout>`); a second click before it is paid opens the same order.
|
|
52
|
+
Signing in is the host's: with `signedIn={false}`, or when OpenRails answers
|
|
53
|
+
401, they call `onSignInRequired` instead. `onPaid` receives the completed
|
|
54
|
+
order; it is a hint, so confirm access from your own API (your gate) before
|
|
55
|
+
granting anything.
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
<BuyButton
|
|
59
|
+
product="course-101"
|
|
60
|
+
price="rent" // labelled from the catalog unless you pass label
|
|
61
|
+
signedIn={signedIn}
|
|
62
|
+
onSignInRequired={() => openSignIn()}
|
|
63
|
+
onPaid={() => navigate("/courses/css-101")} // your gate now admits them
|
|
64
|
+
/>
|
|
65
|
+
|
|
66
|
+
<Offers
|
|
67
|
+
keys={["course-101", "course-bundle", "channel-membership"]} // from your server, or products={...} from its Client.ListOffers
|
|
68
|
+
signedIn={signedIn}
|
|
69
|
+
onSignInRequired={() => openSignIn()}
|
|
70
|
+
onPaid={() => navigate("/courses/css-101")}
|
|
71
|
+
/>
|
|
72
|
+
```
|
|
55
73
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
74
|
+
`returnUrl` returns the buyer from a redirect step (Stripe's hosted page); it
|
|
75
|
+
must be on one of your app's return origins.
|
|
76
|
+
|
|
77
|
+
For prepaid credits, a fixed pack is another product/price. A
|
|
78
|
+
`customer_amount` price takes the buyer's `amount`: collect it on your product
|
|
79
|
+
page, and the checkout charges exactly that:
|
|
60
80
|
|
|
61
81
|
```tsx
|
|
62
82
|
import { decimalToAmount } from "@openrails/billing-ui/client"
|
|
@@ -65,72 +85,59 @@ import { decimalToAmount } from "@openrails/billing-ui/client"
|
|
|
65
85
|
// `enteredAmount` is the text from an <input inputMode="decimal">, e.g. "100".
|
|
66
86
|
const amount = decimalToAmount(enteredAmount, currency.decimals)
|
|
67
87
|
const bounds = depositPrice.customer_amount
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
BigInt(amount) < BigInt(bounds.min_amount) ||
|
|
72
|
-
BigInt(amount) > BigInt(bounds.max_amount)
|
|
73
|
-
) {
|
|
74
|
-
throw new Error("Choose an amount within the deposit limits")
|
|
75
|
-
}
|
|
76
|
-
const session = await billing.createCheckoutSession({
|
|
77
|
-
priceId: depositPrice.id,
|
|
78
|
-
amount, // USD 100 becomes "100000000"; no floating-point conversion
|
|
79
|
-
})
|
|
88
|
+
const valid = amount && bounds && BigInt(amount) >= BigInt(bounds.min_amount) && BigInt(amount) <= BigInt(bounds.max_amount)
|
|
89
|
+
|
|
90
|
+
<BuyButton product="api-credits" price="deposit" amount={valid ? amount : undefined} label="Add credits" {...handlers} />
|
|
80
91
|
```
|
|
81
92
|
|
|
82
93
|
OpenRails validates the bounds and whole currency minor units before accepting
|
|
83
94
|
payment. Fixed prices reject `amount`; their pack benefit can be greater than
|
|
84
95
|
the purchase price for a bulk discount. Customer-selected deposits currently
|
|
85
|
-
use NMI or Stripe.
|
|
96
|
+
use NMI or Stripe.
|
|
86
97
|
|
|
87
|
-
A rail's own step is the
|
|
88
|
-
https page, opened in the top window)
|
|
89
|
-
as a QR code)
|
|
90
|
-
Stripe Elements needs the buyer's billing client (`<Checkout>` inside a
|
|
91
|
-
`BillingProvider`): the shared payment page offers token rails only.
|
|
98
|
+
A rail's own step is the order's `payment.next_action`: `redirect_to_url` (an
|
|
99
|
+
https page, opened in the top window), `solana_pay` (a `solana:` link shown
|
|
100
|
+
as a QR code) or `authenticate` (the bank's challenge, run in the page).
|
|
92
101
|
|
|
93
|
-
|
|
94
|
-
behind its adapter's `CheckoutFramePolicy` (only the sites in
|
|
95
|
-
`Config.Checkout.EmbedOrigins` may frame it):
|
|
102
|
+
### An order your server creates
|
|
96
103
|
|
|
97
|
-
|
|
98
|
-
|
|
104
|
+
A server that decides what its customer buys creates the order with the Go
|
|
105
|
+
`Client.CreateOrder` (`POST /v1/app/orders`) and hands its id to the
|
|
106
|
+
customer's browser, which pays it signed in:
|
|
99
107
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
<CheckoutPage appearance={{ variables: brand }} />
|
|
103
|
-
</BillingUiProvider>
|
|
104
|
-
)
|
|
108
|
+
```tsx
|
|
109
|
+
<OrderCheckout orderId={orderId} returnUrl={returnUrl} onSettled={(order) => refetchAccess(order)} />
|
|
105
110
|
```
|
|
106
111
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
`resize {height}`, `complete {status}` and `redirect {url}` (a nested frame
|
|
110
|
-
cannot navigate the top window, so the app does); app to page
|
|
111
|
-
`init {theme}`. The page messages only the app origin recorded on its session.
|
|
112
|
+
`onSettled` follows a complete order, or one processing on an asynchronous
|
|
113
|
+
rail. A subscription line names its subscription once paid.
|
|
112
114
|
|
|
113
115
|
## Account billing
|
|
114
116
|
|
|
117
|
+
One `BillingProvider`, usually at the app's root, gives every component the
|
|
118
|
+
client, the appearance and the words. `@openrails/billing-ui/react` exports the
|
|
119
|
+
same provider for an app that uses only the hooks; it loads no stylesheet.
|
|
120
|
+
|
|
115
121
|
```tsx
|
|
116
|
-
import { createBillingClient } from "@openrails/billing-ui
|
|
117
|
-
import { BillingProvider } from "@openrails/billing-ui/react"
|
|
118
|
-
import { AccountBilling, BillingUiProvider } from "@openrails/billing-ui"
|
|
122
|
+
import { AccountBilling, BillingProvider, createBillingClient } from "@openrails/billing-ui"
|
|
119
123
|
import { de } from "@openrails/billing-ui/locales/de"
|
|
120
124
|
|
|
121
|
-
// auth-ui's authFetch attaches the bearer and retries once after a refresh;
|
|
122
|
-
// `getToken: () => token` works for any other auth.
|
|
123
125
|
const billing = createBillingClient({ baseUrl: "/billing/v1", fetch: auth.authFetch })
|
|
124
126
|
|
|
125
|
-
<
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
127
|
+
<BillingProvider
|
|
128
|
+
client={billing}
|
|
129
|
+
appearance={{ theme: "auto" }}
|
|
130
|
+
messages={de}
|
|
131
|
+
locale="de"
|
|
132
|
+
navigate={navigate}
|
|
133
|
+
onChange={() => queryClient.invalidateQueries({ queryKey: ["billing"] })}
|
|
134
|
+
>
|
|
135
|
+
<AccountBilling
|
|
136
|
+
plansHref="/plans"
|
|
137
|
+
defaultCurrency="USD" // offers "Make default"
|
|
138
|
+
sendSolanaTransaction={(tx) => wallet.signAndSend(tx)} // signs a Solana cancel
|
|
139
|
+
/>
|
|
140
|
+
</BillingProvider>
|
|
134
141
|
```
|
|
135
142
|
|
|
136
143
|
The panels read OpenRails' public configuration (`GET /config`) themselves,
|
|
@@ -143,17 +150,15 @@ for host components.
|
|
|
143
150
|
## Catalog and plan changes
|
|
144
151
|
|
|
145
152
|
The client also reads the public catalog and changes a subscription's plan.
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
routes; `GET /config` is always served.
|
|
153
|
+
The catalog read is a public route, served only when the host mounts
|
|
154
|
+
OpenRails' checkout routes; `GET /config` is always served.
|
|
149
155
|
|
|
150
156
|
| Call | Route |
|
|
151
157
|
| --------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
|
|
152
|
-
| `listProducts()` (
|
|
158
|
+
| `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` |
|
|
153
159
|
| `getConfig()` (capabilities, `currencies`, `payment` with PSPs, `solana.network` and tokens; `client.currencies` is the pinned registry) | `GET /config` |
|
|
154
160
|
| `previewSubscriptionChange(id, { priceId?, quantity? })` | `POST /me/subscriptions/{id}/change/preview` |
|
|
155
161
|
| `changeSubscription(id, { priceId?, quantity?, idempotencyKey, signature? })` | `POST /me/subscriptions/{id}/change` |
|
|
156
|
-
| `listSolanaTokens({ priceId, wallet })` | `GET /solana/tokens` |
|
|
157
162
|
|
|
158
163
|
```ts
|
|
159
164
|
const preview = await billing.previewSubscriptionChange(sub.id, { quantity: 5 }) // amount_due_now, effective
|
|
@@ -188,18 +193,15 @@ browser flow from each PSP's `flow` and public `config`
|
|
|
188
193
|
(Collect.js, native card inputs, Stripe Elements, redirect, wallet). Hosts never branch on a
|
|
189
194
|
provider:
|
|
190
195
|
|
|
191
|
-
-
|
|
192
|
-
sale (Solana when it is configured, never CCBill),
|
|
193
|
-
each with
|
|
194
|
-
cards on them, newest first
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
carrying an `operation` run 3-D Secure in the page. A `failed` result stays
|
|
201
|
-
on the panel with `failure.message` next to its `field`, so the buyer can
|
|
202
|
-
pick another card; the host gives each new attempt a new idempotency key.
|
|
196
|
+
- An order's `payment_options` are exactly the armed PSPs whose rail can make
|
|
197
|
+
this sale (Solana for one-time lines when it is configured, never CCBill),
|
|
198
|
+
each with what it `accepts`. `OrderCheckout` renders one panel: the
|
|
199
|
+
customer's saved cards on them, newest first, an inline new card and one Pay
|
|
200
|
+
button. No provider chooser with one rail.
|
|
201
|
+
- A new card is charged and saved in one pay; an `authenticate` next action
|
|
202
|
+
runs 3-D Secure in the page, then `confirm`. A decline stays on the panel
|
|
203
|
+
with its message, so the buyer can pick another card under a new
|
|
204
|
+
idempotency key.
|
|
203
205
|
- Only PSPs with `checkout !== false` (`checkoutPsps`) take new cards; others
|
|
204
206
|
stay listed so existing cards and subscriptions keep working.
|
|
205
207
|
- `<SavePaymentMethod psp={psp} onSaved={(id) => ...} />` saves a card in the
|
|
@@ -208,10 +210,8 @@ provider:
|
|
|
208
210
|
`returnURL(setupId)`; confirm with `client.confirmCardSetup(id)`.
|
|
209
211
|
- `authenticatePayment(client, operationId, psp)` completes a pending
|
|
210
212
|
payment's 3-D Secure challenge when `canAuthenticatePayment(psp)`.
|
|
211
|
-
- `defaultCountry` (
|
|
212
|
-
the billing country, else the browser locale's region.
|
|
213
|
-
- `CheckoutModal gate={<SignIn />}` renders host content (e.g. sign-in) in
|
|
214
|
-
the modal instead of the checkout until cleared.
|
|
213
|
+
- `defaultCountry` (SavePaymentMethod, AccountBilling)
|
|
214
|
+
preselects the billing country, else the browser locale's region.
|
|
215
215
|
|
|
216
216
|
- Panels: `SubscriptionsPanel` (cancel, resume, change card),
|
|
217
217
|
`PaymentMethodsPanel`, `PaymentHistory`, `BillingStatusBadge`,
|
|
@@ -220,8 +220,8 @@ provider:
|
|
|
220
220
|
subscription row; `useBillingRefresh()` refetches after host-side changes.
|
|
221
221
|
- Hooks: `useSubscriptions` (`cancel`, `resume`, `setPaymentMethod`,
|
|
222
222
|
`changeSubscription`, per-row `pending`, `nextCursor`), `usePaymentMethods` (`add`,
|
|
223
|
-
`remove`, `setDefault`), `usePayments` (cursor pages), `useProducts` (
|
|
224
|
-
catalog).
|
|
223
|
+
`remove`, `setDefault`), `usePayments` (cursor pages), `useProducts` (a
|
|
224
|
+
catalog lookup by product key).
|
|
225
225
|
Actions resolve to `null` or a `BillingError`; they never throw.
|
|
226
226
|
`changeSubscription` resolves to the `SubscriptionChange` instead of `null`. Cancel, resume
|
|
227
227
|
and the card change answer the subscription, which replaces the row.
|
|
@@ -1,12 +1,10 @@
|
|
|
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
5
|
/**
|
|
8
6
|
* Offers to make a card the default for this currency: it pays the
|
|
9
|
-
* currency's
|
|
7
|
+
* currency's usage orders and every subscription in it without its own card.
|
|
10
8
|
*/
|
|
11
9
|
defaultCurrency?: string;
|
|
12
10
|
/** Billing country to preselect when adding a card. */
|
|
@@ -19,4 +17,4 @@ export interface AccountBillingProps {
|
|
|
19
17
|
className?: string;
|
|
20
18
|
}
|
|
21
19
|
/** Subscriptions, payment methods and history in one column. */
|
|
22
|
-
export declare function AccountBilling({
|
|
20
|
+
export declare function AccountBilling({ defaultCurrency, defaultCountry, sendSolanaTransaction, plansHref, renderSubscriptionFooter, historyPageSize, appearance, className, }: AccountBillingProps): import("react").JSX.Element;
|
|
@@ -1,11 +1,9 @@
|
|
|
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
4
|
/**
|
|
7
5
|
* Offers to make a card the default for this currency: it pays the
|
|
8
|
-
* currency's
|
|
6
|
+
* currency's usage orders and every subscription in it without its own card.
|
|
9
7
|
*/
|
|
10
8
|
defaultCurrency?: string;
|
|
11
9
|
/** Billing country to preselect when adding a card. */
|
|
@@ -17,4 +15,4 @@ export interface PaymentMethodsPanelProps {
|
|
|
17
15
|
* The customer's saved cards. "Add card" offers each PSP of the merchant's
|
|
18
16
|
* payment setup (`GET /config`) that saves a card in the page.
|
|
19
17
|
*/
|
|
20
|
-
export declare function PaymentMethodsPanel({
|
|
18
|
+
export declare function PaymentMethodsPanel({ defaultCurrency, defaultCountry, appearance, className, }: PaymentMethodsPanelProps): React.JSX.Element;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { CheckoutAppearance } from './appearance';
|
|
2
|
+
import { Order } from './client/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 order charges exactly this.
|
|
14
|
+
*/
|
|
15
|
+
amount?: string;
|
|
16
|
+
/** Where a redirect PSP (Stripe's hosted page) returns the buyer: one of the app's return origins. */
|
|
17
|
+
returnUrl?: string;
|
|
18
|
+
/**
|
|
19
|
+
* The order is paid. A hint: the host's own gate decides access, so send
|
|
20
|
+
* the buyer to the content it guards.
|
|
21
|
+
*/
|
|
22
|
+
onPaid: (order: Order) => void;
|
|
23
|
+
/**
|
|
24
|
+
* Buying needs a signed-in customer: called instead of the checkout when
|
|
25
|
+
* `signedIn` is false or OpenRails answers 401.
|
|
26
|
+
*/
|
|
27
|
+
onSignInRequired: () => void;
|
|
28
|
+
/** false when the host knows the visitor is signed out. */
|
|
29
|
+
signedIn?: boolean;
|
|
30
|
+
appearance?: CheckoutAppearance;
|
|
31
|
+
className?: string;
|
|
32
|
+
}
|
|
33
|
+
export declare function BuyButton(props: BuyButtonProps): React.JSX.Element;
|
|
34
|
+
export type PurchaseProps = Omit<BuyButtonProps, "product" | "price"> & {
|
|
35
|
+
/** The price to buy; the button waits while it is unknown. */
|
|
36
|
+
priceId?: string;
|
|
37
|
+
label: string;
|
|
38
|
+
};
|
|
39
|
+
/** One price's button and its order. Requires `BillingProvider`. */
|
|
40
|
+
export declare function Purchase({ priceId, label, amount, returnUrl, onPaid, onSignInRequired, signedIn, appearance, className, }: PurchaseProps): 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;
|