tf-checkout-react 2.0.0-beta.25 → 2.0.0-beta.27

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 CHANGED
@@ -7,25 +7,46 @@ components, plus the shared API and React Query hooks. Which paths a buyer can
7
7
  actually use depend on the event, the deployed API, the buyer's session and the
8
8
  payment provider.
9
9
 
10
- This reference describes **2.0.0-beta.25**. It is a prerelease with a different
10
+ This reference describes **2.0.0-beta.27**. It is a prerelease with a different
11
11
  API from 1.x. Pin the version while evaluating it. npm's `latest` and `beta`
12
12
  tags can move independently and an unqualified install may select 1.x:
13
13
 
14
14
  ```bash
15
15
  npm view tf-checkout-react dist-tags
16
- npm install tf-checkout-react@2.0.0-beta.25 @tanstack/react-query react react-dom
16
+ npm install tf-checkout-react@2.0.0-beta.27 @tanstack/react-query react react-dom
17
17
  ```
18
18
 
19
- The [API reference](https://unpkg.com/tf-checkout-react@2.0.0-beta.25/docs/API.md)
20
- is included as `docs/API.md` in this package.
19
+ The [API reference](https://unpkg.com/tf-checkout-react@2.0.0-beta.27/docs/API.html)
20
+ is included as `docs/API.md` and `docs/API.html` in this package. A
21
+ [standalone guide](https://unpkg.com/tf-checkout-react@2.0.0-beta.27/docs/GUIDE.html)
22
+ is also available.
21
23
 
22
- Contents: [Requirements](#requirements) · [Quick start](#quick-start) ·
23
- [Configuration](#configuration-and-sessions) · [Embedded checkout](#embeddedcheckout)
24
- · [Composition](#compose-sections) · [Checkout lifecycle](#checkout-lifecycle)
25
- · [Payments](#payments) · [Authentication](#authentication) ·
26
- [Reference](#component-reference) · [Headless APIs](#headless-apis) ·
24
+ If the HTML has not reached the CDN yet, open the packaged `docs/API.md` in a
25
+ Markdown viewer or read the [plain-text API Markdown](https://cdn.jsdelivr.net/npm/tf-checkout-react@2.0.0-beta.27/docs/API.md).
26
+ The full guide is also shown on this npm page.
27
+
28
+ ## Choose a checkout layout
29
+
30
+ | Your goal | Start here | What the SDK renders |
31
+ | --- | --- | --- |
32
+ | Add a complete checkout to an event page | [`EmbeddedCheckout`](#embeddedcheckout) | A ready-made ticket or seat selection view followed by add-ons, buyer details, waiver and payment when applicable. |
33
+ | Continue a cart created by an earlier flow | [`ResumedCheckout`](#resume-a-server-cart) | Ready-made buyer details, waiver and payment for the current session's existing cart. |
34
+ | Keep the checkout on one page and arrange the sections yourself | [Single-page composition](#single-page-checkout) | Ticket selection, seats, add-ons, billing and payment sections in the host's layout. |
35
+ | Show progressive steps or connect routes | [Step-by-step composition](#step-by-step-checkout) | The same sections; your application controls what is visible and how the buyer moves between steps. |
36
+ | Organize the form as an accordion | [Accordion composition](#accordion-checkout) | Native HTML disclosure sections around the SDK's checkout components. |
37
+ | Build your own checkout UI | [Headless APIs](#headless-apis) | The shared engine, hooks and API; your application owns the UI and navigation. |
38
+
39
+ `EmbeddedCheckout` and `ResumedCheckout` are the ready-made flows in this
40
+ version. The 1.x `*Container` components and named single-page/accordion
41
+ examples are not drop-in 2.x exports. The [migration guide](#migrating-from-1x)
42
+ shows how to reproduce those layouts with the current components.
43
+
44
+ [Requirements](#requirements) · [Quick start](#quick-start) ·
45
+ [Configure sessions](#configuration-and-sessions) · [Ready-made checkout](#embeddedcheckout) ·
46
+ [Custom layouts](#compose-sections) · [Payments](#payments) ·
47
+ [Authentication](#authentication) · [Component reference](#component-reference) ·
27
48
  [Styling](#styling-and-localization) · [Migration](#migrating-from-1x) ·
28
- [Testing and troubleshooting](#testing-and-troubleshooting)
49
+ [Troubleshooting](#testing-and-troubleshooting)
29
50
 
30
51
  ## Requirements
31
52
 
@@ -50,17 +71,25 @@ Its `dist/styles` files and referenced assets are part of the npm package.
50
71
 
51
72
  ## Quick start
52
73
 
53
- Configure once before mounting the SDK. Create the query client once rather
54
- than on every render. Replace the placeholders with an event and a registered
55
- public client ID in the environment you are using.
74
+ This example puts the ready-made checkout on one event page. You need a test
75
+ event ID and the matching **public** SDK client ID. Ask your Ticket Fairy
76
+ integration contact for these values and the appropriate environment.
77
+
78
+ 1. Install the package and its peers using the command above.
79
+ 2. Replace `EVENT_ID` and `PUBLIC_SDK_CLIENT_ID` below. Configure the SDK once
80
+ in your browser entry, before rendering the checkout.
81
+ 3. Mount `App` in your React application. The package entry imports its CSS;
82
+ the bundler must process CSS imports and their assets.
56
83
 
57
84
  ```tsx
58
85
  import { useState } from 'react'
59
86
  import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
60
- import { configureCheckout, EmbeddedCheckout } from 'tf-checkout-react'
87
+ import {
88
+ configureCheckout, EmbeddedCheckout, type PlaceOrderResult,
89
+ } from 'tf-checkout-react'
61
90
 
62
91
  configureCheckout({
63
- env: 'PROD',
92
+ env: 'STAG',
64
93
  clientId: 'PUBLIC_SDK_CLIENT_ID',
65
94
  clientSecret: '',
66
95
  cookieAuth: true,
@@ -69,30 +98,41 @@ configureCheckout({
69
98
  const queryClient = new QueryClient()
70
99
 
71
100
  export function App() {
72
- const [notice, setNotice] = useState('')
101
+ const [order, setOrder] = useState<PlaceOrderResult | null>(null)
102
+
73
103
  return (
74
104
  <QueryClientProvider client={queryClient}>
75
- <EmbeddedCheckout
76
- eventId="EVENT_ID"
77
- onComplete={(order) => {
78
- setNotice(order.pending ? 'Payment pending' : 'Order submitted')
79
- }}
80
- />
81
- <p role="status">{notice}</p>
105
+ {order ? (
106
+ <section role="status">
107
+ <h1>Checking your order</h1>
108
+ <p>Check your order status before making another purchase.</p>
109
+ {order.pending && <p>Your payment is pending.</p>}
110
+ {order.needIdVerification && <p>Identity verification may be required.</p>}
111
+ </section>
112
+ ) : (
113
+ <EmbeddedCheckout
114
+ eventId="EVENT_ID"
115
+ confirmationMode="redirect"
116
+ onComplete={setOrder}
117
+ />
118
+ )}
82
119
  </QueryClientProvider>
83
120
  )
84
121
  }
85
122
  ```
86
123
 
87
- Use a test event and the corresponding test client ID with `env: 'STAG'` for
88
- integration testing. `onComplete` receives a result that may be pending; do not
89
- fulfil an order merely because this callback fired. See
90
- [Payments](#payments) for reconciliation and retry guidance.
124
+ The buyer first chooses tickets or seats. After reservation, the checkout shows
125
+ applicable add-ons, buyer details, waiver and payment. `onComplete` hands the
126
+ result to the host; the example displays a neutral status instead of claiming
127
+ that tickets are ready. Connect that screen to **your backend's authoritative
128
+ order status** using `order.orderHash`, handle any required ID verification and
129
+ pending voucher, and provide support if the status cannot be established. The
130
+ callback alone is not proof of settlement or ticket issuance.
91
131
 
92
- The inline confirmation also offers a generic share link using the current
93
- page URL without checking referral eligibility. That URL can contain query
94
- parameters or lack the buyer's referral. Review the
95
- [confirmation limitations](#checkout-sections) before using the default flow.
132
+ Use `env: 'PROD'` with the matching production public client ID when your
133
+ integration is ready. This beta's stock payment UI can ask a buyer to retry an
134
+ unconfirmed payment; that wording does not establish that retrying is safe. See
135
+ [Payments](#payments) before using the stock flow in production.
96
136
 
97
137
  ## Configuration and sessions
98
138
 
@@ -147,7 +187,31 @@ third-party dependencies should be tested in that framework.
147
187
  seat selection, add-ons, billing, waivers, payment and inline completion as the
148
188
  flow requires. It must be under `QueryClientProvider`.
149
189
 
150
- Only `eventId` is required. Callbacks and options are optional.
190
+ Only `eventId` is required. Callbacks and options are optional. The component
191
+ owns its checkout provider; do not wrap it in another `CheckoutProvider` to
192
+ control its internal steps. The host still supplies `QueryClientProvider`.
193
+
194
+ ### What the buyer sees
195
+
196
+ 1. **Tickets:** the event's catalog, quantities, applicable codes and any
197
+ organizer-configured waiting list. Seated events can open a seat map.
198
+ 2. **Checkout:** a selection summary and the applicable add-ons, buyer/holder
199
+ details, waiver and payment. The reservation timer is displayed when available.
200
+ 3. **Completion:** `inline` shows the SDK's confirmation view; `redirect` calls
201
+ `onComplete` and leaves the host to show the next screen. The quick start uses
202
+ `redirect` so the host can check order status before presenting a receipt.
203
+
204
+ `initialView="seats"` opens the seat-selection view first for an applicable
205
+ seated event. `onViewChange` reports `tickets`, `seats` or `checkout`; it does
206
+ not turn the component into a controlled router. For custom paths or step
207
+ arrangements, use [section composition](#compose-sections).
208
+
209
+ When choosing `confirmationMode="redirect"`, provide `onComplete` to receive
210
+ the result and navigate to your status page. Although the prop is optional in
211
+ the type, without that handoff the checkout remains mounted and no host status
212
+ page is reached.
213
+
214
+ ### Options and callbacks
151
215
 
152
216
  | Prop | Type / default | Description |
153
217
  | --- | --- | --- |
@@ -179,98 +243,408 @@ session authorities; use the one appropriate to your configured journey.
179
243
  for the public checkout registration or account-completion journey; those
180
244
  paths force it on in this version.
181
245
 
246
+ ### Example: brand the ready-made checkout
247
+
248
+ Use the same `QueryClientProvider` and one-time configuration as the quick
249
+ start. Here `onViewChange` can update surrounding page content; `onComplete`
250
+ passes the order result to the host's status screen.
251
+
252
+ ```tsx
253
+ import { useState } from 'react'
254
+ import { EmbeddedCheckout, type PlaceOrderResult } from 'tf-checkout-react'
255
+
256
+ export function BrandedCheckout() {
257
+ const [order, setOrder] = useState<PlaceOrderResult | null>(null)
258
+ const [view, setView] = useState('tickets')
259
+
260
+ if (order) return <p role="status">Checking your order status…</p>
261
+
262
+ return (
263
+ <main>
264
+ <p>Checkout step: {view}</p>
265
+ <EmbeddedCheckout
266
+ eventId="EVENT_ID"
267
+ confirmationMode="redirect"
268
+ onComplete={setOrder}
269
+ onViewChange={setView}
270
+ initialPromoCode="WELCOME"
271
+ referrer="REFERRER_ID"
272
+ readAttributionFromUrl={false}
273
+ locale="en-US"
274
+ messages={{ 'tickets.getTickets': 'Continue to checkout' }}
275
+ theme={{ colorScheme: 'light', accent: '#6b2bd9', radiusCard: '16px' }}
276
+ className="festival-checkout"
277
+ />
278
+ </main>
279
+ )
280
+ }
281
+ ```
282
+
283
+ Use a promo or referral only when it belongs to this visit. Remove those props
284
+ when they do not apply. By default the component also reads `promo` /
285
+ `promo_code` and `ttf_r` / `referrer_id` from the URL. The example disables
286
+ that behavior so the host controls attribution. `initialPromoCode` is applied
287
+ once when the catalog mounts. It is not a guarantee that a code is valid.
288
+ `theme` changes SDK design tokens, `className` lets your stylesheet target this
289
+ instance, and `messages` overrides individual SDK copy keys. `locale` and
290
+ `messages` do not automatically translate the event's own content or an
291
+ external payment-provider page. See [Styling and localization](#styling-and-localization)
292
+ for the available theme tokens and locales.
293
+
294
+ ### Example: connect host navigation and sign-in
295
+
296
+ Pass callbacks when the surrounding application owns these actions. For
297
+ example, `openSignIn` can open your sign-in UI, and `onOrder` can hand the
298
+ result to your order-status screen. `onViewChange` is a notification, not a
299
+ way to force the checkout to a particular step.
300
+
301
+ ```tsx
302
+ import { EmbeddedCheckout, type PlaceOrderResult } from 'tf-checkout-react'
303
+
304
+ type View = 'tickets' | 'seats' | 'checkout'
305
+
306
+ export function HostCheckout(props: {
307
+ eventId: string
308
+ openSignIn: (email?: string) => void
309
+ showOrderStatus: (order: PlaceOrderResult) => void
310
+ reportView: (view: View) => void
311
+ }) {
312
+ return (
313
+ <EmbeddedCheckout
314
+ eventId={props.eventId}
315
+ onLogin={props.openSignIn}
316
+ onViewChange={props.reportView}
317
+ confirmationMode="redirect"
318
+ onComplete={props.showOrderStatus}
319
+ />
320
+ )
321
+ }
322
+ ```
323
+
324
+ The email passed to `openSignIn` can be absent. Supplying `onLogin` replaces
325
+ the built-in login modal; your host must complete the appropriate sign-in
326
+ journey. An event-scoped checkout identity is not automatically a full
327
+ account session. `showOrderStatus` must check the order and provider rather
328
+ than treating the callback as a paid receipt.
329
+
182
330
  ### Resume a server cart
183
331
 
184
- `ResumedCheckout` calls `resumeCart` for an existing cart in the current
185
- session. It does not create that cart or accept a cart ID as a prop. A resale
186
- acceptance flow, for example, must create the cart first. The component shows
187
- billing, waivers and payment and requires the same query provider and
188
- configuration as the embedded flow.
332
+ `ResumedCheckout` is a ready-made layout for a cart that **already exists in
333
+ the current session**, such as one created by a preceding resale flow. It
334
+ starts with buyer details and payment; it does not show ticket selection, create
335
+ the cart, or accept a cart ID. Its `confirmationMode` defaults to `redirect`.
336
+ Provide `onComplete` for the redirect/status handoff: without it the successful
337
+ flow remains on its loading transition. Alternatively select `inline` and
338
+ account for the confirmation and verification-copy limitations below.
189
339
 
190
340
  ```tsx
191
341
  import { useState } from 'react'
192
- import { ResumedCheckout } from 'tf-checkout-react'
342
+ import { ResumedCheckout, type PlaceOrderResult } from 'tf-checkout-react'
193
343
 
344
+ // Mount only after your preceding flow created a cart in this session.
194
345
  export function ResumePage() {
195
- const [notice, setNotice] = useState('')
346
+ const [order, setOrder] = useState<PlaceOrderResult | null>(null)
347
+ if (order) return <p role="status">Checking your order status…</p>
348
+
196
349
  return (
197
- <>
198
350
  <ResumedCheckout
199
351
  eventId="EVENT_ID"
200
352
  isFromResale
201
- confirmationMode="inline"
202
- onComplete={(order) => setNotice(order.pending ? 'Payment pending' : 'Order submitted')}
353
+ confirmationMode="redirect"
354
+ onComplete={setOrder}
203
355
  />
204
- <p role="status">{notice}</p>
205
- </>
206
356
  )
207
357
  }
208
358
  ```
209
359
 
360
+ Wrap this page in the same `QueryClientProvider` as the quick start. Set
361
+ `isFromResale` only for the resale path. Check the result with your backend,
362
+ including any verification or pending-payment requirements. If the preceding
363
+ flow did not create a cart, or the session changed, return the buyer to that
364
+ flow instead of assuming a cart can be resumed.
365
+
210
366
  Props are `eventId` (required), `isFromResale` (default `false`),
211
- `onComplete`, `confirmationMode` (default **`redirect`**),
212
- `requireEmailConfirmation`, `collectTicketHolders`, `collectHolderContact`
213
- (each default `true`), `privacyPolicy`, `brandOptInLabel`,
214
- `additionalConditions`, `theme` and `className`. With the default redirect
215
- mode, route in `onComplete`; otherwise a transition remains visible. Unlike
216
- `EmbeddedCheckout`, this component does not accept `locale`, `messages`,
217
- `auth` or `onLogin` props in this version.
367
+ `onComplete`, `confirmationMode`, `requireEmailConfirmation`,
368
+ `collectTicketHolders`, `collectHolderContact` (each default `true`),
369
+ `privacyPolicy`, `brandOptInLabel`, `additionalConditions`, `theme` and
370
+ `className`. Unlike `EmbeddedCheckout`, this component does not accept
371
+ `locale`, `messages`, `auth` or `onLogin` props in this version.
218
372
 
219
373
  ## Compose sections
220
374
 
221
- For a different layout, render sections beneath `CheckoutProvider` and the
222
- host's `QueryClientProvider`. The provider creates a web-wired `useCheckout`
223
- instance unless `value` is supplied. Sections share its context. The app-wide
224
- checkout store is shared by default, so separate `useCheckout()` calls are not
225
- independent carts. Avoid mounting competing checkout flows for different events
226
- at once; coordinate completion/reset and session transitions in the host.
375
+ Use sections when the ready-made flow does not fit your page. Put all sections
376
+ for one checkout inside a single `CheckoutProvider`, under the host's
377
+ `QueryClientProvider`. The provider shares one checkout instance with its
378
+ children. The default store is app-wide: mounting two flows does not create two
379
+ independent carts. Configure the SDK once, before mounting the provider.
380
+
381
+ The examples below assume the quick start's `configureCheckout` and
382
+ `QueryClientProvider` setup. They demonstrate page layout and section wiring;
383
+ your host still needs to implement order-status reconciliation, expiry and
384
+ recovery for its event and enabled payment methods.
385
+
386
+ These are wiring recipes, not complete buyer navigation. Each includes a return
387
+ from the seat view and keeps `Tickets` mounted while switching views. The host
388
+ still needs to implement Edit and Back from checkout (including payment to
389
+ billing), preserve entered details, handle seat holds and cart expiry, and
390
+ guard event/session changes and in-flight or retained payments. Use
391
+ `EmbeddedCheckout` for its built-in Back and guarded Edit behavior.
392
+ The seat-view Back button only changes views: it does not itself release reserved
393
+ seats or cancel an already-started reservation or cart priming. Coordinate any
394
+ in-flight work and the hold's expiry or release under your deployed backend
395
+ contract before permitting a conflicting selection.
396
+
397
+ ### Single-page checkout
398
+
399
+ This layout keeps the buyer on one browser page. Selecting tickets or seats
400
+ reveals a single combined form with add-ons, buyer details, waiver and payment.
401
+ The host supplies the status view after `onComplete`.
227
402
 
228
403
  ```tsx
229
404
  import { useState } from 'react'
230
405
  import {
231
- AddOns, BillingInfo, CheckoutProvider, OrderSummary, Payment,
232
- Tickets, WaiverSigning,
406
+ AddOns, BillingInfo, CheckoutProvider, Payment, SeatMap,
407
+ Tickets, TimerBanner, WaiverSigning, useCheckoutContext, type PlaceOrderResult,
233
408
  } from 'tf-checkout-react'
234
409
 
235
- export function ComposedCheckout() {
236
- const eventId = 'EVENT_ID'
237
- const [notice, setNotice] = useState('')
410
+ function SinglePageContents({ eventId }: { eventId: string }) {
411
+ const checkout = useCheckoutContext()
412
+ const [view, setView] = useState<'tickets' | 'seats' | 'checkout'>('tickets')
413
+ const [order, setOrder] = useState<PlaceOrderResult | null>(null)
414
+
415
+ if (order) return <p role="status">Checking your order status…</p>
416
+
417
+ return (
418
+ <main>
419
+ <div hidden={view !== 'tickets'}>
420
+ <Tickets
421
+ eventId={eventId}
422
+ onContinue={() => setView('checkout')}
423
+ onSelectSeats={() => setView('seats')}
424
+ />
425
+ </div>
426
+ {view === 'seats' && (
427
+ <>
428
+ <button type="button" onClick={() => setView('tickets')}>Back to tickets</button>
429
+ <SeatMap eventId={eventId} onContinue={() => setView('checkout')} />
430
+ </>
431
+ )}
432
+ {view === 'checkout' && (
433
+ <>
434
+ <TimerBanner seconds={checkout.secondsLeft} />
435
+ <AddOns eventId={eventId} />
436
+ <BillingInfo
437
+ eventId={eventId}
438
+ requireEmailConfirmation
439
+ collectTicketHolders
440
+ collectHolderContact
441
+ />
442
+ <WaiverSigning eventId={eventId} />
443
+ <Payment
444
+ eventId={eventId}
445
+ requireEmailConfirmation
446
+ collectTicketHolders
447
+ requireHolderContact
448
+ onComplete={setOrder}
449
+ />
450
+ </>
451
+ )}
452
+ </main>
453
+ )
454
+ }
455
+
456
+ export function SinglePageCheckout({ eventId }: { eventId: string }) {
238
457
  return (
239
458
  <CheckoutProvider theme={{ colorScheme: 'light', accent: '#2458d3' }}>
240
- <Tickets eventId={eventId} />
241
- <AddOns eventId={eventId} />
242
- <BillingInfo eventId={eventId} requireEmailConfirmation />
243
- <WaiverSigning eventId={eventId} />
244
- <OrderSummary eventId={eventId} />
245
- <Payment
246
- eventId={eventId}
247
- requireEmailConfirmation
248
- onComplete={(order) => setNotice(order.pending ? 'Payment pending' : 'Order submitted')}
249
- />
250
- <p role="status">{notice}</p>
459
+ <SinglePageContents eventId={eventId} />
251
460
  </CheckoutProvider>
252
461
  )
253
462
  }
254
463
  ```
255
464
 
256
- This is a basic general-admission layout. Use section callbacks to reveal and
257
- navigate between steps as needed. For seated events wire `Tickets.onSelectSeats`
258
- to a `SeatMap` view; without the callback its seat-selection CTA is hidden.
259
- Keep `BillingInfo`'s email/holder rules aligned with `Payment`'s corresponding
260
- rules. If using a host privacy policy, pass the policy to `BillingInfo`, pass
261
- `requirePrivacyPolicy` to `Payment`, and use `onPrivacyRequired` to reveal the
262
- billing error. Render the event's required conditions and waiver, not just a
263
- card input. Read the consent limitations below before using `privacyPolicy` or
264
- the billing-skip option. Do not skip add-ons when one is required.
265
-
266
- `CheckoutProvider` props: `children` (required), `value?: UseCheckout`,
465
+ `Tickets.onContinue` fires after the cart is primed. `onSelectSeats` opens the
466
+ seat view; without it the seat-selection button is hidden in a composed
467
+ `Tickets` section. `SeatMap.onContinue` runs after seats are added and priced.
468
+ Without an `onContinue` prop, `BillingInfo` and `AddOns` use their combined-form
469
+ behavior instead of showing a separate Continue button. Match the billing and
470
+ payment email/holder settings, as shown. `AddOns` renders nothing when the
471
+ event has no applicable add-ons; do not skip a required one.
472
+
473
+ ### Step-by-step checkout
474
+
475
+ To make billing and payment separate steps, give `BillingInfo` an `onContinue`
476
+ callback. It then validates and saves the buyer details before moving on. This
477
+ example keeps the checkout provider mounted for the whole flow; it does not
478
+ change URLs or assume a browser refresh can resume the in-memory layout.
479
+
480
+ ```tsx
481
+ import { useState } from 'react'
482
+ import {
483
+ AddOns, BillingInfo, CheckoutProvider, Payment, SeatMap,
484
+ Tickets, TimerBanner, WaiverSigning, useCheckoutContext, type PlaceOrderResult,
485
+ } from 'tf-checkout-react'
486
+
487
+ type Step = 'tickets' | 'seats' | 'details' | 'payment'
488
+
489
+ function StepContents({ eventId }: { eventId: string }) {
490
+ const checkout = useCheckoutContext()
491
+ const [step, setStep] = useState<Step>('tickets')
492
+ const [order, setOrder] = useState<PlaceOrderResult | null>(null)
493
+
494
+ if (order) return <p role="status">Checking your order status…</p>
495
+
496
+ return (
497
+ <main>
498
+ {(step === 'details' || step === 'payment') && (
499
+ <TimerBanner seconds={checkout.secondsLeft} />
500
+ )}
501
+ <div hidden={step !== 'tickets'}>
502
+ <Tickets
503
+ eventId={eventId}
504
+ onContinue={() => setStep('details')}
505
+ onSelectSeats={() => setStep('seats')}
506
+ />
507
+ </div>
508
+ {step === 'seats' && (
509
+ <>
510
+ <button type="button" onClick={() => setStep('tickets')}>Back to tickets</button>
511
+ <SeatMap eventId={eventId} onContinue={() => setStep('details')} />
512
+ </>
513
+ )}
514
+ {step === 'details' && (
515
+ <BillingInfo
516
+ eventId={eventId}
517
+ requireEmailConfirmation
518
+ collectTicketHolders
519
+ collectHolderContact
520
+ onContinue={() => setStep('payment')}
521
+ />
522
+ )}
523
+ {step === 'payment' && (
524
+ <>
525
+ <AddOns eventId={eventId} />
526
+ <WaiverSigning eventId={eventId} />
527
+ <Payment
528
+ eventId={eventId}
529
+ requireEmailConfirmation
530
+ collectTicketHolders
531
+ requireHolderContact
532
+ onComplete={setOrder}
533
+ />
534
+ </>
535
+ )}
536
+ </main>
537
+ )
538
+ }
539
+
540
+ export function StepCheckout({ eventId }: { eventId: string }) {
541
+ return <CheckoutProvider><StepContents eventId={eventId} /></CheckoutProvider>
542
+ }
543
+ ```
544
+
545
+ For separate routes, keep the provider above the routed pages and connect the
546
+ callbacks to your router. A full page reload is a different session/navigation
547
+ problem: `ResumedCheckout` can attach only to an existing cart in the current
548
+ session, and the old 1.x localStorage state is not a migration format. Handle
549
+ expired reservations, browser Back, buyer changes and unconfirmed payments
550
+ before enabling navigation or a new purchase. Do not unmount or reset an
551
+ in-flight payment merely to move between steps.
552
+
553
+ The step example is a progressive layout: the next step appears after a
554
+ successful callback. A router can replace `setStep`, but the SDK does not
555
+ supply a route-based checkout or implement your browser Back handling. Do not
556
+ assume that a successful TypeScript build verifies route, refresh or payment
557
+ behavior; test those flows with your own backend and providers.
558
+
559
+ ### Accordion checkout
560
+
561
+ The SDK does not export a named accordion component. This host-built layout
562
+ uses native HTML disclosures to group the same SDK sections. The payment field
563
+ stays outside the disclosures so it remains visible while the buyer pays.
564
+ It uses the configuration and `QueryClientProvider` from the quick start.
565
+
566
+ ```tsx
567
+ import { useState } from 'react'
568
+ import {
569
+ AddOns, BillingInfo, CheckoutProvider, Payment, SeatMap,
570
+ Tickets, TimerBanner, WaiverSigning, useCheckoutContext, type PlaceOrderResult,
571
+ } from 'tf-checkout-react'
572
+
573
+ function AccordionContents({ eventId }: { eventId: string }) {
574
+ const checkout = useCheckoutContext()
575
+ const [view, setView] = useState<'tickets' | 'seats' | 'checkout'>('tickets')
576
+ const [order, setOrder] = useState<PlaceOrderResult | null>(null)
577
+
578
+ if (order) return <p role="status">Checking your order status…</p>
579
+
580
+ return (
581
+ <main>
582
+ <div hidden={view !== 'tickets'}>
583
+ <Tickets
584
+ eventId={eventId}
585
+ onContinue={() => setView('checkout')}
586
+ onSelectSeats={() => setView('seats')}
587
+ />
588
+ </div>
589
+ {view === 'seats' && (
590
+ <>
591
+ <button type="button" onClick={() => setView('tickets')}>Back to tickets</button>
592
+ <SeatMap eventId={eventId} onContinue={() => setView('checkout')} />
593
+ </>
594
+ )}
595
+ {view === 'checkout' && (
596
+ <>
597
+ <TimerBanner seconds={checkout.secondsLeft} />
598
+ <details open>
599
+ <summary>Add-ons</summary>
600
+ <AddOns eventId={eventId} />
601
+ </details>
602
+ <details open>
603
+ <summary>Buyer and ticket holders</summary>
604
+ <BillingInfo
605
+ eventId={eventId}
606
+ requireEmailConfirmation
607
+ collectTicketHolders
608
+ collectHolderContact
609
+ />
610
+ </details>
611
+ <details open>
612
+ <summary>Waiver</summary>
613
+ <WaiverSigning eventId={eventId} />
614
+ </details>
615
+ <Payment
616
+ eventId={eventId}
617
+ requireEmailConfirmation
618
+ collectTicketHolders
619
+ requireHolderContact
620
+ onComplete={setOrder}
621
+ />
622
+ </>
623
+ )}
624
+ </main>
625
+ )
626
+ }
627
+
628
+ export function AccordionCheckout({ eventId }: { eventId: string }) {
629
+ return <CheckoutProvider><AccordionContents eventId={eventId} /></CheckoutProvider>
630
+ }
631
+ ```
632
+
633
+ The buyer reserves tickets or seats, then can open or close the form groups.
634
+ Closing a `<details>` element hides its contents without unmounting them. In a
635
+ production accordion, open the relevant group when validation needs the buyer's
636
+ attention, and make sure required add-ons, fields and waivers cannot be missed.
637
+ Do not hide or replace the active payment field during a payment attempt.
638
+ As with the other examples, replace the status placeholder with your own
639
+ verified order-status and support flow.
640
+
641
+ `CheckoutProvider` props are `children` (required), `value?: UseCheckout`,
267
642
  `theme?: CheckoutTheme`, `locale?: LocaleCode`,
268
643
  `messages?: Partial<Messages>`, and `className?: string`. Use
269
- `useCheckoutContext()` inside the provider. `useCheckoutValidation()` exposes
270
- `{ attempt, requestValidation }` for coordinating validation between sections;
271
- both context hooks require a provider. The provider's default checkout includes
272
- the installment picker. If supplying your own `value`, wire any custom payment
273
- ports you need when creating that checkout.
644
+ `useCheckoutContext()` inside the provider to read the current checkout and
645
+ `useCheckoutValidation()` to coordinate validation between sections. The
646
+ provider's default checkout includes the installment picker. If supplying your
647
+ own `value`, wire any custom payment ports you need when creating that checkout.
274
648
 
275
649
  ## Checkout lifecycle
276
650
 
@@ -309,6 +683,18 @@ error or closed provider page means the charge failed. A custom provider page mu
309
683
  settle with `{ event: 'dismiss' }` on close/unmount, and other custom payment
310
684
  ports must settle, or a waiting reset cannot finish.
311
685
 
686
+ The beta.27 declaration comments for `reset` and `PAID_ORDER_PENDING_MESSAGE`
687
+ say pressing Pay again finishes an unconfirmed order; the `resetWhenSettled`
688
+ comment also omits retired checkouts as a reason for `false`. Those comments do
689
+ not establish that a retry is safe. Use the session and reconciliation guidance
690
+ above.
691
+
692
+ Stock `Payment` can display an engine message telling the buyer to press Pay
693
+ again when payment is unconfirmed. That wording does not establish that a retry
694
+ is safe. If neutral buyer guidance is required for uncertain outcomes, use a
695
+ custom payment flow with host-controlled error UI, retain the order and reconcile
696
+ the current session and authoritative order/provider status before recovery.
697
+
312
698
  ### Tickets, codes, seats and waiting lists
313
699
 
314
700
  `Tickets` reads the catalog, presents the event's promo/access-code controls and
@@ -354,7 +740,7 @@ holder's answers in `ticketDataCapture`, and add-on answers via
354
740
  the opaque catalog ID. Use backend country/state IDs for `countryId` and
355
741
  `stateId`; `countryCode` / `stateCode` are payment billing codes.
356
742
 
357
- In beta.25 conditional custom-field requiredness is left to the backend and
743
+ In beta.27 conditional custom-field requiredness is left to the backend and
358
744
  not all hosted-checkout conditional visibility is reproduced. Verify the
359
745
  actual event against the deployed checkout, including blanks, images, holder
360
746
  scoping and validation errors; do not claim parity from the presence of a
@@ -362,11 +748,20 @@ field alone. Terms and `additionalConditions` are client-side acceptance gates;
362
748
  respect the server's own requirements. `Payment` renders condition `content`,
363
749
  or `name` as a fallback, as HTML. Only supply trusted or appropriately sanitized
364
750
  content, and verify the source and handling of server-provided conditions too.
365
- Failed waiver reads other than a 404 remain unknown and block placement; use
366
- `reloadCheckoutDetails()` to retry rather than silently skipping it.
751
+
752
+ During cart priming, failed custom-field, checkout-config, add-on or event
753
+ condition reads fall back to empty/default data. The stock UI cannot show or
754
+ gate requirements it did not load. Do not treat an empty list as proof that no
755
+ requirement exists; verify deployment/failure behavior and add tested host
756
+ safeguards for mandatory fields and acknowledgments. Server validation of
757
+ required answers is not proof that a condition was acknowledged. Only failed
758
+ waiver reads other than a 404 are recorded in `unavailableDetails`. Before money
759
+ may have moved, placement retries an unavailable waiver and blocks if it remains
760
+ unknown; `reloadCheckoutDetails()` can also retry the waiver read. A retained
761
+ order with a possible payment follows separate recovery and needs reconciliation.
367
762
  `WaiverSigning` handles the SDK's signing flow.
368
763
 
369
- **Consent limitation in beta.25:** when `privacyPolicy` is supplied, the
764
+ **Consent limitation in beta.27:** when `privacyPolicy` is supplied, the
370
765
  stock `BillingInfo` replaces the Ticket Fairy marketing checkbox with the
371
766
  required privacy checkbox, but both use `consent.ttfOptIn`; the order sends
372
767
  that value as `ttf_opt_in`. The privacy acknowledgement and Ticket Fairy
@@ -406,6 +801,19 @@ for authoritative confirmation before showing a paid receipt or fulfilling.
406
801
  ID verification may still be required after payment. An ID-verification failure
407
802
  is not a reason to repeat the charge.
408
803
 
804
+ The stock in-flow `Confirmation` displays confirmed/ticket-available copy when
805
+ `pending` is absent or false; it does not independently verify final settlement.
806
+ For both paid and free orders, use `confirmationMode="redirect"` with a
807
+ host-owned status page, or a custom composed confirmation gated on authoritative
808
+ order and provider status. Arrange ID verification on the host route when
809
+ `needIdVerification` is true; redirect does not render the inline verification.
810
+ The stock `IDVerification` and routed `OrderIDVerification` verified messages
811
+ say tickets are on their way; they do not check final settlement or issuance.
812
+ Use host-owned neutral verification and status UI when that copy is premature.
813
+ If `onComplete` navigates away or unmounts the checkout, its inline verification
814
+ cannot appear: keep the provider mounted or handle verification at the destination.
815
+ For a custom flow, see [host-owned ID verification](#host-owned-id-verification).
816
+
409
817
  **Zero-total orders:** this SDK ignores the free-registration response body and
410
818
  reports `needIdVerification: false`. Verification or approval may still be
411
819
  required, and `free: true` is not proof tickets were issued. The default inline
@@ -421,7 +829,8 @@ order's payment status before showing its default confirmed/ticket-availability
421
829
  copy. Gate it on an authoritative status appropriate for that page. When
422
830
  `onComplete` reports `pending`, show a pending page (and any voucher) instead;
423
831
  do not route directly to `OrderConfirmation` as a paid receipt. The in-flow
424
- `Confirmation` component reads the current result and has a pending state.
832
+ `Confirmation` component reads the current result and has a pending state, but
833
+ the absence of `pending` is not proof of settlement.
425
834
 
426
835
  `CardInput` renders Stripe Elements and uses the checkout's default card owner;
427
836
  `StripePaymentElement` is the deferred Elements option when its server config
@@ -452,6 +861,24 @@ three only when the provider confirms nothing was charged. An unclassified
452
861
  error is treated as unknown. Avoid automatic blind retries and never promise
453
862
  exactly-once charging across your app, backend and provider.
454
863
 
864
+ ### Host-owned ID verification
865
+
866
+ `runIdVerification(orderHash, options)` does not open the provider on its own.
867
+ Supply `openUrl` to validate the URL against your provider and scheme policy
868
+ and open it in the browser; handle a missing URL or blocked/failed opening in
869
+ your UI. The engine swallows asynchronous `openUrl` rejections, so catch and
870
+ record opening failures inside the callback: polling can otherwise continue
871
+ without a handoff. Use `onPhase` for polling state and handle browser returns;
872
+ `registerForegroundListener` can trigger a recheck on return and must return an
873
+ unsubscribe function.
874
+
875
+ Alternatively, `onRequired` receives the order hash and possibly null URL; if
876
+ it returns `true`, the engine resolves `undefined` without opening or polling,
877
+ and your host owns both steps. A timeout or poll error rejects with a message
878
+ promising that payment is safe and tickets will be emailed. Replace that text
879
+ with neutral status and reconcile authoritative payment, verification and
880
+ issuance. Verification alone does not prove settlement or issuance.
881
+
455
882
  ## Authentication
456
883
 
457
884
  The embedded checkout supplies the event to its built-in `AuthModal`. A
@@ -520,7 +947,7 @@ package are the authoritative detail for callback payloads and DTOs.
520
947
  | `BillingInfo` | `eventId?`, `onContinue?`, `heading?`, `buttonLabel?`, `onLogin?`, `skipWhenProfileComplete?`, `requireEmailConfirmation?`, `collectTicketHolders?`, `collectHolderContact?`, `ttfOptInLabel?`, `brandOptInLabel?`, `privacyPolicy?`, `showPrivacyError?`, `step?`, `className?`. Standalone holder collection follows backend gates unless explicitly enabled. |
521
948
  | `WaiverSigning` | `eventId`, `className?`. Render before payment for an event requiring a waiver. |
522
949
  | `Payment` | `onComplete?`, `eventId?`, `requireEmailConfirmation?`, `collectTicketHolders?`, `requireHolderContact?`, `additionalConditions?`, `requirePrivacyPolicy?`, `onPrivacyRequired?`, `showTestCardHint?`, `className?`. Match the paired billing policies. |
523
- | `OrderSummary` | `eventId?`, `className?`. Uses the current checkout's pricing. |
950
+ | `OrderSummary` | `eventId?`, `className?`. Uses the current checkout's pricing. `Payment` already renders it; use it separately when you need a summary elsewhere. |
524
951
  | `IDVerification` | `className?`. Uses the current checkout/order. |
525
952
  | `Confirmation` | `shareUrl?`, `shareMessage?`, `viewOrderUrl?`, `children?`, `className?`. Uses the current checkout result and handles pending presentation. The default share URL and eligibility have the limitations below. |
526
953
  | `OrderConfirmation` | `orderHash`, `title?`, `message?`, `helper?`, `eventTitle?`, `eventDate?`, `eventLocation?`, `viewOrderUrl?` (string or function), `showReferral?`, `showShareButtons?`, `showRewards?`, `snapchat?`, `onLoaded?`, `onError?`, `className?`. Reloads confirmation by hash; the host must gate it on appropriate payment status. The three `show*` options default to true; Snapchat defaults to false. |
@@ -688,7 +1115,7 @@ as the final order amount.
688
1115
  | `setAcceptedConditions(map)`, `acceptCondition(id, accepted)`, `setAddonDataCapture(map)` | Record condition acceptance and add-on answers. |
689
1116
  | `setWaiverSignature(index, signature, hold?)`, `holdWaiverSigning(index)` | Coordinate a signature save with the cart/holder it belongs to; release a hold when finished. Prefer `WaiverSigning` for standard UI. |
690
1117
  | `reloadCheckoutDetails()`, `syncCurrency(eventId, code?, options?)` | Retry unavailable details or switch/seed event currency. Refresh selection and prices after changing currency. |
691
- | `placeOrder(method?, options?)`, `runIdVerification(orderHash, options?)` | Place/pay; optionally run verification separately. `placeOrder` accepts `autoRunIdVerification`, `idVerification` and `claimCard` options. |
1118
+ | `placeOrder(method?, options?)`, `runIdVerification(orderHash, options?)` | Place/pay; optionally run verification separately with a host URL opener or takeover. See [host-owned ID verification](#host-owned-id-verification). `placeOrder` accepts `autoRunIdVerification`, `idVerification` and `claimCard` options. |
692
1119
  | `applyPromoCode(eventId, code)`, `attributeReferrer(eventId, referrer)`, `joinWaitingList(eventId, entry)` | Helpers return promises. Refetch catalog after a code changes availability. |
693
1120
  | `reset()`, `resetWhenSettled()` | Restart subject to payment-settlement protection; see [lifecycle](#checkout-lifecycle). |
694
1121
  | `snapshot`, `store`, `isLoggedIn` | Entire state, shared store, and whether the hook sees an authenticated or checkout buyer session. The latter does not imply private-account authority. |
@@ -721,11 +1148,13 @@ for a different gateway.
721
1148
 
722
1149
  The package re-exports the shared core: `ttf` is a configured domain facade,
723
1150
  `api` contains raw calls, `hooks` contains React bindings, and DTOs, logic,
724
- `createEngine` and `createCheckoutStore` are named exports. `ttf` calls return
725
- promises; they do not render UI or replace the engine's sequencing, payment
726
- ownership and preflight. Use them directly only when your application owns the
727
- corresponding workflow. The installed `dist/index.d.ts` includes the complete
728
- exported signatures. Avoid importing a private shared-core package.
1151
+ `createEngine` and `createCheckoutStore` are named exports. Network calls usually
1152
+ return promises; `ttf.events.filterAddonsForTickets` and
1153
+ `ttf.order.mapProfileToBilling` are synchronous helpers. They do not render UI
1154
+ or replace the engine's sequencing, payment ownership and preflight. Use them
1155
+ directly only when your application owns the corresponding workflow. The
1156
+ installed `dist/index.d.ts` includes the complete exported signatures. Avoid
1157
+ importing a private shared-core package.
729
1158
 
730
1159
  ```tsx
731
1160
  import { hooks } from 'tf-checkout-react'
@@ -819,7 +1248,7 @@ existing 1.x integration pinned until the new flow and backend are verified.
819
1248
  There is no automatic translation of old URLs, component props, account
820
1249
  sessions or persisted 1.x state.
821
1250
 
822
- | 1.x integration | 2.0.0-beta.25 approach |
1251
+ | 1.x integration | 2.0.0-beta.27 approach |
823
1252
  | --- | --- |
824
1253
  | `setConfigs({ BASE_URL, ... })` | `configureCheckout({ env` or `baseUrl`, `clientId`, `clientSecret: ''`, ... `})`; public client registration and session setup are required. |
825
1254
  | `TicketsContainer`, `SeatMapContainer`, `AddOnsContainer` | `EmbeddedCheckout` or `Tickets`, `SeatMap`, `AddOns` under `CheckoutProvider`. Adapt callbacks and navigation. |
@@ -832,6 +1261,25 @@ sessions or persisted 1.x state.
832
1261
  | Promo/access, waiting list, pixel and referral behavior | Re-check catalog metadata, code/attribution precedence, waiting-list contract and explicit tracking. Do not infer numeric `?r=`. |
833
1262
  | Legacy single-page state in localStorage | Use the 2.x checkout store/engine and server cart; do not copy sensitive state or assume 1.x persisted state can be resumed. |
834
1263
 
1264
+ ### Migrating layout callbacks
1265
+
1266
+ A callback name change can also change what the host receives. In particular,
1267
+ `onContinue` does not pass the old callback's response payload:
1268
+
1269
+ | 1.x usage | 2.x replacement |
1270
+ | --- | --- |
1271
+ | `TicketsContainer`'s `getTicketsLabel` | `Tickets`'s `buttonLabel`. |
1272
+ | `TicketsContainer`'s `onAddToCartSuccess(data)` | `Tickets.onContinue()` after reservation; read the current cart through `useCheckoutContext()` under the provider. |
1273
+ | `BillingInfoContainer`'s `handleSubmit(values, helpers, eventId, response)` | `BillingInfo.onContinue()` after validation and save; read the current billing and holders from the checkout context. Omit the callback for combined-form behavior. |
1274
+ | `AddOnsContainer` timer | Render `TimerBanner` with `useCheckoutContext().secondsLeft` in the host layout. |
1275
+ | Separate 1.x page URLs and localStorage state | Keep the 2.x provider mounted across host steps; implement route/refresh handling and verify any existing server cart in the current session. |
1276
+
1277
+ These are migration starting points, not prop-compatible aliases. In 2.x
1278
+ `eventId` is a string; convert a numeric ID before passing it. Old Material UI
1279
+ theme options, callback payloads and optional page behavior need explicit host
1280
+ work. For the two complete flows see [Choose a checkout layout](#choose-a-checkout-layout);
1281
+ for single-page and stepped alternatives see [Compose sections](#compose-sections).
1282
+
835
1283
  Compare the exact event's ticket types, dated pricing, seats, add-ons, holder
836
1284
  questions, conditional fields, consent, waivers, fees, currencies, payment
837
1285
  methods, account and post-purchase behavior before switching. Backend rollout