@lockerverse/react 0.2.156 → 0.2.158
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/README.md +14 -57
- package/dist/auction-image.js +1 -1
- package/dist/auction-image.js.map +1 -1
- package/dist/auction-item.d.ts.map +1 -1
- package/dist/auction-item.js +1 -1
- package/dist/auction-item.js.map +1 -1
- package/dist/auctions.d.ts.map +1 -1
- package/dist/auctions.js +1 -1
- package/dist/auctions.js.map +1 -1
- package/dist/lockerverse-auction-item-card.d.ts.map +1 -1
- package/dist/lockerverse-auction.js +1 -1
- package/dist/lockerverse-auction.js.map +1 -1
- package/guides/auctions.md +549 -0
- package/guides/events.md +100 -0
- package/guides/integration.md +259 -0
- package/guides/payment-widgets.md +704 -0
- package/guides/signup.md +94 -0
- package/package.json +7 -5
|
@@ -0,0 +1,704 @@
|
|
|
1
|
+
# Lockerverse payment-widgets guide
|
|
2
|
+
|
|
3
|
+
Package: @lockerverse/react@0.2.158
|
|
4
|
+
|
|
5
|
+
Generated from this release's maintained documentation. Do not edit this copy.
|
|
6
|
+
|
|
7
|
+
Read [the shared integration guide](integration.md) first.
|
|
8
|
+
|
|
9
|
+
## Payment widget host integration
|
|
10
|
+
|
|
11
|
+
## Keep the requested integration mode
|
|
12
|
+
|
|
13
|
+
Read the shared integration guide first. If the resource prompt specifies
|
|
14
|
+
`integrationMode: "hosted-checkout"`, keep the hosted checkout modal described
|
|
15
|
+
below. Do not silently replace it with native checkout. If an existing site
|
|
16
|
+
already uses native SDK checkout, keep that mode unless a change was requested.
|
|
17
|
+
For a new native integration, use the catalog hook and payment component
|
|
18
|
+
contract later in this guide. Do not add a duplicate checkout page or send the
|
|
19
|
+
visitor away from the existing page.
|
|
20
|
+
|
|
21
|
+
Use the saved `communitySlug`, `widgetSlug`, `apiBaseUrl`, and `environment` to
|
|
22
|
+
load live catalog data with `useLockerverseWidget` from
|
|
23
|
+
`@lockerverse/react/widget`, or `createLockerverseCheckoutClient` from
|
|
24
|
+
`@lockerverse/sdk/checkout` and its `load()` method. Do not infer active products
|
|
25
|
+
or current prices from an old prompt snapshot. Preserve inactive/unavailable
|
|
26
|
+
states and do not enable payment when the live resource cannot be purchased.
|
|
27
|
+
Use the saved `widgetUrl` for hosted navigation; do not build another
|
|
28
|
+
payment API. Hosted checkout does not need native Stripe peers in the host.
|
|
29
|
+
|
|
30
|
+
## Match existing controls
|
|
31
|
+
|
|
32
|
+
Follow any explicit button or section target in the prompt. Otherwise inspect
|
|
33
|
+
nearby product name, price, billing period, and section heading. Reuse a suitable
|
|
34
|
+
button instead of adding another. If several targets remain equally plausible,
|
|
35
|
+
ask one short question before changing the page. Keep the page's product cards,
|
|
36
|
+
cart, quantities, selected tier, and entered amounts. Names, prices, limits,
|
|
37
|
+
recurring cadence, inventory, and shared capacity come from the live catalog.
|
|
38
|
+
|
|
39
|
+
For separate product buttons, select the matching product. For a cart with one
|
|
40
|
+
pay button, combine only one-time products. Monthly and annual products require
|
|
41
|
+
an exclusive single-product flow; never combine them with each other or a
|
|
42
|
+
one-time cart. Disable checkout when no product is selected or a value violates
|
|
43
|
+
live limits. Local totals are feedback only; Lockerverse decides the final
|
|
44
|
+
payable amount.
|
|
45
|
+
|
|
46
|
+
## Hosted checkout URLs
|
|
47
|
+
|
|
48
|
+
Given the configured public widget URL
|
|
49
|
+
`https://host.example/community/widgets/v2/widget/donate`, a product's direct
|
|
50
|
+
URL is the same resource with `/options/{encodedProductSlug}/donate` in place
|
|
51
|
+
of its final `/donate`. Use the live product's `slug` and `encodeURIComponent`.
|
|
52
|
+
Use `URL` and `URLSearchParams`; preserve the supplied widget origin and route.
|
|
53
|
+
|
|
54
|
+
- For a fixed-price direct product, set `quantity` only when the page collects
|
|
55
|
+
a quantity. Do not set `amount` or `paymentOption`.
|
|
56
|
+
- For a custom-price direct product, set `amount` to the selected dollar amount.
|
|
57
|
+
- For a combined one-time cart, start from the widget base URL. Set
|
|
58
|
+
`quantity.{productSlug}` to each selected fixed product's positive integer
|
|
59
|
+
quantity and `amount.{productSlug}` to each selected custom product's positive
|
|
60
|
+
dollar amount with at most two decimals. Omit zero or unselected products.
|
|
61
|
+
- Do not pass one aggregate `amount`; the server computes the total.
|
|
62
|
+
- For editable custom-field prefill, use `custom.<fieldId>` with real configured
|
|
63
|
+
field IDs. Text uses one string, a checkbox uses `true` or `false`, a single
|
|
64
|
+
choice uses its option ID, and multiple choices repeat the parameter. Do not
|
|
65
|
+
use `meta.*` for form fields. Keep personal and sensitive data out of URLs.
|
|
66
|
+
|
|
67
|
+
## Hosted modal
|
|
68
|
+
|
|
69
|
+
Use the existing site-owned Radix/shadcn Dialog or equivalent accessible modal.
|
|
70
|
+
Provide an accessible title and description, a visible close button, an iframe
|
|
71
|
+
title, and `allow="payment *"` on the iframe. Do not add resize-message logic.
|
|
72
|
+
Make DialogContent a flex column with `gap-0`, `overflow-hidden`, and `p-0`.
|
|
73
|
+
Use a compact header with room for the close control. Its hit area must be at
|
|
74
|
+
least 40 by 40 CSS pixels.
|
|
75
|
+
|
|
76
|
+
On mobile, use a small gutter: `w-[calc(100vw-1rem)]`,
|
|
77
|
+
`h-[calc(100dvh-1rem)]`, and `max-h-[calc(100dvh-1rem)]`. On desktop, use
|
|
78
|
+
`sm:w-full`, about 720px height, and a maximum width near 640px. The iframe
|
|
79
|
+
wrapper uses `min-h-0 flex-1 overflow-hidden`; the iframe uses
|
|
80
|
+
`h-full w-full border-0`. Only the iframe scrolls. Do not apply viewport-height
|
|
81
|
+
sizing to its wrapper or leave a blank strip below the header. Closing and
|
|
82
|
+
reopening must preserve the host cart. Closing is not payment completion.
|
|
83
|
+
Do not infer paid access from an iframe URL or close event.
|
|
84
|
+
|
|
85
|
+
## Native SDK checkout
|
|
86
|
+
|
|
87
|
+
Keep existing product presentation. Use `useLockerverseWidget` for the complete
|
|
88
|
+
live catalog, and store only customer selection in host state. Load
|
|
89
|
+
`LockerversePayment` from `@lockerverse/react/payment` and its CSS when checkout
|
|
90
|
+
is near or visible; pass the full `selection` and `widget` hook result. Use
|
|
91
|
+
`LockerverseDonateLauncher` only when a product picker is needed; a button that
|
|
92
|
+
already selects a product can open that product's checkout directly. Do not
|
|
93
|
+
add an iframe around native SDK checkout or another Stripe Elements wrapper.
|
|
94
|
+
|
|
95
|
+
Use supported `initialCustomFieldValues` for editable prefill. For explicitly
|
|
96
|
+
requested custom checkout, reuse payment primitives with the Core SDK; the
|
|
97
|
+
host then owns quote, confirmation, validation, and recovery duties. Retain
|
|
98
|
+
recovery in an owner that stays mounted when a custom dialog closes. Read the
|
|
99
|
+
payment outcome and recovery contracts in this guide before implementing
|
|
100
|
+
success or retries.
|
|
101
|
+
|
|
102
|
+
## Public attendee list and checks
|
|
103
|
+
|
|
104
|
+
If the prompt includes an event ID and requests a public attendee list, follow
|
|
105
|
+
[Public attendee lists](events.md#public-attendee-lists). Show names only;
|
|
106
|
+
do not show party size, total counts, or private registration data.
|
|
107
|
+
|
|
108
|
+
Build and inspect desktop and 390 by 844 layouts. Verify the matching button,
|
|
109
|
+
selection, direct or combined URL parameters, modal close target, no header gap,
|
|
110
|
+
and no horizontal overflow. Open checkout to check saved settings, but do not
|
|
111
|
+
submit payment. For native SDK checkout, inspect callback and recovery handling
|
|
112
|
+
without creating live transactions.
|
|
113
|
+
|
|
114
|
+
## Custom Product UI
|
|
115
|
+
|
|
116
|
+
Use `useLockerverseWidget` to build custom Product presentation from the live
|
|
117
|
+
Lockerverse catalog. This entry point does not import the payment UI or Stripe.
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
import {
|
|
121
|
+
LockerversePayment,
|
|
122
|
+
preloadLockerversePayment,
|
|
123
|
+
type LockerversePaymentSelectionItem,
|
|
124
|
+
} from "@lockerverse/react/payment";
|
|
125
|
+
import "@lockerverse/react/payment.css";
|
|
126
|
+
import { useLockerverseWidget } from "@lockerverse/react/widget";
|
|
127
|
+
import { useState } from "react";
|
|
128
|
+
|
|
129
|
+
const widgetOptions = {
|
|
130
|
+
communitySlug: "auburn",
|
|
131
|
+
widgetSlug: "tailgate-party",
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export function CustomProductCheckout() {
|
|
135
|
+
const widget = useLockerverseWidget(widgetOptions);
|
|
136
|
+
const [selection, setSelection] =
|
|
137
|
+
useState<LockerversePaymentSelectionItem | null>(null);
|
|
138
|
+
|
|
139
|
+
if (widget.loading) {
|
|
140
|
+
return <p>Loading checkout...</p>;
|
|
141
|
+
}
|
|
142
|
+
if (widget.error) {
|
|
143
|
+
return <button onClick={widget.refresh}>Try again</button>;
|
|
144
|
+
}
|
|
145
|
+
if (!widget.catalog) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
if (selection) {
|
|
149
|
+
return (
|
|
150
|
+
<LockerversePayment
|
|
151
|
+
selection={[selection]}
|
|
152
|
+
widget={widget}
|
|
153
|
+
/>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return widget.catalog.products
|
|
158
|
+
.filter((product) => product.pricingMode === "fixed")
|
|
159
|
+
.map((product) => (
|
|
160
|
+
<button
|
|
161
|
+
key={product.id}
|
|
162
|
+
onFocus={() => {
|
|
163
|
+
void preloadLockerversePayment(widget.catalog);
|
|
164
|
+
}}
|
|
165
|
+
onClick={() =>
|
|
166
|
+
setSelection(
|
|
167
|
+
product.paymentOption === "one-time"
|
|
168
|
+
? { productSlug: product.slug, quantity: 1 }
|
|
169
|
+
: { productSlug: product.slug }
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
onPointerEnter={() => {
|
|
173
|
+
void preloadLockerversePayment(widget.catalog);
|
|
174
|
+
}}
|
|
175
|
+
>
|
|
176
|
+
{product.name}
|
|
177
|
+
</button>
|
|
178
|
+
));
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
The hook returns immutable `catalog` data together with `loading`, `error`, and
|
|
183
|
+
`refresh`. The host owns presentation and selection state. Pass the
|
|
184
|
+
selected Product slugs to `LockerversePayment`; Lockerverse remains
|
|
185
|
+
authoritative for availability, pricing, and payment.
|
|
186
|
+
|
|
187
|
+
Pass the hook result to `LockerversePayment` with `widget={widget}`. The result
|
|
188
|
+
contains the loaded catalog and its client, so the component requests
|
|
189
|
+
only the authoritative quote without repeated configuration. The host can show
|
|
190
|
+
a local catalog total before checkout. The component uses the same local
|
|
191
|
+
estimate to mount the payment form and Stripe immediately, then replaces it
|
|
192
|
+
with the authoritative quote in the background. Payment confirmation still
|
|
193
|
+
requires the current authoritative quote.
|
|
194
|
+
|
|
195
|
+
This small example shows fixed-price Products. For a custom-price Product, ask
|
|
196
|
+
for the amount in the host UI and pass `amountCents` with `productSlug`.
|
|
197
|
+
|
|
198
|
+
For a monthly or annual Product, omit quantity. The component shows the
|
|
199
|
+
cadence and configures Stripe Elements for a subscription:
|
|
200
|
+
|
|
201
|
+
```tsx
|
|
202
|
+
<LockerversePayment
|
|
203
|
+
selection={[{ productSlug: "monthly-supporter" }]}
|
|
204
|
+
widget={widget}
|
|
205
|
+
/>
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Recurring checkout accepts one fixed Product. It does not accept quantities,
|
|
209
|
+
custom amounts, or multiple Products.
|
|
210
|
+
|
|
211
|
+
### Live product availability
|
|
212
|
+
|
|
213
|
+
Use the live `widget.catalog` for names, prices, and limits. Find the requested
|
|
214
|
+
product by its `listingId` or `slug`; do not keep a separate static price list.
|
|
215
|
+
Amounts are integer cents. Format them with `catalog.payment.currency`, not a
|
|
216
|
+
currency field on the product.
|
|
217
|
+
|
|
218
|
+
Before enabling a new purchase, check the selected product's `minimumQuantity`,
|
|
219
|
+
`maximumQuantity`, and `inventoryQuantity`; nullable limits mean no limit.
|
|
220
|
+
For custom amounts, use `getLockerverseMinimumCustomAmountCents(product)` from
|
|
221
|
+
`@lockerverse/sdk/checkout` to include the SDK minimum and the product minimum.
|
|
222
|
+
Also check:
|
|
223
|
+
|
|
224
|
+
- The matching `catalog.availability.listings` entry: `status`,
|
|
225
|
+
`maxPurchasableQuantity`, and `remainingQuantity`.
|
|
226
|
+
- Each `catalog.availability.sharedLimits` entry whose `listingIds` includes
|
|
227
|
+
the selected product's listing: `status` and `remainingQuantity`. Add the
|
|
228
|
+
quantities of all selected listings in that shared group before checking its
|
|
229
|
+
remaining quantity.
|
|
230
|
+
|
|
231
|
+
A missing listing availability entry means no listing-specific limit was
|
|
232
|
+
reported. Other product and shared limits still apply. These checks help the
|
|
233
|
+
host show an unavailable state; the authoritative quote decides whether the
|
|
234
|
+
selection can be purchased. Show loading, retry, and empty states. Keep an
|
|
235
|
+
unresolved payment available for recovery even if a new purchase is unavailable.
|
|
236
|
+
|
|
237
|
+
If an existing button already selects one product, open its checkout directly.
|
|
238
|
+
Use the donate launcher when the visitor needs a product picker. For a custom
|
|
239
|
+
dialog, use an accessible title, close button, focus handling, and mobile scroll
|
|
240
|
+
area. Keep recovery state in an owner that remains mounted when the dialog
|
|
241
|
+
closes. Closing a dialog does not complete or cancel a payment.
|
|
242
|
+
|
|
243
|
+
## Donate launcher
|
|
244
|
+
|
|
245
|
+
`LockerverseDonateLauncher` turns an existing payment widget into a native
|
|
246
|
+
React dialog. The launcher selects its controls from each Product's existing
|
|
247
|
+
capabilities. Visitors can set quantities for fixed one-time Products and add
|
|
248
|
+
an inline amount for custom one-time Products. The launcher combines these
|
|
249
|
+
one-time choices in one checkout. Monthly and annual Products are exclusive:
|
|
250
|
+
the visitor selects one tier before the same `LockerversePayment` form opens in
|
|
251
|
+
subscription mode.
|
|
252
|
+
|
|
253
|
+
```tsx
|
|
254
|
+
import { LockerverseDonateLauncher } from "@lockerverse/react/donate-launcher";
|
|
255
|
+
import { useLockerverseWidget } from "@lockerverse/react/widget";
|
|
256
|
+
import "@lockerverse/react/payment.css";
|
|
257
|
+
|
|
258
|
+
export function Donate() {
|
|
259
|
+
const widget = useLockerverseWidget({
|
|
260
|
+
communitySlug: "auburn",
|
|
261
|
+
widgetSlug: "general-donations",
|
|
262
|
+
});
|
|
263
|
+
return (
|
|
264
|
+
<LockerverseDonateLauncher
|
|
265
|
+
buttonLabel="Donate"
|
|
266
|
+
position="bottom-right"
|
|
267
|
+
widget={widget}
|
|
268
|
+
/>
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Use `position="inline"` to place the launcher in the normal page layout. Use
|
|
274
|
+
`bottom-left` or `bottom-right` for a fixed launcher. Payment callbacks,
|
|
275
|
+
branding, themes, metadata, and recovery properties are the same as
|
|
276
|
+
`LockerversePayment`. API and transport options belong to
|
|
277
|
+
`useLockerverseWidget`.
|
|
278
|
+
|
|
279
|
+
The launcher starts loading Stripe when the visitor opens it. Catalog-only
|
|
280
|
+
pages do not load Stripe.
|
|
281
|
+
|
|
282
|
+
## Catalog hook
|
|
283
|
+
|
|
284
|
+
```tsx
|
|
285
|
+
import { useLockerverseWidget } from "@lockerverse/react/widget";
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
## Input
|
|
289
|
+
|
|
290
|
+
```ts
|
|
291
|
+
type CreateLockerverseWidgetOptions = {
|
|
292
|
+
communitySlug: string;
|
|
293
|
+
widgetSlug: string;
|
|
294
|
+
requestTimeoutMs?: number;
|
|
295
|
+
sentryDsn?: string | null;
|
|
296
|
+
fetch?: typeof globalThis.fetch;
|
|
297
|
+
onError?: (error: LockerverseSdkError) => void;
|
|
298
|
+
} & (
|
|
299
|
+
| { apiBaseUrl: string; environment: "development" | "production" }
|
|
300
|
+
| { apiBaseUrl?: undefined; environment?: "production" }
|
|
301
|
+
);
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
## Result
|
|
305
|
+
|
|
306
|
+
```ts
|
|
307
|
+
type UseLockerverseWidgetResult = {
|
|
308
|
+
catalog: LockerverseCatalog | null;
|
|
309
|
+
client: LockerverseCheckoutClient;
|
|
310
|
+
error: Error | null;
|
|
311
|
+
loading: boolean;
|
|
312
|
+
refresh: () => void;
|
|
313
|
+
};
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
The loaded catalog includes an optional event:
|
|
317
|
+
|
|
318
|
+
```ts
|
|
319
|
+
type LockerverseEvent = {
|
|
320
|
+
description: string | null;
|
|
321
|
+
endAt: string;
|
|
322
|
+
id: string;
|
|
323
|
+
location: string | null;
|
|
324
|
+
name: string;
|
|
325
|
+
startAt: string;
|
|
326
|
+
status: "draft" | "active" | "finished" | "cancelled";
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
type LockerverseCatalog = {
|
|
330
|
+
event: LockerverseEvent | null;
|
|
331
|
+
products: LockerverseProduct[];
|
|
332
|
+
// Community, payment, pricing policy, and widget configuration are also present.
|
|
333
|
+
};
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
`startAt` and `endAt` are event timestamps. Use `status` to determine the
|
|
337
|
+
event lifecycle state.
|
|
338
|
+
|
|
339
|
+
## Product interface
|
|
340
|
+
|
|
341
|
+
```ts
|
|
342
|
+
type LockerverseProduct = {
|
|
343
|
+
id: string;
|
|
344
|
+
listingId: string;
|
|
345
|
+
slug: string;
|
|
346
|
+
name: string;
|
|
347
|
+
description: string | null;
|
|
348
|
+
fulfillmentType: "none" | "event_admission";
|
|
349
|
+
pricingMode: "fixed" | "custom";
|
|
350
|
+
paymentOption: "one-time" | "monthly" | "annually";
|
|
351
|
+
amountCents: number | null;
|
|
352
|
+
minimumAmountCents: number | null;
|
|
353
|
+
minimumQuantity: number | null;
|
|
354
|
+
maximumQuantity: number | null;
|
|
355
|
+
inventoryQuantity: number | null;
|
|
356
|
+
acceptsDiscounts: boolean;
|
|
357
|
+
};
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
An `event_admission` Product is fulfilled by the catalog event.
|
|
361
|
+
|
|
362
|
+
The hook treats all options as initialization values and keeps one client for
|
|
363
|
+
its mounted lifetime. `refresh()` requests that same widget again. To use a
|
|
364
|
+
different widget, remount the component that owns the hook with a new React
|
|
365
|
+
`key`. Selection and other view state can change without a remount. This entry
|
|
366
|
+
does not import Stripe or payment UI.
|
|
367
|
+
|
|
368
|
+
## Reuse the loaded catalog
|
|
369
|
+
|
|
370
|
+
```tsx
|
|
371
|
+
const widget = useLockerverseWidget(options);
|
|
372
|
+
|
|
373
|
+
<LockerversePayment
|
|
374
|
+
selection={selection}
|
|
375
|
+
widget={widget}
|
|
376
|
+
/>
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
The hook result carries its catalog and client. The host can calculate
|
|
380
|
+
selection totals from the catalog while the payment component uses that client to request the
|
|
381
|
+
current, authoritative quote. The payment form mounts immediately with a local
|
|
382
|
+
estimate. The authoritative quote replaces that estimate in the background
|
|
383
|
+
before payment confirmation.
|
|
384
|
+
|
|
385
|
+
## Payment component
|
|
386
|
+
|
|
387
|
+
```tsx
|
|
388
|
+
import { LockerversePayment } from "@lockerverse/react/payment";
|
|
389
|
+
import "@lockerverse/react/payment.css";
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
## Required properties
|
|
393
|
+
|
|
394
|
+
| Property | Type | Description |
|
|
395
|
+
| --- | --- | --- |
|
|
396
|
+
| `widget` | `UseLockerverseWidgetResult` | Result from `useLockerverseWidget` |
|
|
397
|
+
| `selection` | `LockerversePaymentSelectionItem[]` | Products to quote and pay |
|
|
398
|
+
|
|
399
|
+
## Payment properties
|
|
400
|
+
|
|
401
|
+
```ts
|
|
402
|
+
type LockerversePaymentProps = {
|
|
403
|
+
widget: UseLockerverseWidgetResult;
|
|
404
|
+
selection: LockerversePaymentSelectionItem[];
|
|
405
|
+
branding?: {
|
|
406
|
+
accentColor?: string;
|
|
407
|
+
imageUrl?: string | null;
|
|
408
|
+
name: string;
|
|
409
|
+
};
|
|
410
|
+
metadata?: Record<string, string>;
|
|
411
|
+
initialCustomFieldValues?: Record<string, boolean | string | string[]>;
|
|
412
|
+
style?: LockerverseStyle;
|
|
413
|
+
theme?: "dark" | "light";
|
|
414
|
+
resumePayment?: {
|
|
415
|
+
connectedAccountId: string | null;
|
|
416
|
+
paymentReference: string;
|
|
417
|
+
} | null;
|
|
418
|
+
onPaymentComplete?: (payment: LockerversePaymentCompletion) => void;
|
|
419
|
+
onPaymentRecoveryChange?: (recovery: LockerversePaymentRecovery | null) => void;
|
|
420
|
+
onPaymentUncertain?: (payment: LockerversePaymentUncertain) => void;
|
|
421
|
+
};
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
The component also accepts `<section>` properties and `className`. See [Theming](integration.md#styling) for the shared typed CSS tokens.
|
|
425
|
+
|
|
426
|
+
## Completion
|
|
427
|
+
|
|
428
|
+
```tsx
|
|
429
|
+
import type { LockerversePaymentCompletion } from "@lockerverse/react/payment";
|
|
430
|
+
|
|
431
|
+
const widget = useLockerverseWidget({
|
|
432
|
+
communitySlug: "your-community",
|
|
433
|
+
widgetSlug: "your-widget",
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
const handlePaymentComplete = (payment: LockerversePaymentCompletion) => {
|
|
437
|
+
const admissionItems = payment.lineItems?.filter(
|
|
438
|
+
(item) => item.fulfillmentType === "event_admission"
|
|
439
|
+
);
|
|
440
|
+
console.info("Payment complete; admission item count", admissionItems?.length ?? 0);
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
<LockerversePayment
|
|
444
|
+
onPaymentComplete={handlePaymentComplete}
|
|
445
|
+
selection={selection}
|
|
446
|
+
widget={widget}
|
|
447
|
+
/>
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
Use `onPaymentComplete` as the confirmed success signal. A direct completion
|
|
451
|
+
includes the normalized customer `email` and authoritative `lineItems`. A
|
|
452
|
+
completion recovered after reload has `null` for both fields.
|
|
453
|
+
|
|
454
|
+
## Recovery
|
|
455
|
+
|
|
456
|
+
Store `onPaymentRecoveryChange` and pass it back through `resumePayment` after reload. Keep `onPaymentUncertain` unresolved.
|
|
457
|
+
|
|
458
|
+
## Prefill custom fields
|
|
459
|
+
|
|
460
|
+
Pass configured field IDs to `initialCustomFieldValues`:
|
|
461
|
+
|
|
462
|
+
```tsx
|
|
463
|
+
<LockerversePayment
|
|
464
|
+
widget={widget}
|
|
465
|
+
selection={selection}
|
|
466
|
+
initialCustomFieldValues={{
|
|
467
|
+
field_muw2q7vx_sf7r94: "Autographed basketball",
|
|
468
|
+
}}
|
|
469
|
+
/>
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
The form reads these values when a checkout form starts. Buyers can edit them;
|
|
473
|
+
parent re-renders and price refreshes do not overwrite their edits. To apply a
|
|
474
|
+
new set of initial values, start a new form with a new React `key`. This also
|
|
475
|
+
resets the other form inputs.
|
|
476
|
+
|
|
477
|
+
Text and textarea fields take strings. A checkbox takes a boolean. Radio and
|
|
478
|
+
select fields take an option ID. Checkboxes and multi-select fields take arrays
|
|
479
|
+
of option IDs. Unknown fields, wrong value types, and unknown options are
|
|
480
|
+
ignored. Only fields for the selected products are used. Required-field
|
|
481
|
+
validation and server validation still apply.
|
|
482
|
+
|
|
483
|
+
`LockerverseDonateLauncher` accepts the same property. For live control over
|
|
484
|
+
individual values and a custom submission flow, use the
|
|
485
|
+
[payment primitives](payment-widgets.md) with the Core SDK.
|
|
486
|
+
|
|
487
|
+
## Payment outcomes
|
|
488
|
+
|
|
489
|
+
`onPaymentComplete` runs only after Lockerverse reports `success`. A successful payment is terminal, even if the host callback throws.
|
|
490
|
+
|
|
491
|
+
A direct completion includes the normalized customer `email` and authoritative
|
|
492
|
+
`lineItems`. A completion recovered after reload has `null` for both fields.
|
|
493
|
+
|
|
494
|
+
If the authoritative total, discount, currency, line items, or connected Stripe account changes, the component displays the new quote before creating a Stripe token. The customer must explicitly submit again.
|
|
495
|
+
|
|
496
|
+
An authoritative `failed` result is retryable and the next submit receives a new payment reference. `pending`, unresolved `action_required`, and failed same-reference recovery stay locked so the customer cannot accidentally create a second payment. The optional `onPaymentUncertain` callback receives the retained `paymentReference` and one of these reasons:
|
|
497
|
+
|
|
498
|
+
- `pending`
|
|
499
|
+
- `action_required`
|
|
500
|
+
- `recovery_failed`
|
|
501
|
+
|
|
502
|
+
To abandon a locked attempt and intentionally start a new checkout, the host must remount `LockerversePayment` with a new React `key` after resolving the payment status through its own workflow.
|
|
503
|
+
|
|
504
|
+
### Payment recovery
|
|
505
|
+
|
|
506
|
+
Persist `onPaymentRecoveryChange` synchronously before navigation and pass the
|
|
507
|
+
saved value back through `resumePayment` after a refresh. Do not defer the save
|
|
508
|
+
to an effect. A recovery record has only `paymentReference: string` and
|
|
509
|
+
`connectedAccountId: string | null`; it contains no Stripe secret. Scope its
|
|
510
|
+
storage key to the environment, community, widget, and selection.
|
|
511
|
+
|
|
512
|
+
This hook reads storage after mount, validates the saved record, and keeps an
|
|
513
|
+
in-memory record if storage is unavailable:
|
|
514
|
+
|
|
515
|
+
```tsx
|
|
516
|
+
import { useEffect, useState } from "react";
|
|
517
|
+
import type { LockerversePaymentRecovery } from "@lockerverse/react/payment";
|
|
518
|
+
|
|
519
|
+
function usePaymentRecovery(recoveryKey: string) {
|
|
520
|
+
const [recovery, setRecovery] = useState<LockerversePaymentRecovery | null>(null);
|
|
521
|
+
const [loaded, setLoaded] = useState(false);
|
|
522
|
+
|
|
523
|
+
useEffect(() => {
|
|
524
|
+
try {
|
|
525
|
+
const raw = sessionStorage.getItem(recoveryKey);
|
|
526
|
+
const saved: unknown = raw ? JSON.parse(raw) : null;
|
|
527
|
+
if (
|
|
528
|
+
saved !== null && typeof saved === "object" &&
|
|
529
|
+
"paymentReference" in saved && typeof saved.paymentReference === "string" &&
|
|
530
|
+
"connectedAccountId" in saved &&
|
|
531
|
+
(saved.connectedAccountId === null || typeof saved.connectedAccountId === "string")
|
|
532
|
+
) {
|
|
533
|
+
setRecovery({
|
|
534
|
+
paymentReference: saved.paymentReference,
|
|
535
|
+
connectedAccountId: saved.connectedAccountId,
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
} catch {
|
|
539
|
+
// Invalid or unavailable storage must not break checkout.
|
|
540
|
+
}
|
|
541
|
+
setLoaded(true);
|
|
542
|
+
}, [recoveryKey]);
|
|
543
|
+
|
|
544
|
+
function saveRecovery(next: LockerversePaymentRecovery | null) {
|
|
545
|
+
setRecovery(next);
|
|
546
|
+
try {
|
|
547
|
+
if (next) sessionStorage.setItem(recoveryKey, JSON.stringify(next));
|
|
548
|
+
else sessionStorage.removeItem(recoveryKey);
|
|
549
|
+
} catch {
|
|
550
|
+
// Keep the in-memory record if storage is unavailable.
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return { loaded, recovery, saveRecovery };
|
|
555
|
+
}
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
Use it in the payment owner, keyed by the recovery key. Render checkout only
|
|
559
|
+
after `loaded` is true:
|
|
560
|
+
|
|
561
|
+
```tsx
|
|
562
|
+
const { loaded, recovery, saveRecovery } = usePaymentRecovery(recoveryKey);
|
|
563
|
+
|
|
564
|
+
return loaded ? (
|
|
565
|
+
<LockerversePayment
|
|
566
|
+
selection={selection}
|
|
567
|
+
resumePayment={recovery}
|
|
568
|
+
onPaymentRecoveryChange={saveRecovery}
|
|
569
|
+
widget={widget}
|
|
570
|
+
/>
|
|
571
|
+
) : <p>Loading checkout...</p>;
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
Retain the record on `onPaymentUncertain` and let the component recover the
|
|
575
|
+
same payment. Do not create a replacement charge for an unknown result.
|
|
576
|
+
`onPaymentComplete` confirms success; its `paymentRequired` distinguishes a
|
|
577
|
+
paid purchase from a no-charge order. This browser callback does not grant paid
|
|
578
|
+
access; access checks require server verification. Keep private contact and
|
|
579
|
+
payment data out of URLs, analytics, logs, and public metadata.
|
|
580
|
+
|
|
581
|
+
Malformed host selections and invalid custom tips render customer-safe validation messages and do not call the Lockerverse API. Product selection remains owned by the host application.
|
|
582
|
+
|
|
583
|
+
## Payment primitives
|
|
584
|
+
|
|
585
|
+
Use `LockerversePayment` for the complete checkout. Use these controlled components
|
|
586
|
+
when your website needs its own layout and payment flow. The ready-made checkout
|
|
587
|
+
uses the same fields and `PaymentSummary`; this entry does not import Stripe or
|
|
588
|
+
the ready-made payment controller.
|
|
589
|
+
|
|
590
|
+
```tsx
|
|
591
|
+
import {
|
|
592
|
+
buildCustomFieldAnswers,
|
|
593
|
+
createCustomFieldValues,
|
|
594
|
+
CustomField,
|
|
595
|
+
CustomFields,
|
|
596
|
+
EmailField,
|
|
597
|
+
getCheckoutCustomFields,
|
|
598
|
+
PaymentSummary,
|
|
599
|
+
reconcileCustomFieldValues,
|
|
600
|
+
validateCustomFields,
|
|
601
|
+
} from "@lockerverse/react/payment-primitives";
|
|
602
|
+
import "@lockerverse/react/payment.css";
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
See the [live composition example](https://sdk.lockerverse.com/examples/payment-primitives). It loads an
|
|
606
|
+
authoritative quote with the Core SDK and lets a website input and an SDK field
|
|
607
|
+
share the same React state. It previews submission fields and does not charge.
|
|
608
|
+
|
|
609
|
+
## Controlled custom fields
|
|
610
|
+
|
|
611
|
+
```tsx
|
|
612
|
+
const fields = getCheckoutCustomFields(catalog, checkout.items);
|
|
613
|
+
const [values, setValues] = useState(() =>
|
|
614
|
+
createCustomFieldValues(fields, { [itemNameFieldId]: "Autographed basketball" })
|
|
615
|
+
);
|
|
616
|
+
const [errors, setErrors] = useState<Record<string, string>>({});
|
|
617
|
+
|
|
618
|
+
<CustomFields
|
|
619
|
+
errors={errors}
|
|
620
|
+
fields={fields}
|
|
621
|
+
onChange={setValues}
|
|
622
|
+
values={values}
|
|
623
|
+
/>
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
`CustomFields` renders all selected fields. Use `CustomField` for one field at a
|
|
627
|
+
time, so you can put your own UI between them. Pass its `field`, `value`,
|
|
628
|
+
`onChange`, and optional `error`. The caller owns state; a website input can
|
|
629
|
+
change the same state at any time. The components supply labels, control IDs,
|
|
630
|
+
required states, and error associations.
|
|
631
|
+
|
|
632
|
+
Use field IDs as keys. Text uses strings, a single checkbox uses a boolean,
|
|
633
|
+
single-choice fields use an option ID, and multiple-choice fields use an array
|
|
634
|
+
of option IDs. `createCustomFieldValues` filters initial values against the field
|
|
635
|
+
definitions. Call `reconcileCustomFieldValues(fields, values)` when the selected
|
|
636
|
+
products or field definitions change. This removes fields and options that no
|
|
637
|
+
longer apply. These helpers do not change the input object.
|
|
638
|
+
|
|
639
|
+
Before submission, call `validateCustomFields`. It returns messages keyed by
|
|
640
|
+
field ID for missing required values. Render those messages and focus the first
|
|
641
|
+
invalid field. Then use `buildCustomFieldAnswers(fields, values)` for the
|
|
642
|
+
Core SDK submission shape. The backend remains the final validation authority.
|
|
643
|
+
|
|
644
|
+
## Submit through the Core SDK
|
|
645
|
+
|
|
646
|
+
The UI components do not submit or approve payments. Keep the existing Core SDK
|
|
647
|
+
flow: obtain an authoritative quote for the final selection, email, fees, tip,
|
|
648
|
+
and promo code; show any changed total; configure Stripe for that quote; and
|
|
649
|
+
complete Stripe Elements validation and confirmation-token creation.
|
|
650
|
+
|
|
651
|
+
At the submission step, add the shared field values to the normal request:
|
|
652
|
+
|
|
653
|
+
```ts
|
|
654
|
+
const fieldErrors = validateCustomFields(fields, values);
|
|
655
|
+
if (Object.keys(fieldErrors).length > 0) {
|
|
656
|
+
setErrors(fieldErrors);
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// finalCheckout comes from client.calculateTotal() for the final pricing inputs.
|
|
661
|
+
// confirmationToken comes from Stripe after Elements validation. Omit it only
|
|
662
|
+
// for a free checkout. Include required phone/shipping fields in contactDetails.
|
|
663
|
+
const result = await client.submitPayment({
|
|
664
|
+
checkout: finalCheckout,
|
|
665
|
+
submission: {
|
|
666
|
+
...contactDetails,
|
|
667
|
+
email,
|
|
668
|
+
confirmationToken,
|
|
669
|
+
customFieldAnswers: buildCustomFieldAnswers(fields, values),
|
|
670
|
+
},
|
|
671
|
+
});
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
This is the submission step, not a complete payment controller. Keep Stripe
|
|
675
|
+
authentication, payment-status recovery, duplicate-submit protection, and
|
|
676
|
+
confirmed completion handling in your host flow. A returned result can need
|
|
677
|
+
more action; do not show success merely because this promise resolves. If an
|
|
678
|
+
error has `recoveryRecommended: true`, call
|
|
679
|
+
`client.getPaymentStatus(finalCheckout.paymentReference)` before allowing a
|
|
680
|
+
new payment attempt.
|
|
681
|
+
|
|
682
|
+
## Public components and styles
|
|
683
|
+
|
|
684
|
+
| Export | Purpose |
|
|
685
|
+
| --- | --- |
|
|
686
|
+
| `FormField`, `FieldError` | Label, content wrapper, and associated error message |
|
|
687
|
+
| `TextField`, `EmailField`, `Checkbox` | Standard controlled form inputs |
|
|
688
|
+
| `PhoneInput`, `getInternationalPhone` | Country-aware phone input and normalization before submission |
|
|
689
|
+
| `CustomField`, `CustomFields` | Widget-defined text and choice fields |
|
|
690
|
+
| `PaymentSummary` | The same order summary used by ready-made checkout |
|
|
691
|
+
|
|
692
|
+
Each component has a named `...Props` type. `CustomFieldValues` and
|
|
693
|
+
`PhoneInputValue` describe the controlled state.
|
|
694
|
+
|
|
695
|
+
Import `@lockerverse/react/payment.css` and use a `lockerverse-payment` wrapper
|
|
696
|
+
with `data-lockerverse-theme="light"` or `"dark"` to retain the checkout
|
|
697
|
+
appearance. For a custom design, omit that stylesheet and style the components
|
|
698
|
+
in your host. Their labels, input behavior, and error associations remain.
|
|
699
|
+
|
|
700
|
+
`PaymentSummary` needs only `quote`: `<PaymentSummary quote={checkout} />`.
|
|
701
|
+
Its optional `displayedBaseCents`, `displayedTipCents`, and `displayedTotalCents`
|
|
702
|
+
properties let a host show pending price changes, as the ready-made checkout
|
|
703
|
+
does. They default to the quote's total minus tip, tip, and total. Display
|
|
704
|
+
overrides affect the UI only; they never change the payable amount.
|