tf-checkout-react-native 2.0.0-beta.0 → 2.0.0-beta.10

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 (71) hide show
  1. package/README.md +115 -24
  2. package/dist/index.d.mts +8194 -282
  3. package/dist/index.d.ts +8194 -282
  4. package/dist/index.js +32308 -7001
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +32547 -7127
  7. package/dist/index.mjs.map +1 -1
  8. package/package.json +23 -11
  9. package/scripts/build-seat-map.mjs +31 -0
  10. package/scripts/seat-map-entry.jsx +22 -0
  11. package/scripts/verify-discussion-support.cjs +131 -0
  12. package/src/CardInput.tsx +64 -3
  13. package/src/EmbeddedCheckout.tsx +140 -38
  14. package/src/auth/AuthModal.tsx +186 -9
  15. package/src/auth/EmailCodeForm.tsx +91 -0
  16. package/src/auth/RegisterForm.tsx +91 -5
  17. package/src/auth/TwoFactorForm.tsx +74 -0
  18. package/src/auth/index.ts +4 -0
  19. package/src/configure.ts +19 -14
  20. package/src/confirmer.ts +112 -20
  21. package/src/generated/seatMapWidget.ts +3 -0
  22. package/src/index.tsx +1 -1
  23. package/src/sections/Accommodation.tsx +57 -0
  24. package/src/sections/AccountDeletion.tsx +347 -0
  25. package/src/sections/AccountEmailEnrichment.tsx +122 -0
  26. package/src/sections/AccountInformation.tsx +139 -0
  27. package/src/sections/AccountPaymentPlan.tsx +105 -0
  28. package/src/sections/AccountRewards.tsx +108 -0
  29. package/src/sections/AddOns.tsx +99 -148
  30. package/src/sections/BillingInfo.tsx +80 -78
  31. package/src/sections/Bundles.tsx +467 -0
  32. package/src/sections/EventDiscussionView.tsx +655 -0
  33. package/src/sections/NotificationPreferences.tsx +244 -0
  34. package/src/sections/OrderSummary.tsx +29 -2
  35. package/src/sections/Passes.tsx +177 -0
  36. package/src/sections/Payment.tsx +170 -53
  37. package/src/sections/PhoneAuthentication.tsx +371 -0
  38. package/src/sections/PostReaction.tsx +122 -0
  39. package/src/sections/ProfileSettings.tsx +446 -0
  40. package/src/sections/Rewards.tsx +320 -0
  41. package/src/sections/SeatMap.tsx +225 -0
  42. package/src/sections/SeatsIoMap.tsx +110 -0
  43. package/src/sections/Streaming.tsx +278 -0
  44. package/src/sections/TicketChange.tsx +379 -0
  45. package/src/sections/TicketGift.tsx +206 -0
  46. package/src/sections/TicketRefund.tsx +104 -0
  47. package/src/sections/Tickets.tsx +289 -208
  48. package/src/sections/Trips.tsx +87 -0
  49. package/src/sections/WaiverSigning.tsx +429 -0
  50. package/src/sections/billing/PhoneIdentity.tsx +169 -0
  51. package/src/sections/billing/TicketHoldersCard.tsx +4 -0
  52. package/src/sections/discussion/CommentImages.tsx +366 -0
  53. package/src/sections/discussion/SwipeReply.tsx +88 -0
  54. package/src/sections/index.ts +46 -0
  55. package/src/sections/rewards/RewardsLadder.tsx +167 -0
  56. package/src/sections/rewards/RewardsTree.tsx +117 -0
  57. package/src/storage.ts +8 -9
  58. package/src/ui/CheckoutProvider.tsx +51 -15
  59. package/src/ui/CustomFields.tsx +48 -12
  60. package/src/ui/Field.tsx +21 -3
  61. package/src/ui/Frozen.tsx +22 -0
  62. package/src/ui/InstallmentPicker.tsx +15 -18
  63. package/src/ui/PaymentMethods.tsx +6 -3
  64. package/src/ui/ProviderPages.tsx +214 -0
  65. package/src/ui/QuantitySelect.tsx +28 -3
  66. package/src/ui/Select.tsx +4 -1
  67. package/src/ui/SignaturePad.tsx +218 -0
  68. package/src/ui/index.ts +3 -0
  69. package/src/ui/primitives.tsx +13 -2
  70. package/src/ui/useAccountT.ts +16 -0
  71. package/src/useCheckout.ts +3 -1
@@ -0,0 +1,467 @@
1
+ /**
2
+ * `<Bundles/>` — the RN analogue of the web section: the event's bundles,
3
+ * bought on their own order. Choose bundles, enter your details (and, when
4
+ * offered, a name on each ticket), pay by card, see the confirmation.
5
+ * Everything the flow decides is in the shared core (`hooks.useBundleFlow`);
6
+ * this file only renders it.
7
+ *
8
+ * At most one native card field exists at a time: the bundle card field is
9
+ * mounted only while this section is visible.
10
+ *
11
+ * Renders nothing when the event sells no bundles. Translated through the
12
+ * shared translator; `locale`, `messages` or `translate` override it.
13
+ */
14
+ import React, { useMemo, type FC } from 'react'
15
+ import { StyleSheet, Text, View } from 'react-native'
16
+
17
+ import {
18
+ bundleAvailabilityCopy,
19
+ bundleContentsLine,
20
+ htmlToPlainText,
21
+ bundleQuantityRange,
22
+ createTranslator,
23
+ hooks,
24
+ type LocaleCode,
25
+ type Messages,
26
+ type PlaceOrderResult,
27
+ type Translate,
28
+ } from 'tf-checkout-shared'
29
+
30
+ import { CardInput, claimCardField } from '../CardInput'
31
+ import { rnConfirmer } from '../instance'
32
+ import { useCheckoutContext, useTheme } from '../ui/CheckoutProvider'
33
+ import { Field, Input } from '../ui/Field'
34
+ import { Button, Card, ErrorText, Loader } from '../ui/primitives'
35
+ import { QuantitySelect } from '../ui/QuantitySelect'
36
+ import type { ResolvedTheme } from '../ui/theme'
37
+
38
+ const money = (currency: string, amount: number) => `${currency} ${amount.toFixed(2)}`
39
+
40
+ export interface BundlesProps {
41
+ eventId: string
42
+ /** Fired once when a bundle purchase is complete, with its order hash. */
43
+ onPurchased?: (orderHash: string) => void
44
+ /**
45
+ * Fired once when a bundle purchase is complete, in the same shape as a
46
+ * ticket order, so the host routes both the same way.
47
+ */
48
+ onComplete?: (order: PlaceOrderResult) => void
49
+ /** `redirect`: skip the inline receipt while the host navigates. */
50
+ confirmationMode?: 'inline' | 'redirect'
51
+ /** False while the section is hidden: no card field is mounted then. */
52
+ visible?: boolean
53
+ /** Section heading. Defaults to the translated "Bundles". */
54
+ heading?: string
55
+ locale?: LocaleCode
56
+ messages?: Partial<Messages>
57
+ translate?: Translate
58
+ }
59
+
60
+ export const Bundles: FC<BundlesProps> = ({
61
+ eventId,
62
+ onPurchased,
63
+ onComplete,
64
+ confirmationMode = 'inline',
65
+ visible = true,
66
+ heading,
67
+ locale,
68
+ messages,
69
+ translate,
70
+ }) => {
71
+ const checkout = useCheckoutContext()
72
+ const theme = useTheme()
73
+ const s = useBundleStyles(theme)
74
+ const t = translate ?? createTranslator({ locale, messages })
75
+ const flow = hooks.useBundleFlow(rnConfirmer, eventId, {
76
+ claimCard: claimCardField,
77
+ defaults: {
78
+ firstName: checkout.billing?.firstName ?? '',
79
+ lastName: checkout.billing?.lastName ?? '',
80
+ email: checkout.billing?.email ?? '',
81
+ },
82
+ store: checkout.store,
83
+ visible,
84
+ confirmationMode,
85
+ onComplete,
86
+ onPurchased,
87
+ })
88
+
89
+ if (flow.view === 'loading') {
90
+ return (
91
+ <Card>
92
+ <View style={s.loader}>
93
+ <Loader />
94
+ </View>
95
+ </Card>
96
+ )
97
+ }
98
+ if (flow.view === 'catalogError') {
99
+ return (
100
+ <Card>
101
+ <ErrorText>{t('bundles.error.catalog')}</ErrorText>
102
+ <Button
103
+ variant="ghost"
104
+ loading={flow.catalogRetrying}
105
+ disabled={flow.catalogRetrying}
106
+ onPress={flow.retryCatalog}
107
+ >
108
+ {t('bundles.retryCard')}
109
+ </Button>
110
+ </Card>
111
+ )
112
+ }
113
+ // No bundles to sell and no purchase to show.
114
+ if (flow.view === 'hidden') return null
115
+
116
+ const title = <Text style={s.heading}>{heading ?? t('bundles.heading')}</Text>
117
+
118
+ if (flow.view === 'redirecting') {
119
+ return (
120
+ <Card>
121
+ {title}
122
+ <Text style={s.muted} accessibilityLiveRegion="polite">
123
+ {t('bundles.redirecting')}
124
+ </Text>
125
+ <Loader />
126
+ </Card>
127
+ )
128
+ }
129
+ if (flow.view === 'receipt' && flow.result) {
130
+ const { result } = flow
131
+ return (
132
+ <Card>
133
+ {title}
134
+ <Text style={s.strong}>{t('bundles.done.title')}</Text>
135
+ <Text style={s.muted}>
136
+ {result.pending
137
+ ? t('bundles.done.pending')
138
+ : result.email
139
+ ? t('bundles.done.email', { email: result.email })
140
+ : t('bundles.done.emailUnknown')}
141
+ </Text>
142
+ <Text style={s.muted}>
143
+ {t('bundles.done.order', { orderHash: result.orderHash })}
144
+ </Text>
145
+ {flow.gatewayWait ? (
146
+ <Text style={s.muted} accessibilityLiveRegion="polite">
147
+ {t(flow.gatewayWait)}
148
+ </Text>
149
+ ) : null}
150
+ <Button
151
+ variant="ghost"
152
+ disabled={flow.gatewayBlocked}
153
+ onPress={() => void flow.again()}
154
+ >
155
+ {t('bundles.done.again')}
156
+ </Button>
157
+ </Card>
158
+ )
159
+ }
160
+
161
+ const anySelected = Object.values(flow.qty).some((n) => n > 0)
162
+
163
+ if (flow.view === 'details') {
164
+ const amount = money(flow.payAmount.currency || flow.currency, flow.payAmount.amount)
165
+ return (
166
+ <Card>
167
+ {title}
168
+ {flow.notice ? (
169
+ <Text style={s.muted} accessibilityLiveRegion="polite">
170
+ {t(flow.notice.key, flow.notice.values)}
171
+ </Text>
172
+ ) : null}
173
+ <Text style={s.strong}>{t('bundles.buyerHeading')}</Text>
174
+ <Field label={t('bundles.firstName')}>
175
+ <Input
176
+ value={flow.buyer.firstName}
177
+ autoCapitalize="words"
178
+ editable={!flow.busy}
179
+ onChangeText={(firstName) => flow.patchBuyer({ firstName })}
180
+ />
181
+ </Field>
182
+ <Field label={t('bundles.lastName')}>
183
+ <Input
184
+ value={flow.buyer.lastName}
185
+ autoCapitalize="words"
186
+ editable={!flow.busy}
187
+ onChangeText={(lastName) => flow.patchBuyer({ lastName })}
188
+ />
189
+ </Field>
190
+ <Field label={t('bundles.email')}>
191
+ <Input
192
+ value={flow.buyer.email}
193
+ keyboardType="email-address"
194
+ autoCapitalize="none"
195
+ editable={!flow.busy}
196
+ onChangeText={(email) => flow.patchBuyer({ email })}
197
+ />
198
+ </Field>
199
+
200
+ {flow.slots.length > 0 ? (
201
+ <>
202
+ <Text style={s.strong}>{t('bundles.holdersHeading')}</Text>
203
+ {!flow.namesRequired ? (
204
+ <Text style={s.muted}>{t('bundles.holdersOptional')}</Text>
205
+ ) : null}
206
+ {flow.slots.map((slot) => {
207
+ const value = flow.holderValue(slot)
208
+ const label = t('bundles.holderLabel', {
209
+ ticket: slot.ticketName,
210
+ number: String(slot.ticketIndex + 1),
211
+ })
212
+ return (
213
+ <View key={`${slot.bundleId}-${slot.ticketIndex}`}>
214
+ <Field label={`${label} · ${t('bundles.holderFirstName')}`}>
215
+ <Input
216
+ value={value?.firstName ?? ''}
217
+ autoCapitalize="words"
218
+ editable={!flow.busy}
219
+ onChangeText={(firstName) => flow.setHolder(slot, { firstName })}
220
+ />
221
+ </Field>
222
+ <Field label={`${label} · ${t('bundles.holderLastName')}`}>
223
+ <Input
224
+ value={value?.lastName ?? ''}
225
+ autoCapitalize="words"
226
+ editable={!flow.busy}
227
+ onChangeText={(lastName) => flow.setHolder(slot, { lastName })}
228
+ />
229
+ </Field>
230
+ </View>
231
+ )
232
+ })}
233
+ </>
234
+ ) : null}
235
+ {flow.problems.map((key) => (
236
+ <ErrorText key={key}>{t(key)}</ErrorText>
237
+ ))}
238
+
239
+ {flow.awaitingCard ? (
240
+ <View style={s.card}>
241
+ <Text style={s.strong}>{t('bundles.cardHeading')}</Text>
242
+ {flow.cardElsewhere ? (
243
+ <>
244
+ <Text style={s.muted}>{t('bundles.payingElsewhere')}</Text>
245
+ <Button variant="ghost" onPress={flow.payHere}>
246
+ {t('bundles.payHere')}
247
+ </Button>
248
+ </>
249
+ ) : flow.cardFailed ? (
250
+ <Button variant="ghost" onPress={flow.retryCard}>
251
+ {t('bundles.retryCard')}
252
+ </Button>
253
+ ) : flow.cardReady && visible ? (
254
+ <CardInput owner={flow.cardOwner} visible={visible} />
255
+ ) : (
256
+ <Loader />
257
+ )}
258
+ </View>
259
+ ) : null}
260
+
261
+ {flow.stillSaving ? (
262
+ <Text style={s.muted} accessibilityLiveRegion="polite">
263
+ {t('common.stillSaving')}
264
+ </Text>
265
+ ) : null}
266
+ {flow.checkingAttempt ? (
267
+ <Text style={s.muted} accessibilityLiveRegion="polite">
268
+ {t('bundles.checkingAttempt')}
269
+ </Text>
270
+ ) : null}
271
+ {flow.waitingForTickets ? (
272
+ <Text style={s.muted} accessibilityLiveRegion="polite">
273
+ {t('bundles.waitTickets')}
274
+ </Text>
275
+ ) : null}
276
+ {flow.waitingForBundle ? (
277
+ <Text style={s.muted} accessibilityLiveRegion="polite">
278
+ {t(flow.gatewayWait ?? 'bundles.waitOtherBundle')}
279
+ </Text>
280
+ ) : null}
281
+ <Button
282
+ onPress={() => void flow.submit()}
283
+ loading={flow.busy}
284
+ disabled={flow.submitDisabled}
285
+ >
286
+ {flow.paying
287
+ ? t('bundles.paying')
288
+ : flow.awaitingCard
289
+ ? t('bundles.pay', { amount })
290
+ : t('bundles.continue')}
291
+ </Button>
292
+ <Button
293
+ variant="ghost"
294
+ disabled={flow.busy || flow.gatewayBlocked}
295
+ onPress={() => void flow.back()}
296
+ >
297
+ {t('bundles.back')}
298
+ </Button>
299
+ {flow.error ? <ErrorText>{t(flow.error)}</ErrorText> : null}
300
+ {flow.orderInProgress ? (
301
+ <Button variant="ghost" onPress={flow.continueOrder}>
302
+ {t('bundles.continueOrder')}
303
+ </Button>
304
+ ) : null}
305
+ {flow.canStartOver ? (
306
+ <Button
307
+ variant="ghost"
308
+ disabled={flow.gatewayBlocked}
309
+ onPress={() => void flow.startOver()}
310
+ >
311
+ {t('bundles.startOver')}
312
+ </Button>
313
+ ) : null}
314
+ </Card>
315
+ )
316
+ }
317
+
318
+ const ticketsInCart = checkout.selections.length > 0
319
+ return (
320
+ <Card>
321
+ {title}
322
+ <Text style={s.muted}>{t('bundles.subtitle')}</Text>
323
+ {flow.notice ? (
324
+ <Text style={s.muted} accessibilityLiveRegion="polite">
325
+ {t(flow.notice.key, flow.notice.values)}
326
+ </Text>
327
+ ) : null}
328
+ {flow.view === 'heldOrder' ? (
329
+ <>
330
+ <Text style={s.muted} accessibilityLiveRegion="polite">
331
+ {t('bundles.heldOrder')}
332
+ </Text>
333
+ {flow.error ? <ErrorText>{t(flow.error)}</ErrorText> : null}
334
+ {flow.checkingAttempt ? (
335
+ <Text style={s.muted} accessibilityLiveRegion="polite">
336
+ {t('bundles.checkingAttempt')}
337
+ </Text>
338
+ ) : null}
339
+ {flow.gatewayWait ? (
340
+ <Text style={s.muted} accessibilityLiveRegion="polite">
341
+ {t(flow.gatewayWait)}
342
+ </Text>
343
+ ) : null}
344
+ <Button
345
+ disabled={flow.busy || flow.checkingAttempt}
346
+ onPress={flow.continueOrder}
347
+ >
348
+ {t('bundles.continueOrder')}
349
+ </Button>
350
+ <Button
351
+ variant="ghost"
352
+ disabled={flow.busy || flow.checkingAttempt || flow.gatewayBlocked}
353
+ onPress={() => void flow.startOver()}
354
+ >
355
+ {t('bundles.startOver')}
356
+ </Button>
357
+ </>
358
+ ) : null}
359
+
360
+ {flow.bundles.map((bundle) => {
361
+ const range = bundleQuantityRange(bundle)
362
+ const stock = bundleAvailabilityCopy(bundle)
363
+ const contents = bundleContentsLine(bundle)
364
+ const desc = htmlToPlainText(bundle.description ?? '')
365
+ return (
366
+ <View key={bundle.id} style={[s.item, { borderTopColor: theme.border }]}>
367
+ <View style={s.info}>
368
+ <Text style={s.name}>{bundle.name}</Text>
369
+ <Text style={s.price}>{money(bundle.currency, bundle.price)}</Text>
370
+ {contents ? (
371
+ <Text style={s.muted}>{t('bundles.includes', { contents })}</Text>
372
+ ) : null}
373
+ {bundle.bookingFee > 0 && (
374
+ <Text style={s.muted}>
375
+ {t('bundles.bookingFee', {
376
+ amount: money(bundle.currency, bundle.bookingFee),
377
+ })}
378
+ </Text>
379
+ )}
380
+ {desc ? <Text style={s.muted}>{desc}</Text> : null}
381
+ {stock && stock.key !== 'bundles.soldOut' ? (
382
+ <Text style={[s.muted, s.stock]}>{t(stock.key, stock.values)}</Text>
383
+ ) : null}
384
+ </View>
385
+ {range.max === 0 ? (
386
+ <Text style={s.soldOut}>{t('bundles.soldOut')}</Text>
387
+ ) : (
388
+ <QuantitySelect
389
+ label={t('bundles.quantity', { name: bundle.name })}
390
+ value={flow.qty[bundle.id] ?? 0}
391
+ min={range.min}
392
+ max={range.max}
393
+ onChange={(v) => flow.setQuantity(bundle.id, v)}
394
+ />
395
+ )}
396
+ </View>
397
+ )
398
+ })}
399
+
400
+ {anySelected ? (
401
+ <Text style={s.strong}>
402
+ {t('bundles.total', { amount: money(flow.currency, flow.total) })}
403
+ </Text>
404
+ ) : null}
405
+ {anySelected && ticketsInCart ? (
406
+ <Text style={s.muted}>{t('bundles.boughtSeparately')}</Text>
407
+ ) : null}
408
+ <Button onPress={flow.continueToDetails} disabled={!anySelected}>
409
+ {t('bundles.continue')}
410
+ </Button>
411
+ </Card>
412
+ )
413
+ }
414
+
415
+ const useBundleStyles = (theme: ResolvedTheme) =>
416
+ useMemo(
417
+ () =>
418
+ StyleSheet.create({
419
+ loader: { paddingVertical: 20, alignItems: 'center' },
420
+ heading: {
421
+ color: theme.text,
422
+ fontSize: theme.fontSize + 2,
423
+ fontWeight: '700',
424
+ fontFamily: theme.fontFamily,
425
+ },
426
+ strong: {
427
+ color: theme.text,
428
+ fontSize: theme.fontSize,
429
+ fontWeight: '700',
430
+ fontFamily: theme.fontFamily,
431
+ },
432
+ muted: {
433
+ color: theme.muted,
434
+ fontSize: theme.fontSize - 1,
435
+ fontFamily: theme.fontFamily,
436
+ },
437
+ stock: { fontWeight: '600' },
438
+ card: { gap: 8 },
439
+ item: {
440
+ flexDirection: 'row',
441
+ alignItems: 'flex-start',
442
+ gap: 12,
443
+ paddingTop: 12,
444
+ borderTopWidth: StyleSheet.hairlineWidth,
445
+ },
446
+ info: { flex: 1, gap: 2 },
447
+ name: {
448
+ color: theme.text,
449
+ fontSize: theme.fontSize,
450
+ fontWeight: '600',
451
+ fontFamily: theme.fontFamily,
452
+ },
453
+ price: {
454
+ color: theme.text,
455
+ fontSize: theme.fontSize - 1,
456
+ fontVariant: ['tabular-nums'],
457
+ fontFamily: theme.fontFamily,
458
+ },
459
+ soldOut: {
460
+ color: theme.muted,
461
+ fontSize: theme.fontSize - 1,
462
+ fontWeight: '600',
463
+ fontFamily: theme.fontFamily,
464
+ },
465
+ }),
466
+ [theme]
467
+ )