tf-checkout-react-native 2.0.0-beta.22 → 2.0.0-beta.24

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.
Files changed (3) hide show
  1. package/README.md +929 -318
  2. package/docs/API.md +602 -0
  3. package/package.json +3 -2
package/README.md CHANGED
@@ -1,384 +1,995 @@
1
- # tf-checkout-react-native
2
-
3
- The TicketFairy checkout for React Native. Drop in one component and you have
4
- ticket selection, add-ons, buyer details, payment, ID verification, and the
5
- receipt — or compose the sections yourself.
6
-
7
- Same API as [`tf-checkout-react`](https://github.com/theticketfairy/tf-shared-libraries/blob/main/packages/tf-checkout-react/README.md); the flow,
8
- validation, and gating rules are literally the same code.
9
-
10
- The 2.x source is maintained in [`tf-shared-libraries/packages/tf-checkout-react-native`](https://github.com/theticketfairy/tf-shared-libraries/tree/main/packages/tf-checkout-react-native).
11
- The core is private and bundled into both platform packages. Use React 18+ and
12
- React Query 5 on web; the native requirements below reflect its supported build.
13
- Never put a confidential OAuth client secret in browser or mobile code. Public
14
- checkout integrations use registered SDK client IDs and owned buyer sessions.
15
-
16
- ## Prepared native storage change — beta.22
17
-
18
- The prepared paired beta.22 prerelease requires an explicit synchronous credential-storage
19
- adapter. Native `configureCheckout` and its root `configure` alias no longer create
20
- an unencrypted MMKV store. The MMKV factory/default exports and peer dependency
21
- are removed. This is a native beta breaking change; web defaults are unchanged.
22
- See [beta.22 release preparation](../../docs/releases/2.0.0-beta.22.md) and
23
- [secure native setup and migration](../../docs/integration/native-session-storage.md).
24
-
25
- The replacement release is not published yet. The beta.21 receipt below remains
26
- the last verified registry checkpoint; its published artifact still has the older
27
- MMKV dependency. The following setup examples describe current source, not an
28
- in-place change to an already published version.
29
-
30
- ## Install
31
-
32
- ### Last verified paired prerelease 2.0.0-beta.21
33
-
34
- Both SDKs are registry-verified as `2.0.0-beta.21` from exact tagged shared source
35
- `d7de6a71058cc293e6d521d52b42d9911768d1c2`, through protected paired pipeline15.
36
- The [canonical receipt](https://github.com/theticketfairy/tf-shared-libraries/blob/main/docs/releases/2.0.0-beta.21.registry.json)
37
- records exact archive/member/SHA/SRI proof. Both stable `latest` tags are unchanged.
38
- Pin the exact version below; `@beta` follows the most recent prerelease. Publication
39
- does not establish API rollout, app delivery or device/provider acceptance.
40
-
41
- This version preserves quantities across rotating encrypted ticket IDs using
42
- the API's optional canonical `selectionKey`, while quantity/change APIs and cart
43
- requests retain the latest encrypted IDs. Deploy and verify the canonical
44
- base/dated key and dated numeric context before relying on that behavior:
45
- `display_price`, `price_with_fees`, `fee_included`, `price_currency` and
46
- `price_symbol`. No localized price parsing or base-price/cap substitution is
47
- used. Missing optional keys retain opaque-ID compatibility, without a
48
- rotation-retention claim. The [beta.21 notes](https://github.com/theticketfairy/tf-shared-libraries/blob/main/docs/releases/2.0.0-beta.21.md)
49
- and [app integration contract](https://github.com/theticketfairy/tf-shared-libraries/blob/main/docs/integration/app-sdk.md)
50
- record API rollout requirements and separate deployment/provider/device gates.
51
-
52
- Post-purchase upgrade waiting is a separate server-owned contract. Only valid
53
- change-options targets can expose Join or Leave; malformed target IDs, labels
54
- or existing entries are ignored. A sold-out row does not enable it. The backend
55
- must explicitly offer that organizer-configured flow; local receiver validation
56
- does not prove its deployed availability or a completed join.
57
-
58
- After the replacement paired prerelease is published:
59
-
60
- ```bash
61
- npx expo install tf-checkout-react-native@2.0.0-beta.22 \
62
- @tanstack/react-query \
63
- @stripe/stripe-react-native \
64
- @react-native-clipboard/clipboard \
65
- react-native-webview \
66
- react-native-render-html@6.3.4
1
+ # Ticket Fairy Checkout for React Native
2
+
3
+ Native React components and a headless checkout engine for selling Ticket Fairy
4
+ tickets. Start with `EmbeddedCheckout`, compose individual sections, or own the
5
+ presentation using `useCheckout` and the shared API. The package includes its
6
+ private shared core; hosts do not install `tf-checkout-shared`.
7
+
8
+ This reference is for **`tf-checkout-react-native@2.0.0-beta.24`**, a
9
+ prerelease with a different API from 1.x (including its `CheckoutV2`). Pin the
10
+ version: `latest` and `beta` tags can move; check `npm view tf-checkout-react-native dist-tags`.
11
+ The [API reference](https://unpkg.com/tf-checkout-react-native@2.0.0-beta.24/docs/API.md)
12
+ covers components and hooks; the [declarations](https://unpkg.com/tf-checkout-react-native@2.0.0-beta.24/dist/index.d.ts)
13
+ contain all types. The [web package](https://www.npmjs.com/package/tf-checkout-react/v/2.0.0-beta.24)
14
+ shares the engine but has different UI and ports. Availability depends on the
15
+ backend, event, gateway, account and device; installation does not establish
16
+ hosted-checkout parity or payment acceptance.
17
+
18
+ ## Contents
19
+
20
+ - [Requirements and installation](#requirements-and-installation)
21
+ - [Initialize secure storage and configure](#initialize-secure-storage-and-configure)
22
+ - [Embed a checkout](#embed-a-checkout)
23
+ - [Compose sections and navigation](#compose-sections-and-navigation)
24
+ - [Checkout section reference](#checkout-section-reference)
25
+ - [Tickets, seats, codes and attribution](#tickets-seats-codes-and-attribution)
26
+ - [Billing, custom fields and consent](#billing-custom-fields-and-consent)
27
+ - [Payments and safe completion](#payments-and-safe-completion)
28
+ - [Headless checkout](#headless-checkout)
29
+ - [Hooks and API](#hooks-and-api)
30
+ - [Authentication and account components](#authentication-and-account-components)
31
+ - [Theme, primitives and localization](#theme-primitives-and-localization)
32
+ - [Migration from 1.x](#migration-from-1x)
33
+ - [Testing and troubleshooting](#testing-and-troubleshooting)
34
+
35
+ ## Requirements and installation
36
+
37
+ The package declares Node `>=20.19.4`, React `>=19.1`, React Native `>=0.81`,
38
+ `@tanstack/react-query >=5`, `@stripe/stripe-react-native >=0.45.0`,
39
+ `@react-native-clipboard/clipboard >=1.14.0`, `react-native-webview >=13.15.0`,
40
+ and `react-native-render-html >=6.3.4 <7` as host requirements or peers. The
41
+ supported RN 0.81 cohort uses the New Architecture, iOS 15.1+ and Android API
42
+ 24+. Check the native dependencies' own requirements and the versions supported
43
+ by your app; a peer range is not proof that every combination has been tested.
44
+ The package itself declares `axios`, `entities` and `htmlparser2` as runtime
45
+ dependencies. Use one copy of React, React Query and each native module in the
46
+ host. `npm ls react @tanstack/react-query @stripe/stripe-react-native` helps
47
+ identify duplicates.
48
+
49
+ For an Expo project with compatible React and RN versions:
50
+
51
+ ```sh
52
+ npx expo install tf-checkout-react-native@2.0.0-beta.24 \
53
+ @tanstack/react-query @stripe/stripe-react-native \
54
+ @react-native-clipboard/clipboard react-native-webview \
55
+ react-native-render-html@6.3.4 expo-secure-store
67
56
  ```
68
57
 
69
- Bare React Native:
70
-
71
- ```bash
72
- npm install tf-checkout-react-native@2.0.0-beta.22 @tanstack/react-query @stripe/stripe-react-native @react-native-clipboard/clipboard react-native-webview react-native-render-html@6.3.4
73
- ```
74
-
75
- These are **peer dependencies** on purpose. React contexts and native
76
- modules must exist exactly once in an app — a second copy of react-query means
77
- your `<QueryClientProvider>` is invisible to our hooks, and a second Stripe
78
- means a duplicate pod. As peers there is one copy and no Metro aliasing is ever
79
- needed.
80
-
81
- Everything else — the SDK core, the flow, the UI — ships inside the package.
82
-
83
- Beta.21 retains the `react-native-render-html` peer `>=6.3.4 <7`
84
- for formatted ticket, add-on, bundle and accommodation descriptions and its
85
- public `RichText` renderer. Development and component tests use `6.3.4`; the
86
- install commands above pin that compatible version. See [native rich content](../../docs/integration/native-rich-content.md)
87
- for the rendering contract and pending acceptance coverage.
88
-
89
- ## Requirements
90
-
91
- - React Native `0.81+` with the New Architecture enabled (the supported build cohort)
92
- - React `19.1+`, Node `20.19.4+`
93
- - iOS 15.1+ · Android API 24+ (Stripe SDK minimums)
94
- - A development build. **Expo Go will not work** — this package uses native
95
- modules.
96
-
97
- Add the Stripe plugin to `app.json`:
58
+ The last package is used by the storage example below; the SDK does not install
59
+ or require Expo SecureStore. Build a development client containing the native
60
+ modules. Complete checkout compatibility with Expo Go is not established. Add
61
+ the Stripe config plugin to the app configuration, choosing wallet settings for
62
+ your own app, and rebuild after changing native configuration. For example,
63
+ this starts without Google Pay:
98
64
 
99
65
  ```json
100
66
  {
101
67
  "expo": {
68
+ "scheme": "myapp",
102
69
  "plugins": [["@stripe/stripe-react-native", { "enableGooglePay": false }]]
103
70
  }
104
71
  }
105
72
  ```
106
73
 
107
- Then build a dev client once:
74
+ Run `npx expo run:ios` or `npx expo run:android`, or make a development build
75
+ using your normal process. For a bare React Native app, install the peers in
76
+ the app (React and React Native are normally already present):
108
77
 
109
- ```bash
110
- npx expo run:ios
78
+ ```sh
79
+ npm install tf-checkout-react-native@2.0.0-beta.24 @tanstack/react-query \
80
+ @stripe/stripe-react-native @react-native-clipboard/clipboard \
81
+ react-native-webview react-native-render-html@6.3.4
111
82
  ```
112
83
 
113
- ## Use it
84
+ Configure autolinking and Stripe's native setup, run `pod install` in the iOS
85
+ directory, and rebuild both platforms. Provide a Keychain/Keystore-backed
86
+ storage adapter using a native library appropriate to the host. Consult the
87
+ [Stripe React Native setup](https://docs.stripe.com/payments/accept-a-payment?platform=react-native)
88
+ and [Expo SecureStore documentation](https://docs.expo.dev/versions/latest/sdk/securestore/)
89
+ for native configuration and platform behavior. Configure wallet entitlements,
90
+ merchant registration and deep links separately if those features are used.
91
+
92
+ ## Initialize secure storage and configure
93
+
94
+ `configureCheckout(config)` is synchronous and must run **after credential
95
+ storage is restored**, before any SDK API call, hook or component mounts. Its
96
+ root export alias `configure` has the same native requirements. Configuration
97
+ is app-wide. Set up one environment and buyer session at a time; avoid
98
+ reconfiguring during checkout or payment.
99
+
100
+ `storage` is required. It has synchronous `getItem(key): string | null |
101
+ undefined`, `setItem(key, value): void`, `removeItem(key): void`, and optional
102
+ `flush(): Promise<void>`. The wrapper checks method shape; it does not certify
103
+ encryption, persistence, isolation, backup or correctness. `createMemoryStorage`
104
+ is exported for tests and loses credentials at process exit; it is not a
105
+ production secure store. Use separate storage namespaces per app/environment.
106
+ Never put a confidential OAuth client secret or provider secret key in mobile
107
+ code. Obtain a public SDK client ID authorized for the integration and use an
108
+ empty `clientSecret` for public checkout; do not substitute a confidential
109
+ credential. Environment variables compiled into an app are visible to its users.
110
+
111
+ The SDK reads storage synchronously. The following Expo example asynchronously
112
+ restores one protected snapshot first, then makes each subsequent OS write
113
+ synchronously *before* publishing the new in-memory snapshot. The methods are
114
+ not `async` and a failed write throws instead of reporting success. Replace the
115
+ namespace with an app-owned, environment-specific key.
114
116
 
115
- ```tsx
116
- import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
117
- import { configureCheckout, EmbeddedCheckout } from 'tf-checkout-react-native'
118
- import { restoreCredentialStorage } from './credentialStorage'
119
-
120
- // Await this in the host's startup gate before rendering App or any SDK hook.
121
- export async function prepareCheckout() {
122
- const storage = await restoreCredentialStorage('my_app_checkout_prod')
123
- configureCheckout({
124
- env: 'PROD',
125
- clientId: 'PUBLIC_SDK_CLIENT_ID',
126
- clientSecret: '',
127
- storage,
128
- })
117
+ ```ts
118
+ // credentialStorage.ts
119
+ import * as SecureStore from 'expo-secure-store'
120
+ import type { StorageAdapter } from 'tf-checkout-react-native'
121
+
122
+ export async function restoreCredentialStorage(key: string): Promise<StorageAdapter> {
123
+ const raw = await SecureStore.getItemAsync(key)
124
+ const parsed: unknown = raw === null ? {} : JSON.parse(raw)
125
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed) ||
126
+ Object.values(parsed).some(value => typeof value !== 'string')) {
127
+ throw new Error('Could not restore checkout credentials')
128
+ }
129
+ let values = new Map(Object.entries(parsed as Record<string, string>))
130
+ const commit = (next: Map<string, string>) => {
131
+ SecureStore.setItem(key, JSON.stringify(Object.fromEntries(next)), {
132
+ keychainAccessible: SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
133
+ requireAuthentication: false,
134
+ })
135
+ values = next
136
+ }
137
+ return {
138
+ getItem: name => values.get(name) ?? null,
139
+ setItem: (name, value) => { const next = new Map(values); next.set(name, value); commit(next) },
140
+ removeItem: name => { const next = new Map(values); next.delete(name); commit(next) },
141
+ }
129
142
  }
143
+ ```
130
144
 
131
- const queryClient = new QueryClient()
145
+ This illustrative adapter does not request biometric authentication. Account
146
+ for OS storage size limits, backup/restore policy, device locking and write
147
+ failures in the host; synchronous storage can block the JS thread. If your
148
+ persistence API is asynchronous, hydrate a synchronous mirror before setup,
149
+ serialize its writes, implement `flush`, and await the final flush after every
150
+ credential-changing operation before telling the user that persistence
151
+ succeeded. Built-in auth completion does not await that final flush for you.
152
+ Do not pass promises or async functions as the three synchronous methods. A
153
+ corrupt snapshot or unavailable OS store must be handled explicitly; do not
154
+ silently report durable sign-in. Migrate old credentials securely or require
155
+ sign-in again.
132
156
 
133
- export default function App() {
134
- return (
135
- <QueryClientProvider client={queryClient}>
136
- <EmbeddedCheckout eventId="14304" />
137
- </QueryClientProvider>
138
- )
157
+ ```ts
158
+ // checkoutSetup.ts
159
+ import { configureCheckout } from 'tf-checkout-react-native'
160
+ import { restoreCredentialStorage } from './credentialStorage'
161
+
162
+ let initialization: Promise<void> | undefined
163
+ export function prepareCheckout(): Promise<void> {
164
+ if (!initialization) {
165
+ initialization = (async () => {
166
+ const storage = await restoreCredentialStorage('my_app_checkout_prod')
167
+ configureCheckout({
168
+ env: 'PROD',
169
+ clientId: 'YOUR_PUBLIC_SDK_CLIENT_ID',
170
+ clientSecret: '',
171
+ storage,
172
+ })
173
+ })().catch(error => { initialization = undefined; throw error })
174
+ }
175
+ return initialization
139
176
  }
140
177
  ```
141
178
 
142
- Copy the [Expo example's credential-storage mirror](example/src/credentialStorage.ts)
143
- or supply your own OS-protected adapter. The [complete example startup gate](example/src/app/_layout.tsx)
144
- waits for restoration before mounting checkout and offers retry on failure.
145
- Install `expo-secure-store` only if your host uses that example; the SDK adds no
146
- Expo storage peer. Bare React Native hosts can use their own Keychain/Keystore
147
- integration through the same `StorageAdapter` contract. The backend still validates
148
- every restored session. Setup does not read/probe storage or prove its security.
149
- The runnable example uses actual synchronous OS writes after hydration. If your
150
- host chooses asynchronous writes instead, built-in auth completion does not await
151
- the final flush automatically; follow the [host completion contract](../../docs/integration/native-session-storage.md).
179
+ Configuration fields:
152
180
 
153
- ### Checkout sessions and account scope
181
+ | Field | Meaning |
182
+ | --- | --- |
183
+ | `clientId`, `clientSecret` | Required strings. Public checkout uses a registered public SDK ID and empty secret. |
184
+ | `storage` | Required host-owned synchronous `StorageAdapter`, restored before setup. |
185
+ | `env` | `PROD` (default, `www.ticketfairy.com`), `STAG` (`test.ticketfairy.com`) or `DEV` (`sandbox.ticketfairy.com`). |
186
+ | `baseUrl` | Optional host origin overriding `env`; the SDK appends `/api`. Use only a trusted host. |
187
+ | `sourceOrigin` | Optional `X-Source-Origin` header value. |
188
+ | `auth` | Optional synchronous callback returning additional request headers. Treat credentials in headers as sensitive; the native wrapper also forces header-only session mode and no-store/no-cache. |
189
+ | `cookieAuth` | Defaults to `true` on native. The native wrapper forces `withCredentials: false`; its header-backed session is stored via the adapter, rather than relying on ambient OS cookies. |
190
+ | `accountAuth` | Opt in only for an authorized first-party full-account journey and deployed backend. Defaults off. |
191
+ | `browserAuth`, `sdkCheckoutBrowserAuth` | Optional platform ports for supported browser authentication. The host owns secure proof generation and browser/callback handling. |
192
+ | `sdkCheckoutPhoneAuth` | Opt in only when the corresponding first-party checkout phone journey is deployed. |
154
193
 
155
- The native wrapper defaults `cookieAuth` to true and disables ambient native
156
- cookie handling. Owned credentials use the SDK's header-backed session storage.
157
- Public checkout sign-in preserves the event-bound guest carrier and keeps its
158
- buyer credential server-side; it does not return a full-account ticket or grant
159
- private account access. First-party full-account flows explicitly use
160
- `accountAuth: true`; see the shared identity contract below.
194
+ A restored session is still validated by the backend. Public checkout sign-in
195
+ preserves the event-bound guest cart and keeps the checkout buyer credential
196
+ server-side; it does not return a full-account ticket or grant private account
197
+ access. Do not infer full account authorization from checkout authentication.
161
198
 
162
- ### Environments
199
+ ## Embed a checkout
163
200
 
164
- ```ts
165
- export async function prepareStagingCheckout() {
166
- const storage = await restoreCredentialStorage('my_app_checkout_stag')
167
- configureCheckout({
168
- env: 'STAG',
169
- clientId: 'PUBLIC_SDK_CLIENT_ID',
170
- clientSecret: '',
171
- storage,
172
- })
173
- }
174
- ```
175
-
176
- `PROD` → `www.ticketfairy.com`, `STAG` → `test.ticketfairy.com`, `DEV` →
177
- `sandbox.ticketfairy.com`. Pass `baseUrl` to point elsewhere.
178
-
179
- ## `<EmbeddedCheckout />`
180
-
181
- | Prop | Type | Default | Does |
182
- | ----------------------------------- | ----------------------------------- | ----------- | --------------------------------------------- |
183
- | `eventId` | `string` | — | The event to sell. Required. |
184
- | `onComplete` | `(order: PlaceOrderResult) => void` | — | Fires once payment settles. |
185
- | `confirmationMode` | `'inline' \| 'redirect'` | `'inline'` | `redirect` leaves routing to `onComplete`. |
186
- | `initialView` | `'tickets' \| 'seats'` | `'tickets'` | Opening screen. |
187
- | `onViewChange` | `(view) => void` | — | Reports the visible view. |
188
- | `requireEmailConfirmation` | `boolean` | `true` | Make guests repeat their email. |
189
- | `collectTicketHolders` | `boolean` | `true` | One named holder per ticket. |
190
- | `collectHolderContact` | `boolean` | `true` | Also collect holder email/phone. |
191
- | `skipBillingWhenProfileComplete` | `boolean` | `false` | Skip billing for complete signed-in profiles. |
192
- | `auth` | `false \| AuthOptions` | built-in | Configure or remove the login modal. |
193
- | `onLogin` | `(email?: string) => void` | — | Open your own login UI instead. |
194
- | `privacyPolicy` | `CheckoutPrivacyPolicy` | — | Your privacy policy link and brand name. |
195
- | `additionalConditions` | `EventCondition[]` | — | Extra acknowledgements. |
196
- | `ttfOptInLabel` / `brandOptInLabel` | `string` | — | Override the marketing opt-in copy. |
197
- | `initialPromoCode` | `string` | — | Apply a promo code on load. |
198
- | `referrer` | `string \| number` | — | Attribute the sale. |
199
- | `showTestCardHint` | `boolean` | `false` | Show sandbox card copy. Demos only. |
200
- | `theme` | `CheckoutTheme` | — | Recolor via design tokens. |
201
-
202
- `readAttributionFromUrl` is accepted for parity with web and is a no-op —
203
- there is no `window.location` on a device.
204
-
205
- ## Theming
201
+ Mount only after `prepareCheckout` resolves. `EmbeddedCheckout` provides its own
202
+ `CheckoutProvider` and React Query client. Any separate React Query hooks outside
203
+ that subtree need a `QueryClientProvider` of their own.
206
204
 
207
205
  ```tsx
208
- <EmbeddedCheckout
209
- eventId="14304"
210
- theme={{ colorScheme: 'dark', accent: '#ff2d55', radiusCard: 16 }}
211
- />
206
+ import React, { useEffect, useState } from 'react'
207
+ import { Button, Text, View } from 'react-native'
208
+ import { EmbeddedCheckout, type PlaceOrderResult } from 'tf-checkout-react-native'
209
+ import { prepareCheckout } from './checkoutSetup'
210
+
211
+ export default function EventCheckout() {
212
+ const [status, setStatus] = useState<'loading' | 'ready' | 'error'>('loading')
213
+ const [attempt, setAttempt] = useState(0)
214
+ const [order, setOrder] = useState<PlaceOrderResult | null>(null)
215
+ useEffect(() => {
216
+ let active = true
217
+ prepareCheckout().then(() => { if (active) setStatus('ready') })
218
+ .catch(() => { if (active) setStatus('error') })
219
+ return () => { active = false }
220
+ }, [attempt])
221
+
222
+ if (status === 'error') return (
223
+ <View><Text>Could not prepare checkout.</Text>
224
+ <Button title="Try again" onPress={() => { setStatus('loading'); setAttempt(n => n + 1) }} />
225
+ </View>
226
+ )
227
+ if (status !== 'ready') return <Text>Preparing checkout…</Text>
228
+
229
+ if (order) return (
230
+ <View>
231
+ <Text>Order submitted: {order.orderHash}. Check payment status before fulfillment.</Text>
232
+ {order.pending && <Text>Payment pending.</Text>}
233
+ {order.pendingVoucherUrl && <Text selectable>{order.pendingVoucherUrl}</Text>}
234
+ {order.needIdVerification && <Text>Identity verification is required.</Text>}
235
+ {order.free && <Text>Check if registration needs approval or verification.</Text>}
236
+ </View>
237
+ )
238
+ return <EmbeddedCheckout eventId="YOUR_EVENT_ID" confirmationMode="redirect"
239
+ onComplete={setOrder} />
240
+ }
212
241
  ```
213
242
 
214
- The same token names as web: color (`accent`, `surface`, `text`, `muted`,
215
- `border`, `error`, `success`), shape, and typography. Read them anywhere with
216
- `useTheme()`.
217
-
218
- ## Composing your own layout
219
-
220
- Every section reads the same `useCheckout()` store from the provider.
243
+ `onComplete` is an order result, not proof of settled payment. `pending: true`
244
+ requires pending instructions, but `pending !== true` also does not prove
245
+ capture or settlement. `confirmationMode="inline"` (default) shows the stock
246
+ receipt, which can say tickets are confirmed for a non-pending order before
247
+ final capture/settlement. Hosts requiring verified paid status should use
248
+ `"redirect"` or custom UI and reconcile with the authoritative backend and
249
+ provider before presenting that status or fulfilling. Redirect mode delegates
250
+ routing to `onComplete` and keeps checkout mounted during handoff; the host owns
251
+ navigation. The example shows a local fallback, not a complete status screen:
252
+ the host must implement reconciliation, retain the order reference and voucher
253
+ across its navigation/restart, and arrange verification when required. Validate
254
+ provider URLs against the host's policy before opening them.
255
+
256
+ ### `EmbeddedCheckout` props
257
+
258
+ | Prop | Default | Purpose |
259
+ | --- | --- | --- |
260
+ | `eventId: string` | required | Event to sell. |
261
+ | `onComplete?: (order: PlaceOrderResult) => void` | — | Reports ticket or bundle result; reconcile payment status, even if not marked pending. |
262
+ | `confirmationMode?: 'inline' \| 'redirect'` | `inline` | Show receipt inline or let host route. |
263
+ | `confirmationChildren?: ReactNode` | — | Content inside the inline ticket receipt, e.g. `Rewards`. |
264
+ | `initialView?: 'tickets' \| 'seats'` | `tickets` | Initial selection screen. |
265
+ | `onViewChange?: (view) => void` | — | Reports `tickets`, `seats` or `checkout`. |
266
+ | `requireEmailConfirmation?: boolean` | `true` | Require matching guest email confirmation. |
267
+ | `collectTicketHolders?: boolean` | `true` | Collect one named holder per ticket. |
268
+ | `collectHolderContact?: boolean` | `true` | Also collect holder email/phone when holder details are shown. |
269
+ | `skipBillingWhenProfileComplete?: boolean` | `false` | May hide billing for a complete profile; see the privacy-policy interaction below. |
270
+ | `onLogin?: (email?: string) => void` | — | Use host login instead of the built-in modal. |
271
+ | `auth?: false \| object` | built-in | `false` removes built-in auth; otherwise supports `allowRegistration`, `allowPasswordReset`, `requireRegistrationAddress`, `logoUrl`, `onCheckoutAuthenticated` and `onAuthenticated`. `onLogin` takes precedence. |
272
+ | `privacyPolicy?: CheckoutPrivacyPolicy` | — | Host policy: `{ url, brandName, linkLabel? }`; requires acknowledgment. |
273
+ | `additionalConditions?: EventCondition[]` | `[]` | Extra required/optional acknowledgment blocks. |
274
+ | `ttfOptInLabel`, `brandOptInLabel` | SDK copy / absent | Label overrides; `privacyPolicy` reuses the Ticket Fairy opt-in value. See consent limitations below. |
275
+ | `initialPromoCode?: string`, `referrer?: string \| number` | — | Explicit attribution supplied by the host. |
276
+ | `readAttributionFromUrl?: boolean` | accepted, no-op | Native has no browser URL to read. |
277
+ | `pickImage?: CheckoutImagePicker` | — | Host OS image picker for applicable questions. |
278
+ | `showTestCardHint?: boolean` | `false` | Sandbox demo copy only; keep off in production. |
279
+ | `theme?: CheckoutTheme` | light tokens | Partial token overrides. |
280
+
281
+ `pickImage` is `() => Promise<{ base64: string; width?: number; height?: number } | null>`.
282
+ Return raw base64, not a data URI, or `null` for cancellation. The SDK checks
283
+ base64 encoding and the image signature, the 2 MiB maximum and any supplied
284
+ dimensions (at most 4096 pixels per side); the server validates decoded bytes.
285
+ The host owns permission and picker UX. For the separate event discussion picker, see its props below.
286
+
287
+ ## Compose sections and navigation
288
+
289
+ `CheckoutProvider` owns a native `useCheckout()` instance, a query provider,
290
+ theme and the built-in provider-page and installment modals. Sections inside
291
+ it share the app checkout store. `useCheckoutContext()` reads this instance and
292
+ throws outside the provider. The following intentionally uses one combined
293
+ billing/payment form after reservation; callbacks route tickets and seats.
221
294
 
222
295
  ```tsx
296
+ import React, { useState } from 'react'
297
+ import { ScrollView, Text, View } from 'react-native'
223
298
  import {
224
- CheckoutProvider,
225
- Tickets,
226
- AddOns,
227
- BillingInfo,
228
- Payment,
229
- OrderSummary,
230
- useCheckout,
299
+ CheckoutProvider, Tickets, SeatMap, AddOns, BillingInfo, WaiverSigning,
300
+ OrderSummary, Payment, IDVerification, useCheckoutContext,
301
+ type PlaceOrderResult,
231
302
  } from 'tf-checkout-react-native'
232
303
 
233
- const MyCheckout = () => (
234
- <CheckoutProvider theme={{ colorScheme: 'dark' }}>
235
- <ScrollView>
236
- <Tickets />
237
- <AddOns />
238
- <BillingInfo />
239
- <OrderSummary />
240
- <Payment />
304
+ function Flow({ eventId, onComplete }: {
305
+ eventId: string
306
+ onComplete: (order: PlaceOrderResult) => void
307
+ }) {
308
+ const [view, setView] = useState<'tickets' | 'seats' | 'checkout'>('tickets')
309
+ const checkout = useCheckoutContext()
310
+ if (checkout.status === 'succeeded' && checkout.order) return (
311
+ <View>
312
+ <Text>Order submitted: {checkout.order.orderHash}. Check payment status.</Text>
313
+ {checkout.order.pending && <Text>Payment pending.</Text>}
314
+ {checkout.order.pendingVoucherUrl && <Text selectable>{checkout.order.pendingVoucherUrl}</Text>}
315
+ <IDVerification />
316
+ </View>
317
+ )
318
+ return (
319
+ <ScrollView keyboardShouldPersistTaps="handled">
320
+ {view === 'tickets' && <Tickets eventId={eventId}
321
+ onSelectSeats={() => setView('seats')}
322
+ onContinue={() => setView('checkout')} />}
323
+ {view === 'seats' && <SeatMap eventId={eventId}
324
+ onContinue={() => setView('checkout')} />}
325
+ {view === 'checkout' && <>
326
+ <AddOns eventId={eventId} />
327
+ <BillingInfo eventId={eventId} requireEmailConfirmation
328
+ collectTicketHolders collectHolderContact />
329
+ <WaiverSigning eventId={eventId} />
330
+ <OrderSummary eventId={eventId} />
331
+ <Payment requireEmailConfirmation collectTicketHolders requireHolderContact
332
+ onComplete={onComplete} />
333
+ </>}
241
334
  </ScrollView>
242
- </CheckoutProvider>
243
- )
244
- ```
245
-
246
- **Sections** — `Tickets`, `AddOns`, `BillingInfo`, `OrderSummary`, `Payment`,
247
- `IDVerification`, `Confirmation`.
248
-
249
- **UI primitives** — `Card`, `Button`, `BodyText`, `MutedText`, `Field`,
250
- `Select`, `QuantitySelect`, `PaymentMethods`, `Timer`, `TimerBanner`.
251
-
252
- **Auth** — `AuthModal`, `LoginForm`, `RegisterForm`, `ForgotPasswordForm`,
253
- `ResetPasswordForm`.
335
+ )
336
+ }
254
337
 
255
- ## Headless
338
+ export function ComposedCheckout({ eventId, onComplete }: {
339
+ eventId: string
340
+ onComplete: (order: PlaceOrderResult) => void
341
+ }) {
342
+ return <CheckoutProvider><Flow eventId={eventId} onComplete={onComplete} /></CheckoutProvider>
343
+ }
344
+ ```
256
345
 
257
- `useCheckout()` gives you the whole flow with no UI.
346
+ `AddOns` can render nothing when there are no applicable items; do not make a
347
+ navigation transition depend solely on its `onContinue` unless your host handles
348
+ that case. With no `onContinue`, `AddOns` and `BillingInfo` use combined-form
349
+ behavior. Match `BillingInfo`'s email/holder policy to `Payment`'s
350
+ `requireEmailConfirmation`, `collectTicketHolders` and `requireHolderContact`.
351
+ The composed example renders the verification control for a required order;
352
+ the buyer must press Verify. Before changing event, leaving payment or starting
353
+ another cart, handle in-flight and retained orders as described under
354
+ [safe completion](#payments-and-safe-completion).
355
+
356
+ `CheckoutProvider` props: `children` (required), `theme`, `pickImage`,
357
+ `options` (wallet/redirect `CheckoutOptions`), `paymentSheet`, `queryClient`,
358
+ and `value`. Omit `value` to let the provider create the native hook, provider
359
+ page and installment picker. If supplying `value`, create it with the native
360
+ `useCheckout` and provide any necessary ports yourself; `options` and
361
+ `paymentSheet` on a controlled provider do not configure that value. Keep the
362
+ choice of controlled versus self-provided stable for the mount. A custom query
363
+ client can be supplied with `queryClient`. `CheckoutThemeProvider` supplies only
364
+ theme, not checkout state or a query client.
365
+
366
+ ## Checkout section reference
367
+
368
+ Unless noted, these sections read checkout state from `CheckoutProvider` and
369
+ are imported from `tf-checkout-react-native`. All props are optional except
370
+ those marked **required**. Prop interfaces are represented on the component in
371
+ the published declarations; not every `*Props` name is itself a top-level type
372
+ export. Derive one if needed with `React.ComponentProps<typeof Tickets>`.
373
+
374
+ ### `Tickets`
375
+
376
+ Loads the catalog, quantities, promo/access-code and waiting-list controls,
377
+ currency and time slots when offered. Reserves/prices the cart before calling
378
+ `onContinue`.
379
+
380
+ | Prop | Description |
381
+ | --- | --- |
382
+ | `eventId: string` | **Required** event ID. |
383
+ | `onContinue?: () => void` | Cart was primed; reveal the next screen. |
384
+ | `onSelectSeats?: () => void` | Show a seat map for seated events. Without it the seat CTA is hidden. |
385
+ | `title?: string \| false` | Catalog heading; default `false`. |
386
+ | `buttonLabel?: string` | Reserve CTA; default `Get tickets`. |
387
+ | `timeSlotDates?: string[]` | Force time-slot selection with ISO `yyyy-mm-dd` dates; omit to use event data. |
388
+ | `initialPromoCode?: string`, `referrer?: string \| number` | Apply/attribute once on mount. |
389
+ | `readAttributionFromUrl?: boolean` | Accepted for web parity; does nothing on native. |
390
+ | `unstyled?: boolean` | Place catalog in the host's own container. |
391
+ | `renderTicket?: (row) => ReactNode` | Replace row visuals. `row` has `ticket`, `quantity`, `range: {min,max,step}`, `unavailable`, `seated`, `onQuantityChange`. |
392
+ | `renderContinue?: (cta) => ReactNode` | Replace CTA; `cta` has `onPress`, `disabled`, `loading`, `title`. |
393
+
394
+ The render callbacks retain SDK selection/reservation logic. Preserve the
395
+ provided disabled/loading state and accessible labels in custom UI.
396
+
397
+ ### `SeatMap`, `AddOns`, `Bundles`, `Accommodation`
398
+
399
+ | Component | Props and behavior |
400
+ | --- | --- |
401
+ | `SeatMap` | **`eventId`**; `onContinue?` after a seat cart is primed. Internal or Seats.io maps render through WebView. The event and backend must support the chart, hold and cart contracts. |
402
+ | `AddOns` | **`eventId`**; `onContinue?`, `onSkip?`, `buttonLabel?`, `eyebrow?`, `heading?`. Saves quantities and add-on answers; skip is hidden when a required add-on exists. It may render nothing. |
403
+ | `Bundles` | **`eventId`**; `onPurchased?(orderHash)`, `onComplete?(order)`, `confirmationMode?: 'inline' \| 'redirect'`, `visible?`, `heading?`, `locale?`, `messages?`, `translate?`. Bundles use their own purchase/order flow, not the ticket cart. Set `visible={false}` while hidden so its card field is not mounted. |
404
+ | `Accommodation` | **`eventId`**. Composes stay selection, quote and reservation state for supported events. Availability and authoritative prices come from the server. |
405
+
406
+ ### `BillingInfo`
407
+
408
+ Collects buyer, order-level and ticket-holder answers, then saves them into the
409
+ shared checkout. With `onContinue`, it has a continue button; without it, edits
410
+ are committed in the combined form. It uses event gates and may show additional
411
+ required fields. All props are optional:
412
+
413
+ | Prop | Description |
414
+ | --- | --- |
415
+ | `eventId?: string` | Event context for country defaults. |
416
+ | `onContinue?: () => void` | Called after valid billing is saved. |
417
+ | `heading?`, `buttonLabel?` | Copy overrides. |
418
+ | `onLogin?: (email?: string) => void` | Offer sign-in, including an email already associated with an account. |
419
+ | `skipWhenProfileComplete?: boolean` | May hide the card for a complete signed-in profile; default `false`. Do not combine with `privacyPolicy` without a verified workaround. |
420
+ | `requireEmailConfirmation?: boolean` | Require guest email confirmation; standalone default `false`. |
421
+ | `collectTicketHolders?: boolean` | Require named holders beyond event gates; standalone default `false`. |
422
+ | `collectHolderContact?: boolean` | Collect holder email and phone when holders are shown; standalone default `false`. |
423
+ | `ttfOptInLabel?`, `brandOptInLabel?` | Label overrides; privacy and Ticket Fairy opt-in share a value when `privacyPolicy` is supplied. |
424
+ | `privacyPolicy?: { url: string; brandName: string; linkLabel?: string }` | Host policy and required acknowledgment. |
425
+ | `showPrivacyError?: boolean` | Show inline privacy error after a failed attempt. |
426
+ | `step?: ReactNode` | Accepted for API parity but not rendered by native `SectionHeader`. |
427
+
428
+ ### `OrderSummary`, `WaiverSigning`, `Payment`, `IDVerification`, `Confirmation`
429
+
430
+ | Component | Props and behavior |
431
+ | --- | --- |
432
+ | `OrderSummary` | `eventId?`, `locale?`, `messages?`, `translate?`. Displays server-priced cart totals and optional event name. |
433
+ | `WaiverSigning` | **`eventId`**; `locale?`, `messages?`, `translate?`. Renders only when the event has a waiver; supports the server-offered signing methods. |
434
+ | `Payment` | `onComplete?(order)`, `eventId?` (currently unused by native payment), `requireEmailConfirmation?`, `collectTicketHolders?`, `requireHolderContact?`, `additionalConditions?`, `requirePrivacyPolicy?`, `onPrivacyRequired?`, `showTestCardHint?`, `locale?`, `messages?`, `translate?`. Standalone boolean defaults are false. It validates, renders eligible methods and submits. |
435
+ | `IDVerification` | No props. When required, it shows a Verify button; the buyer must press it to open and poll the provider. A stall proves neither failure nor settlement. |
436
+ | `Confirmation` | `shareUrl?`, `shareMessage?`, `viewOrderUrl?`, `message?`, `children?`. Reads the current order. Its stock title and share copy do not independently verify payment settlement or referral eligibility; see below. |
437
+
438
+ `Payment` gates its rendered event conditions, host `additionalConditions` and
439
+ requested privacy acknowledgment in its UI. Each `EventCondition` is
440
+ `{ id: string, name: string, content: string, isRequired: boolean }`. These
441
+ acknowledgments are client-side and are not sent as separate server fields.
442
+ Conditions use native `RichText`, which filters tags/styles and opens only
443
+ HTTP(S) links; its rendering is not browser HTML parity. A headless
444
+ `placeOrder` checks loaded event conditions in the engine, but does not receive
445
+ host `additionalConditions` or enforce the UI privacy gate. Headless hosts must
446
+ implement and test their own required host acknowledgments before placement.
447
+
448
+ `Rewards` renders order-scoped rewards, a referral tree, ladder, packs and a
449
+ native share sheet. Pass `orderHash` or an already loaded `data` payload; it also
450
+ accepts `locale`, `messages`, `translate`. Embed it as a `Confirmation` child,
451
+ or use `confirmationChildren` on `EmbeddedCheckout`. `RewardsTree` accepts
452
+ **`tree`** and optional `size` (default 260); `RewardsLadder` accepts **`rows`**,
453
+ **`translate`** and optional async `onSubmitDetails(awardId, details)`.
454
+ Account-level `AccountRewardsSection` has separate read-only account semantics.
455
+ `Rewards` uses the server's `share_link` and hides itself when
456
+ `referrals_enabled` is explicitly false. Verify the returned eligibility and
457
+ buyer-specific link against your deployed contract. Standalone `Confirmation`
458
+ displays reward-promising share copy for any non-pending order when `shareUrl`
459
+ is supplied, without checking eligibility or generating a buyer-specific link.
460
+ Omit it unless your host has verified both; `EmbeddedCheckout` does not expose a
461
+ `shareUrl` prop. The example below omits that block and assumes the host has
462
+ already verified the status required to show confirmed receipt copy, and the
463
+ original result was not pending. `Confirmation` reads the original
464
+ `checkout.order.pending`; it does not refresh that flag after later settlement.
465
+ Use a host status UI based on a fresh authoritative read for an asynchronously
466
+ settled order.
258
467
 
259
468
  ```tsx
260
- const checkout = useCheckout({ eventId: '14304' })
469
+ import { Confirmation, Rewards, useCheckoutContext } from 'tf-checkout-react-native'
261
470
 
262
- await checkout.setTickets('14304', [{ ticketTypeId: '1', quantity: 2 }])
263
- checkout.setBilling({ firstName, lastName, email, phone })
264
- checkout.setConsent({ ttfOptIn: true, brandOptIn: false })
265
- const order = await checkout.placeOrder('card')
471
+ function Receipt({ verified }: { verified: boolean }) {
472
+ const checkout = useCheckoutContext()
473
+ if (!verified || checkout.order?.pending) return null
474
+ return (
475
+ <Confirmation>
476
+ {checkout.order?.orderHash ? <Rewards orderHash={checkout.order.orderHash} /> : null}
477
+ </Confirmation>
478
+ )
479
+ }
266
480
  ```
267
481
 
268
- `placeOrder` is retry-safe: one order, one charge, however many times the button
269
- is pressed.
270
-
271
- ### Starting a new checkout
272
-
273
- `reset()` discards the cart. It returns nothing, so
274
- `useEffect(() => checkout.reset(), [eventId])` works as it always has. Before
275
- any money moves it resets at once. While a payment is going through, it waits
276
- for the payment to finish, so the payment never lands in a new cart. An order
277
- that is not confirmed yet is kept when it is charged, or when a payment step
278
- failed without an answer that nothing was charged (a network error, a
279
- timeout, or a Stripe error that is not a decline or a cancel). A Razorpay or
280
- redirect payment page the buyer closes is kept only when a check of the order
281
- fails or finds a payment; otherwise it is a cancel. Pressing Pay again then
282
- finishes the order, or pays as usual when nothing was paid. A server refusal
283
- of the order, such as an expired reservation, releases it.
284
-
285
- To know which happened, await `resetWhenSettled()` instead. It resolves `true`
286
- once the checkout is reset, or `false` when it kept the order. In that case,
287
- show the buyer `PAID_ORDER_PENDING_MESSAGE`. While it waits for a payment
288
- (`status === 'placing'`), tell the buyer, for example "Finishing your
289
- payment…":
482
+ ## Tickets, seats, codes and attribution
483
+
484
+ Use the server's current event catalog and displayed prices. Time-slot ticket
485
+ IDs encode their selected session. `Tickets` supports ticket quantities,
486
+ groups, promo/access codes, waiting-list forms and seat-map routing when the
487
+ backend offers them. Waiting-list availability is a separate, authoritative
488
+ catalog contract; a sold-out row alone does not imply that a list is open.
489
+
490
+ Unlike 1.x, native `Tickets` has no password prompt or password prop. For a
491
+ password-protected event, a host can collect the password and call
492
+ `ttf.events.authenticate(eventId, password)`, handle wrong-password and session
493
+ errors, then refetch the event/catalog for the same buyer and event context.
494
+ Keep the password out of logs and verify the deployed password/session contract
495
+ and the refreshed result; the API call alone is not proof of a working native
496
+ unlock flow.
497
+
498
+ For a headless flow, `TicketSelection` has `id`, `name`, `optionName`, `price`,
499
+ `quantity` and `currency`. Build it from current catalog data and call
500
+ `setTickets(eventId, selections)`. `setSeats` takes server-reserved `SeatLine`s;
501
+ `setSeatsIo` takes a `SeatsIoSelection`. Do not fabricate seat holds. The
502
+ native `SeatMap` drives the chart and shared selection controller; provider
503
+ holds, server cart acceptance and actual device behavior need separate testing.
504
+
505
+ Quantity retention across rotating encrypted IDs requires deployed canonical
506
+ `selectionKey`s and dated numeric context (`display_price`, `price_with_fees`,
507
+ `fee_included`, `price_currency`, `price_symbol`). Without those optional keys,
508
+ opaque-ID compatibility remains; rotation retention is not guaranteed.
509
+
510
+ Supply `initialPromoCode` and `referrer` explicitly to `EmbeddedCheckout` or
511
+ `Tickets`, or use `applyPromoCode` / `attributeReferrer` from the hook. Native
512
+ `readAttributionFromUrl` is a no-op; the host must parse and classify incoming
513
+ links. An ambiguous numeric `?r=` can be a promo or referral. Do not send it as
514
+ a referral without authoritative classification. This beta does not reproduce
515
+ every hosted conditional-field visibility rule; conditional requiredness is
516
+ left to the backend. Test your event's fields and referral/promo links against
517
+ the deployed service before relying on parity.
518
+
519
+ Post-purchase upgrade waiting is server-owned and separate from the event
520
+ waiting list. `TicketChange` exposes it only for valid targets actually offered
521
+ by the server; a sold-out ticket is not sufficient. Validate the organizer's
522
+ configuration and deployed join/leave behavior.
523
+
524
+ ## Billing, custom fields and consent
525
+
526
+ `CheckoutBilling` requires `firstName`, `lastName` and `email`. Other fields
527
+ include `confirmEmail`, phone, address, `countryId`/`stateId` (backend numeric
528
+ IDs encoded as strings), `countryCode`/`stateCode` (payment billing codes), DOB,
529
+ `nationality`, `gender`, `instagramHandle` and order `dataCapture`. A
530
+ `CheckoutHolder` requires `firstName`, `lastName`, `email` and may include
531
+ `phone`, numeric `ticketTypeId` as a string and ticket `ticketDataCapture`.
532
+ `CheckoutConsent` is `{ ttfOptIn: boolean, brandOptIn: boolean }`.
533
+
534
+ The event supplies `buyerFields`, `fieldGates`, `customFields`, `conditions`,
535
+ required add-ons and waiver requirements. Use the server's ticket breakdown to
536
+ associate holder answers with ticket types. Add-on capture is separately keyed by
537
+ add-on ID. Do not hard-code a minimal buyer form as valid for every event. The
538
+ standalone `BillingInfo` default is less strict than `EmbeddedCheckout`'s holder
539
+ and email-confirmation defaults; match it with the paired `Payment` props. The
540
+ SDK's confirmation email value is a UI check; the order API uses the primary
541
+ email. Recheck terms, privacy and marketing consent with your own policy.
542
+
543
+ **Beta.23 consent limitation:** when `privacyPolicy` is supplied, the stock
544
+ privacy checkbox reads and writes `consent.ttfOptIn`, which the order sends as
545
+ `ttf_opt_in`. It is not independent of the Ticket Fairy marketing flag. The
546
+ brand opt-in uses a separate value. If both opt-in labels are explicitly empty,
547
+ `BillingInfo` can skip committing consent even while it shows the privacy
548
+ checkbox, leaving the payment gate blocked. Also, enabling
549
+ `skipBillingWhenProfileComplete` (or standalone `skipWhenProfileComplete`) can
550
+ hide that checkbox while `Payment` still requires it. Avoid those combinations
551
+ without a verified host workaround; where separate privacy and marketing consent
552
+ is required, use a custom flow and a tested backend consent contract.
553
+
554
+ During cart priming, failed custom-field, checkout-config, add-on or event
555
+ condition reads fall back to empty/default data. The stock UI cannot show or
556
+ gate requirements it did not load. Only a failed waiver read (except a 404) is
557
+ recorded in `unavailableDetails` and retried/gated. Do not treat an empty list as
558
+ proof that no requirement exists; verify deployment/failure behavior and add
559
+ tested host safeguards for mandatory fields and acknowledgments. Server
560
+ validation of required answers is not proof that a condition was acknowledged.
561
+
562
+ Waiver signatures are separate from holder fields. The waiver component reads
563
+ the event's permitted methods and stores the uploaded signature reference. If
564
+ waiver details could not be loaded, placement waits/retries that read rather
565
+ than assuming no waiver. Image questions require a host `pickImage` adapter.
566
+
567
+ ## Payments and safe completion
568
+
569
+ `Payment` uses server event/method configuration and device support. Free
570
+ orders, card, native wallets, payment plans, OXXO, provider pages and other
571
+ methods are different paths; an SDK export does not enable a method on your
572
+ account. The server's review and breakdown are authoritative for amount,
573
+ currency and receiving account. Never collect PAN or CVC in your JS form or
574
+ logs. Use the provider-owned card field or eligible PaymentSheet.
575
+
576
+ ### Result, retry and reset
577
+
578
+ `PlaceOrderResult` includes `orderHash`, `free`, `needIdVerification`, optional
579
+ `currency`, `paymentIntentId`, `alreadyPaid`, `pending` and
580
+ `pendingVoucherUrl`. `pending: true` represents an asynchronous result such as
581
+ an OXXO voucher or PayPal settlement. Show pending instructions and verify
582
+ status with the backend/provider; do not label it paid or fulfill it as paid.
583
+ Conversely, `pending !== true` and `status === 'succeeded'` are not settlement
584
+ proof: Stripe `Processing` and `RequiresCapture` can produce a result without
585
+ `pending`. The stock non-pending receipt can show confirmed copy before final
586
+ capture. Reconcile the authoritative order/provider status and use custom or
587
+ redirect routing when your fulfillment policy requires verified payment.
588
+ When `needIdVerification` is true, the host must arrange a separate verification
589
+ journey. Inline `IDVerification` requires the buyer to press Verify; redirect
590
+ mode does not render it. A composed host can render `IDVerification` under the
591
+ active `CheckoutProvider`, or call `runIdVerification(orderHash)` from a custom
592
+ flow. Handle navigation/returns; a failed or delayed verification proves neither
593
+ payment failure nor settlement.
594
+
595
+ **Zero-total orders:** this SDK ignores the free-registration response body and
596
+ reports `needIdVerification: false`. Verification or approval may still be
597
+ required, and `free: true` is not proof tickets were issued. The default inline
598
+ `EmbeddedCheckout` can nevertheless say registration is complete and the buyer
599
+ is registered while approval or verification is still outstanding. Use
600
+ `confirmationMode="redirect"` with host-owned authoritative order and
601
+ verification status, or a custom composed confirmation gated on that status.
602
+ If the status is unavailable, contact support. Do not retry payment for a free
603
+ order.
604
+
605
+ `status` is `idle`, `priming`, `ready`, `repricing`, `placing`, `succeeded`,
606
+ `expired` or `error`. Disable repeated submissions while `isBusy` or `placing`,
607
+ show the reservation countdown and handle expiry. The engine coordinates
608
+ concurrent placement and can retain the original order when payment may have
609
+ moved. A timeout, dismissed provider page, navigation or app interruption is
610
+ not proof that no payment occurred. Reconcile the original order with your
611
+ backend and payment provider before retrying a purchase or fulfilling it. This
612
+ is not an exactly-once charging guarantee across every host/provider.
613
+
614
+ `reset()` starts a reset and returns no completion signal. Prefer
615
+ `await resetWhenSettled()` when navigating to a new checkout: it waits for an
616
+ in-flight payment and returns `true` only if reset completed. `false` can mean
617
+ the original order is retained or the checkout was retired. Show
618
+ `PAID_ORDER_PENDING_MESSAGE` where appropriate and reconcile; do not clear the
619
+ store, start a new order or promise a refund merely to bypass this guard. A
620
+ custom `providerPage` must settle on close/unmount with `{ event: 'dismiss' }`;
621
+ other custom async ports must settle too, or reset can wait indefinitely.
290
622
 
291
623
  ```tsx
292
- if (checkout.status === 'placing') setNotice('Finishing your payment…')
293
- if (!(await checkout.resetWhenSettled())) setNotice(PAID_ORDER_PENDING_MESSAGE)
294
- ```
624
+ import { PAID_ORDER_PENDING_MESSAGE, useCheckoutContext } from 'tf-checkout-react-native'
295
625
 
296
- A custom `providerPage` must resolve `'dismiss'` when it closes or unmounts, and
297
- any other custom port must settle then too, or `resetWhenSettled()` waits for it.
626
+ function useStartAgain(setNotice: (message: string) => void) {
627
+ const checkout = useCheckoutContext()
628
+ return async () => {
629
+ if (checkout.status === 'placing') setNotice('Finishing your payment…')
630
+ if (!(await checkout.resetWhenSettled())) setNotice(PAID_ORDER_PENDING_MESSAGE)
631
+ }
632
+ }
633
+ ```
298
634
 
299
- A custom confirmer marks a decline or cancel by rejecting with
300
- `paymentProviderError(message, 'declined' | 'cancelled')`. Any other rejection
301
- counts as a payment that may have gone through, so the order is kept.
635
+ `TtfError` exposes `message`, HTTP `status` (`0` for transport failure),
636
+ optional backend `code`, `data` and `retryAfter` seconds. `errorMessage(error)`
637
+ and `isCartExpiredError(error)` help with display and expiry handling. Do not
638
+ log tokens, raw payment credentials or sensitive buyer data. An error after
639
+ payment initiation can be ambiguous even when its UI says to retry.
302
640
 
303
- The full SDK is re-exported from this package — `ttf`, `api`, `hooks`, `dtos`,
304
- `logic`, and the engine. See the [SDK reference](https://github.com/theticketfairy/tf-shared-libraries/blob/main/packages/tf-checkout-shared/README.md).
641
+ A custom confirmer can reject with `paymentProviderError(message, outcome)`:
642
+ `declined`, `cancelled`, `notStarted` or `unknown`. Only classify the first
643
+ three when the provider establishes that no charge occurred. Other rejections
644
+ are treated as unknown. Preserve the original order and reconcile it.
305
645
 
306
- ## Rewards
646
+ ### Stripe card and PaymentSheet
307
647
 
308
- The native analogue of the web Rewards section: standing, the referral tree,
309
- the prize ladder, packs, the share card, and one share button.
648
+ By default, native `Payment` uses Stripe's native `CardInput` for an eligible
649
+ card path. A host composing its own UI can opt into PaymentSheet on the
650
+ provider. Use a static merchant name and scheme for the life of the mounted
651
+ checkout. Configure the app to receive the same deep link and pass cold- and
652
+ warm-start Stripe callback URLs to Stripe according to its React Native setup.
653
+ The embedded component does not expose the PaymentSheet opt-in.
310
654
 
311
655
  ```tsx
312
- import { Confirmation, Rewards } from 'tf-checkout-react-native'
313
- ;<Confirmation>
314
- <Rewards orderHash={orderHash} />
315
- </Confirmation>
656
+ import React from 'react'
657
+ import { CheckoutProvider, Payment } from 'tf-checkout-react-native'
658
+
659
+ export function PaymentStep() {
660
+ return (
661
+ <CheckoutProvider
662
+ options={{ urlScheme: 'myapp' }}
663
+ paymentSheet={{
664
+ merchantDisplayName: 'My Events',
665
+ returnURL: 'myapp://stripe-redirect',
666
+ }}
667
+ >
668
+ {/* Render the ticket, billing and consent sections in this provider too. */}
669
+ <Payment />
670
+ </CheckoutProvider>
671
+ )
672
+ }
316
673
  ```
317
674
 
318
- When `<EmbeddedCheckout/>` renders the receipt itself, pass the block through
319
- `confirmationChildren`:
675
+ The return URL must equal `${urlScheme}://stripe-redirect`; register the actual
676
+ scheme in iOS and Android and route callbacks before normal navigation. A
677
+ controlled provider must create its `value` with the same `paymentSheet` and
678
+ `options` on `useCheckout` itself. Changing the config during a mount is not
679
+ supported. In headless UI, use `checkout.paymentSheet?.placeOrder()` when
680
+ available; the normal `Payment` section chooses it for the eligible card row.
681
+
682
+ In beta.24 the sheet is limited to eligible non-MXN Stripe PaymentIntents whose
683
+ original methods are card alone, or card plus Cash App Pay in USD. It does not
684
+ admit delayed methods, create a new intent or add a Stripe Customer/saved-card
685
+ contract. The SDK checks the original intent's identity, amount and currency
686
+ against the order review. Where the method list or sheet setup cannot be
687
+ admitted, the flow may ask for the native card form; a financial identity
688
+ mismatch instead requires order review. MXN installment card payments and new
689
+ card payment plans retain CardField. Account, event, device and provider
690
+ eligibility still apply.
691
+
692
+ Stripe may show card-mode Link when its account and session qualify; the SDK
693
+ requests automatic Link display but cannot promise its availability. Link is
694
+ not the same thing as OS/password-manager card autofill, which is also
695
+ provider/device dependent. Test Link and callbacks on real devices and the
696
+ actual receiving Stripe account.
697
+
698
+ ### Apple Pay, Google Pay and other providers
699
+
700
+ Native wallet rows are separate from PaymentSheet. They appear only when the
701
+ server enables the wallet, the corresponding OS/device supports it and Stripe's
702
+ platform-pay check succeeds. Configure merchant entitlements and identifiers,
703
+ the Stripe account, app scheme and native build for your app. The order review
704
+ must supply valid wallet amount, currency and merchant country.
705
+ A composed provider can pass:
320
706
 
321
707
  ```tsx
322
- <EmbeddedCheckout eventId={eventId} confirmationChildren={<MyRewards />} />
708
+ <CheckoutProvider options={{
709
+ merchantIdentifier: 'merchant.com.example.app',
710
+ merchantName: 'My Events',
711
+ googlePayTestEnv: false,
712
+ urlScheme: 'myapp',
713
+ }}>
714
+ {/* Render the ticket and billing sections in this provider too. */}
715
+ <Payment />
716
+ </CheckoutProvider>
323
717
  ```
324
718
 
325
- Native adaptations, and why:
326
-
327
- - **No SVG.** The tree is positioned `View`s, from the same shared layout the
328
- web SVG uses. Adding `react-native-svg` would be a new native peer for every
329
- customer app; a dot-and-line picture does not need one.
330
- - **One share button.** Per-channel buttons collapse into `Share.share()` — the
331
- OS sheet already lists every installed app, which is what the web's share
332
- sheet channel reaches for through the browser.
333
- - **Copy comes in as props**, this package's idiom. `translate`, or `locale` and
334
- `messages` for the built-in `rewards.*` catalogue.
719
+ `CheckoutOptions` also accepts `stripePublishableKey` as an override and
720
+ `redirectReturnUrl` and `merchantCountryCode`. Normally use the keys from the
721
+ order review; do not hard code a connected account or infer it from the event.
722
+ In this beta, native wallet confirmation takes merchant country from the
723
+ authoritative order review rather than the `merchantCountryCode` option. The
724
+ native confirmer uses `Order` as its merchant-name fallback; set your merchant
725
+ name explicitly. The example matches the earlier `PROD` setup. For Google Pay,
726
+ the `Payment` support probe uses test mode when `env` is `STAG` or `DEV`, while
727
+ confirmation uses `options.googlePayTestEnv`. Set both consistently for the
728
+ intended environment; a `baseUrl` override does not set `env`. Configure the
729
+ native/plugin wallet setting and verify both the support probe and confirmation
730
+ on device, especially with custom hosts. Expo settings, Apple entitlement,
731
+ deep links and provider account approval are host responsibilities. Test
732
+ eligibility, cancellation, return and settlement on each platform.
733
+
734
+ `CheckoutProvider` without a controlled value supplies a WebView provider-page
735
+ port and an installment picker. Provider pages may be used for PayPal,
736
+ Razorpay, Xendit, Adyen or redirect methods when the backend enables them.
737
+ Host routing and provider behavior still determine whether a particular flow
738
+ works; check network, app-link, browser and return behavior on device. A
739
+ headless `useCheckout` host must supply `ports.providerPage` and any other
740
+ needed ports; `rnPorts` alone does not provide the provider-page UI. Its
741
+ `providerPage` receives `{ uri?, html?, baseUrl?, returnUrlPrefix }` and resolves
742
+ `{ event: 'success' | 'dismiss' | 'failed', returnUrl?, data?, description?,
743
+ notOpened?, paymentOutcome? }`. Report `notOpened` or `paymentOutcome:
744
+ 'declined'` only when actually established. The engine still reconciles the
745
+ provider outcome.
746
+
747
+ ### `CardInput` for custom payment UI
748
+
749
+ `CardInput` props are `owner?: string` (default `checkout`),
750
+ `visible?: boolean`, and `onComplete?: (complete: boolean) => void`. The
751
+ native hook's default card claim uses the default owner. If multiple flows can
752
+ mount card fields, give each its own owner and use the matching
753
+ `claimCardField(owner)` and `cardClaimStep` as `placeOrder`'s `claimCard` option;
754
+ otherwise one flow must not use another's field. Keep the field mounted and
755
+ visible through its confirmation. `Payment` handles its own field ownership.
756
+ Do not treat `onComplete` as confirmation or payment success.
757
+
758
+ ## Headless checkout
759
+
760
+ `useCheckout()` is the native-wired React binding, not the old 1.x React Query
761
+ mutation. It returns a flat `CheckoutSnapshot`, `snapshot`, `store`, derived
762
+ state and actions. It injects the native Stripe confirmer and ports. Use it
763
+ inside a React component after configuration, and supply a React Query provider
764
+ where the host also uses query hooks. The app-wide checkout store is shared
765
+ across native hook calls and provider sections: they are not independent carts.
335
766
 
336
- ## Seat maps
337
-
338
- `SeatMap` uses the existing WebView peer to render internal charts or the event's
339
- Seats.io chart, with the shared core owning selection, holds and reservations.
340
- It requires the matching backend seating/session contracts. Provider-backed hold
341
- and deployed-cart acceptance remain separate checks; see the
342
- [seating contract and evidence](https://github.com/theticketfairy/tf-shared-libraries/blob/main/docs/integration/app-sdk.md#seat-chart-and-ticket-refund-checkpoint--25-october-2026).
343
-
344
- ## Known gaps vs web
345
-
346
- - **Smooth scroll** — no `scrollIntoView` equivalent, so `autoScroll` has no
347
- native counterpart.
348
-
349
- ## Development
350
-
351
- From the repo root:
352
-
353
- ```bash
354
- pnpm run start:react-native
767
+ ```tsx
768
+ import React, { useState } from 'react'
769
+ import { Button, Text, View } from 'react-native'
770
+ import { useCheckout, type TicketSelection } from 'tf-checkout-react-native'
771
+
772
+ // selections must come from this event's current server catalog.
773
+ export function Reserve({ eventId, selections }: {
774
+ eventId: string
775
+ selections: TicketSelection[]
776
+ }) {
777
+ const checkout = useCheckout()
778
+ const [message, setMessage] = useState('')
779
+ const reserve = async () => {
780
+ try {
781
+ await checkout.setTickets(eventId, selections)
782
+ setMessage('Tickets reserved. Collect the required details next.')
783
+ } catch {
784
+ setMessage('Could not reserve tickets. Check availability and try again.')
785
+ }
786
+ }
787
+ return <View><Button title="Reserve" disabled={checkout.isBusy} onPress={() => { void reserve() }} />
788
+ <Text>{message}</Text></View>
789
+ }
355
790
  ```
356
791
 
357
- `pnpm run playground:react-native` opens the playground mode instead.
792
+ A real headless host must collect the event's gated buyer/holder data, custom
793
+ answers, conditions, add-ons and waivers, render a provider-owned payment input
794
+ or wallet/provider UI, validate against the event, and handle every result and
795
+ error before placing an order. Enforce host privacy and `additionalConditions`
796
+ yourself; the headless engine does not receive these `Payment` props. The short
797
+ example only primes the cart.
798
+
799
+ ### Snapshot and actions
800
+
801
+ | Member | Purpose |
802
+ | --- | --- |
803
+ | `status`, `error`, `order`, `orderFixed` | Lifecycle, error text, result and whether order inputs are frozen. |
804
+ | `eventId`, `selections`, `ticketTypeIds`, `breakdown`, `priceBreakdown` | Current cart and server-priced display data. |
805
+ | `secondsLeft`, `expiresAt`, `isBusy` | Countdown in whole seconds, absolute expiry in epoch ms and operation state. `secondsLeft` can be undefined. |
806
+ | `customFields`, `buyerFields`, `fieldGates`, `holderNamesRequired` | Event-specific capture requirements. |
807
+ | `requiredAddOns`, `missingRequiredAddonNames`, `addOns`, `addOnDataCapture` | Add-on requirements and selections. |
808
+ | `billing`, `holders`, `consent`, `conditions`, `acceptedConditions` | Buyer data and acknowledgments. |
809
+ | `waiver`, `waiverSignatures`, `waiverSigningIndexes`, `unavailableDetails` | Waiver state; `unavailableDetails` tracks waiver failures, not other failed detail reads. |
810
+ | `gateway`, `stripe`, `paymentStripe`, `paymentPlan`, `redirectPaymentMethods`, `walletAvailability`, `currency` | Server/engine payment and currency context. Their presence is not a guarantee of provider eligibility. |
811
+ | `extraItems`, `extraDraft`, `extraQuotes`, `extraRejections`, `extraExpiresAt`, `reviewedExtraOrderTotal` | Accommodation/extra reservation and price state. |
812
+ | `setTickets(eventId, selections)` | Prime ticket cart and price it. |
813
+ | `setSeats(eventId, lines)`, `setSeatsIo(eventId, selection)`, `resumeCart(eventId, options?)` | Prime a seat cart or attach to a supported existing cart. |
814
+ | `setAddons(selection)` | Set add-on quantities and reprice; await it. |
815
+ | `setExtraItems(items)`, `setExtraDraft(items \| undefined)` | Reserve or edit accommodation/extras. |
816
+ | `setBilling(billing)`, `setHolders(holders, options?)`, `setConsent(consent)` | Commit buyer data; optional holder `backfilled` describes copies of buyer details. |
817
+ | `setAcceptedConditions(map)`, `acceptCondition(id, accepted)` | Commit required/optional acknowledgment state. |
818
+ | `setAddonDataCapture(capture)` | Set answers keyed by add-on ID. |
819
+ | `holdWaiverSigning(index)`, `setWaiverSignature(index, signature, hold?)` | Bind a signature to its original cart/holder; release the returned hold after saving. |
820
+ | `checkBuyerEmail(email)`, `reloadCheckoutDetails()` | Check for an existing account; retry an unavailable waiver read. |
821
+ | `placeOrder(method?, options?)` | Place/pay via an eligible method; returns `Promise<PlaceOrderResult>`. Options include `autoRunIdVerification`, `idVerification`, `claimCard`. |
822
+ | `runIdVerification(orderHash, options?)` | Open/poll verification as a separate post-order step. |
823
+ | `syncCurrency(eventId, code?, options?)` | Seed/change currency; refetch catalog and re-prime as needed. |
824
+ | `applyPromoCode(eventId, promo)`, `attributeReferrer(eventId, referrer)`, `joinWaitingList(eventId, entry)` | Event engagement helpers. |
825
+ | `reset()`, `resetWhenSettled()` | Reset subject to payment safety; use the awaited form to know its outcome. |
826
+ | `paymentSheet?.placeOrder()` | Present the explicitly configured eligible native sheet. May be absent. |
827
+
828
+ `placeOrder` supports simple `'card'`, `'applePay'`, `'googlePay'` or typed
829
+ `PaymentRequest` objects: `auto`, `card`, `applePay`, `googlePay`, `redirect`
830
+ (with `methodType`), `oxxo`, `xenditEWallet` (with `walletKey` and optional extra),
831
+ `paypal`, `razorpay`, `adyen`, `xenditCard`, or `paymentPlan`. A union member
832
+ is not an availability promise. Render choices from server/provider eligibility
833
+ and supply the required port. `UseCheckoutOptions` accepts `{ ports?, options?,
834
+ paymentSheet? }`; ports merge over `rnPorts`. `createRnStripeConfirmer`,
835
+ `createRnPorts`, `rnConfirmer` and `rnPorts` are exported for advanced hosts.
836
+ For non-React integration, `createEngine({ ports, options?, store? })` provides
837
+ `prime`, `placeOrder`, `getSnapshot` and `subscribe`; its required `confirmer`
838
+ and optional ports are described by the published `CheckoutPorts` type.
839
+
840
+ ## Hooks and API
841
+
842
+ Import the shared React Query hooks through the **`hooks` namespace**, not as
843
+ individual top-level imports. `CheckoutProvider` includes a query provider for
844
+ its descendants; outside it, wrap your components with your own
845
+ `QueryClientProvider`. Query hooks expose React Query states such as `data`,
846
+ `isLoading`, `error` and `refetch`; mutation hooks expose `mutate` /
847
+ `mutateAsync`, pending state and error. Query and mutation options follow the
848
+ published `QueryOpts` and `MutationOpts` types. Event/order hooks can be gated
849
+ until their ID is available.
358
850
 
359
- Both need a dev client. Build one with:
851
+ ```tsx
852
+ import React from 'react'
853
+ import { Text } from 'react-native'
854
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
855
+ import { hooks } from 'tf-checkout-react-native'
360
856
 
361
- ```bash
362
- cd example && pnpm run ios
857
+ const queryClient = new QueryClient()
858
+ function CatalogName({ eventId }: { eventId: string }) {
859
+ const tickets = hooks.useTickets(eventId)
860
+ if (tickets.isLoading) return <Text>Loading tickets…</Text>
861
+ if (tickets.error) return <Text>Could not load tickets.</Text>
862
+ return <Text>{tickets.data?.length ?? 0} ticket types</Text>
863
+ }
864
+ export function Catalog({ eventId }: { eventId: string }) {
865
+ return <QueryClientProvider client={queryClient}><CatalogName eventId={eventId} /></QueryClientProvider>
866
+ }
363
867
  ```
364
868
 
365
- Set credentials in `example/.env` (`EXPO_PUBLIC_`-prefixed); see
366
- `example/.env.example`.
869
+ The API reference lists the available hooks by domain and their parameters.
870
+
871
+ The top-level native `useCheckout()` adds native ports. `hooks.useCheckout` is
872
+ the shared lower-level binding and expects an `EngineConfig` with ports; do not
873
+ substitute it into the native quick start. The root also exports `ttf` (a
874
+ configured domain-grouped API facade), `api` (raw API namespace), types,
875
+ `createEngine`, store/logic utilities and translation functions. For example,
876
+ `ttf.events.tickets(eventId)`, `ttf.geo.countries()`, `ttf.orders.get(orderId)`
877
+ and `ttf.order.review(orderHash)` are promise-returning calls. Private account
878
+ reads require an appropriately authorized account session; the checkout buyer
879
+ scope does not grant it. Low-level APIs do not replace checkout preflight or
880
+ payment reconciliation.
881
+
882
+ The [API reference](https://unpkg.com/tf-checkout-react-native@2.0.0-beta.24/docs/API.md)
883
+ includes the full component and hook catalog.
884
+
885
+ ## Authentication and account components
886
+
887
+ `AuthModal` accepts required `open` and `onClose`, plus `checkoutEventId`,
888
+ `onCheckoutAuthenticated`, `onAuthenticated`, `allowRegistration`,
889
+ `allowPasswordReset`, `requireRegistrationAddress`, `logoUrl`, `initialEmail`
890
+ and `message`. Supply `checkoutEventId` for checkout-scoped auth. The
891
+ `LoginForm`, `RegisterForm`, `ForgotPasswordForm`, `TwoFactorForm` and
892
+ `EmailCodeForm` present fields and delegate their callbacks/state to the host
893
+ or shared controller. `ResetPasswordForm` takes a required reset `token` and
894
+ optional `onSuccess` / `onBackToLogin`. `PasswordInput` wraps `Input` with a
895
+ visibility toggle. The expanded reference lists required form callbacks.
896
+
897
+ Additional exports include `TicketChange`, `TicketRefund`, `TicketGift`,
898
+ `Passes`, `Trips`, `AccountRewardsSection`, `AccountInformationSection`,
899
+ `AccountPaymentPlanSection`, account deletion, profile and notification views,
900
+ `Streaming`, `PhoneAuthentication`, `AccountEmailEnrichment`,
901
+ `EventDiscussionView` and post reactions. Their availability depends on the
902
+ deployed backend, authorized account scope and host-owned navigation. See the
903
+ [API reference](https://unpkg.com/tf-checkout-react-native@2.0.0-beta.24/docs/API.md)
904
+ for component variants and required props.
905
+
906
+ Standalone account components that use query hooks need a `QueryClientProvider`;
907
+ `CheckoutThemeProvider` can theme them without creating a checkout. The `View`
908
+ variants let a host create and retain a shared flow itself. The host must handle
909
+ account sign-in, deep links, browser handoff and navigation; these components do
910
+ not grant account scope by being rendered.
911
+
912
+ ## Theme, primitives and localization
913
+
914
+ Pass a partial `CheckoutTheme` to `EmbeddedCheckout`, `CheckoutProvider` or
915
+ `CheckoutThemeProvider`. `colorScheme` is `'light'` (default) or `'dark'`;
916
+ other tokens are `accent`, `accentContrast`, `surface`, `surfaceMuted`,
917
+ `background`, `text`, `muted`, `border`, `error`, `success`, `radiusCard`,
918
+ `radiusControl`, `fontSize`, `fontFamily`. Colors/fonts are strings; radii and
919
+ font size are numbers. `resolveTheme(theme)` returns all resolved tokens and
920
+ `useTheme()` reads the current theme. Check contrast and native accessibility.
367
921
 
368
- The example's `metro.config.js` pins React, react-query, and Stripe to single
369
- copies. That is a **workspace-only** concern — it exists because the example
370
- consumes the library through a symlink. Consumers installing from npm need
371
- nothing of the sort.
372
-
373
- ---
374
-
375
- Part of the [TicketFairy checkout monorepo](https://github.com/theticketfairy/tf-shared-libraries/blob/main/README.md).
922
+ ```tsx
923
+ <EmbeddedCheckout eventId="YOUR_EVENT_ID" theme={{
924
+ colorScheme: 'dark', accent: '#7c3aed', accentContrast: '#ffffff', radiusCard: 16,
925
+ }} />
926
+ ```
376
927
 
377
- Public checkout sign-in uses event-scoped server-only buyer binding, without a bundled
378
- client secret. The checkout's auth modal supplies the event automatically; standalone
379
- `AuthModal` callers pass `checkoutEventId` and can receive the narrow
380
- `onCheckoutAuthenticated(CheckoutBuyerProfile)` callback. Password/signup/email/phone
381
- proofs still complete required two-factor verification. These credentials cannot read
382
- private account preferences or tickets. See the [shared identity contract](https://github.com/theticketfairy/tf-shared-libraries/blob/main/packages/tf-checkout-shared/README.md) for
383
- scope, retry, logout and configuration ownership. Existing confidential OAuth clients
384
- are trusted-server integrations; never distribute their secrets in browser/native code.
928
+ The exported theme-aware primitives are `Loader`, `Card`, `SectionHeader`,
929
+ `Divider`, `BodyText`, `MutedText`, `ErrorText`, `Button`, `FrozenFields`,
930
+ `Field`, `Input`, `Checkbox`, `Select`, `QuantitySelect`, `Timer`, `TimerBanner`,
931
+ `PaymentMethods` and `RichText`. `useStyles` and `useFrozen` are also exported.
932
+ `PaymentMethods` is a controlled presentation component, not a Pay action.
933
+ `RichText` accepts `html`, `textStyle`, `tagsStyles`, `onLinkPress` and
934
+ `selectable`; it uses the render-html peer. Test external links with your host's
935
+ navigation policy. `FrozenFields` disables its supported SDK controls when the
936
+ order is fixed; custom controls need the same guard. The expanded API reference
937
+ has the individual control props.
938
+
939
+ Selected sections accept `locale`, `messages` and `translate`; these are per
940
+ component, not a promise that every `EmbeddedCheckout` string can be globally
941
+ replaced. Shared localization exports include `createTranslator`,
942
+ `resolveLocale`, `registerCatalogue` and `DEFAULT_LOCALE`. Organizer-authored
943
+ content is data, and should not be assumed to have a translation.
944
+
945
+ ## Migration from 1.x
946
+
947
+ This is a major integration change. Pin 1.x while planning migration and test
948
+ against the exact new version, backend and devices. The historical
949
+ [1.0.47 README](https://unpkg.com/tf-checkout-react-native@1.0.47/README.md)
950
+ and [1.0.47 declarations](https://unpkg.com/tf-checkout-react-native@1.0.47/lib/typescript/src/index.d.ts)
951
+ describe the old package; examples there are not 2.x instructions.
952
+
953
+ | 1.x integration | 2.x beta.24 approach |
954
+ | --- | --- |
955
+ | `setConfig({ EVENT_ID, ENV, ... })` and event-bound global UI | Restore a secure adapter and call `configureCheckout({ env, clientId, clientSecret: '', storage })`; pass `eventId` to the checkout or section. |
956
+ | `Tickets` callbacks such as `onAddToCartSuccess` / `onPressMyOrders` | `Tickets` uses `onContinue` / `onSelectSeats`; the host owns orders navigation. Rework the host flow, not just prop names. |
957
+ | 1.x `Checkout`, `CheckoutV2`, `BillingInfo` and older core components | Use `EmbeddedCheckout` or compose the 2.x sections and `CheckoutProvider`. The 1.x `CheckoutV2` name does not mean this 2.x API. |
958
+ | 1.x `useCheckout()` mutation | Native `useCheckout()` is a stateful engine binding with `setTickets`, setters and `placeOrder`; query hooks are under `hooks`. |
959
+ | Implicit old native storage and session behavior | Supply and restore an OS-protected synchronous adapter. Migrate credentials securely or ask the buyer to sign in again. Checkout-only and account scopes differ. |
960
+ | Per-component style/text props from the old UI | Use theme tokens, available render callbacks and section props; rebuild custom presentation where required. |
961
+ | Existing Stripe setup | Verify current peer/native builds, receiving account, provider URLs, return scheme, wallets and PaymentSheet separately. |
962
+
963
+ Rebuild the native binary after replacing native dependencies and pods. The 1.x
964
+ My Orders/order-detail screens, PDF handling and resale screen are not drop-in
965
+ 2.x equivalents: build the needed host UI, navigation and PDF handling around
966
+ the available order/resale hooks and APIs. Revalidate refund, resale, account
967
+ and password-protected event flows against the deployed service.
968
+
969
+ ## Testing and troubleshooting
970
+
971
+ 1. Pin the SDK and peer versions in the lockfile; verify one React, React Query
972
+ and native-module instance. Make a clean development build and install pods.
973
+ 2. Use an authorized test event and receiving account. Check free and paid
974
+ orders, stock and pricing, required fields/waivers, add-ons, holders, promo
975
+ and referral classification, time slots and seat holds. Verify actual orders
976
+ against the backend and provider, including fee/currency/account context.
977
+ 3. Test supported iOS/Android devices: backgrounding, cold/warm returns,
978
+ interruption, network loss, duplicate taps, expiry, cancellation, guest
979
+ sign-in and account changes. Test each enabled method, wallet and account.
980
+ 4. Configure production merchant credentials, entitlements, public client ID,
981
+ HTTPS endpoint and callback scheme. Remove test-card hints; verify storage,
982
+ privacy and backend contracts. Never log real card details.
983
+
984
+ | Symptom | Check |
985
+ | --- | --- |
986
+ | `TS2304: Cannot find name 'Crypto'` in SDK declarations | A host without DOM and without a compatible ambient `Crypto` type may report this. Prefer compatible host type definitions. `skipLibCheck` skips declaration-file checks, and unresolved `Crypto` can degrade affected types to `any`, allowing invalid `newBundleIdempotencyKey` arguments. Neither option installs runtime crypto. |
987
+ | Configuration/storage error | Hydrate first, then pass an object with synchronous `getItem`, `setItem`, `removeItem`; check native storage failures. |
988
+ | `No QueryClient set` or provider context error | Wrap standalone query hooks in `QueryClientProvider`, and checkout sections in `CheckoutProvider`; check duplicate React Query copies. |
989
+ | Blank chart or missing seat CTA | Confirm event seating support, pass `onSelectSeats`, rebuild WebView, and verify provider/chart/hold contracts. |
990
+ | Missing Apple Pay / Google Pay / Link | Check backend eligibility, real receiving account, device support and host wallet configuration; a simulator or local configuration alone may not qualify. |
991
+ | PaymentSheet unavailable or card field requested | Check static scheme/return URL, original intent and supported method/currency combination. The embedded component does not expose sheet configuration. |
992
+ | Redirect does not return | Register and route the exact app scheme, including cold starts; verify the provider account and allowed return URL. |
993
+ | Email/holder/privacy validation does not match UI | Align `BillingInfo` and `Payment` policies; load current event gates and custom fields. |
994
+ | Outcome unclear or reset returns false | Keep the original order and reconcile backend/provider status. A page close or timeout is not proof of no charge. |
995
+ | Code/referral or conditional question differs from hosted checkout | Classify incoming links server-side or from an authoritative source; native URL reading is a no-op and conditional visibility is incomplete in this beta. |