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

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 +8195 -282
  3. package/dist/index.d.ts +8195 -282
  4. package/dist/index.js +32346 -7001
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +32586 -7128
  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,446 @@
1
+ import React, { useEffect, useMemo, type ReactNode } from 'react'
2
+ import {
3
+ ActivityIndicator,
4
+ KeyboardAvoidingView,
5
+ Modal,
6
+ Platform,
7
+ Pressable,
8
+ ScrollView,
9
+ StyleSheet,
10
+ Text,
11
+ TextInput,
12
+ View,
13
+ type StyleProp,
14
+ type ViewStyle,
15
+ } from 'react-native'
16
+ import {
17
+ hooks,
18
+ PROFILE_LINK_LIMIT,
19
+ type ProfileLink,
20
+ type ProfileVisibility,
21
+ } from 'tf-checkout-shared'
22
+ import { useTheme } from '../ui/CheckoutProvider'
23
+ import { resolveTheme, type CheckoutTheme } from '../ui/theme'
24
+ import { useAccountT } from '../ui/useAccountT'
25
+
26
+ type Flow = ReturnType<typeof hooks.useProfileSettings>
27
+ export interface ProfileSettingsProps {
28
+ mode: 'links' | 'visibility'
29
+ showHeader?: boolean
30
+ onBack?: () => void
31
+ onSignIn?: () => void
32
+ locale?: string
33
+ translate?: (english: string, values?: Record<string, string | number>) => string
34
+ renderLinkIcon?: (type: ProfileLink['type']) => ReactNode
35
+ renderSaveButton?: (label: string, onPress: () => void, disabled: boolean) => ReactNode
36
+ theme?: CheckoutTheme
37
+ style?: StyleProp<ViewStyle>
38
+ }
39
+ export const ProfileSettings = (props: ProfileSettingsProps) => {
40
+ const settings = hooks.useProfileSettings()
41
+ return <ProfileSettingsView {...props} settings={settings} />
42
+ }
43
+ /** The same common editor owns drafts, retries and visibility on both platforms. */
44
+ export const ProfileSettingsView = ({
45
+ mode,
46
+ settings: flow,
47
+ showHeader = true,
48
+ onBack,
49
+ onSignIn,
50
+ locale,
51
+ translate,
52
+ renderLinkIcon,
53
+ renderSaveButton,
54
+ theme: override,
55
+ style,
56
+ }: ProfileSettingsProps & { settings: Flow }) => {
57
+ const defaultT = useAccountT(locale)
58
+ const t = translate ?? defaultT
59
+ const inherited = useTheme()
60
+ const theme = useMemo(
61
+ () => (override ? resolveTheme({ ...inherited, ...override }) : inherited),
62
+ [inherited, override]
63
+ )
64
+ const assertOwner = hooks.useSessionAction()
65
+ const act = (action: () => unknown) => {
66
+ try {
67
+ assertOwner()
68
+ action()
69
+ } catch {
70
+ /* A retired control cannot act for another buyer. */
71
+ }
72
+ }
73
+ useEffect(() => {
74
+ if (flow.settings) flow.open(mode)
75
+ }, [mode, flow.scopeKey, Boolean(flow.settings)])
76
+ const text = { color: theme.text, fontFamily: theme.fontFamily }
77
+ const muted = { color: theme.muted, fontFamily: theme.fontFamily }
78
+ const title = mode === 'links' ? 'Links' : 'Profile visibility'
79
+ const button = (label: string, action: () => unknown, disabled = flow.busy) => (
80
+ <Pressable
81
+ accessibilityRole="button"
82
+ disabled={disabled}
83
+ accessibilityState={{ disabled, busy: flow.busy }}
84
+ onPress={() => act(action)}
85
+ style={[s.button, { borderColor: theme.border }, disabled && s.disabled]}
86
+ >
87
+ <Text style={[text, { color: theme.accent }]}>{t(label)}</Text>
88
+ </Pressable>
89
+ )
90
+ const saveButton = (action: () => void, disabled: boolean) =>
91
+ renderSaveButton
92
+ ? renderSaveButton(t('Save'), () => act(action), disabled)
93
+ : button('Save', action, disabled)
94
+ if (flow.confirmingAccount || (flow.signedIn && flow.query.isPending))
95
+ return (
96
+ <ActivityIndicator
97
+ accessibilityLabel={t('Loading profile…')}
98
+ color={theme.accent}
99
+ />
100
+ )
101
+ if (!flow.signedIn)
102
+ return (
103
+ <View style={[s.container, style]}>
104
+ {flow.admission.error && (
105
+ <>
106
+ <Text accessibilityRole="alert" style={{ color: theme.error }}>
107
+ {t(flow.admission.error.message)}
108
+ </Text>
109
+ {button('Try again', () => flow.admission.refetch(), false)}
110
+ </>
111
+ )}
112
+ <Text style={text}>{t('Sign in to edit your profile.')}</Text>
113
+ {onSignIn && button('Sign in', onSignIn, false)}
114
+ </View>
115
+ )
116
+ if (!flow.settings)
117
+ return (
118
+ <View style={[s.container, style]}>
119
+ <Text accessibilityRole="alert" style={{ color: theme.error }}>
120
+ {t(flow.query.error?.message ?? 'Your profile settings could not load.')}
121
+ </Text>
122
+ {button('Try again', () => flow.query.refetch(), false)}
123
+ </View>
124
+ )
125
+ const editor = flow.editor
126
+ const cancel = () => {
127
+ if (flow.cancel()) {
128
+ if (onBack) onBack()
129
+ else flow.open(mode)
130
+ }
131
+ }
132
+ const types: [ProfileLink['type'], string][] = [
133
+ ['instagram', 'Add Instagram account'],
134
+ ['facebook', 'Add Facebook account'],
135
+ ['x', 'Add X account'],
136
+ ['external', 'Add external link'],
137
+ ]
138
+ const choices: [ProfileVisibility, string][] = [
139
+ ['everybody', 'Everybody'],
140
+ ['contacts', 'My contacts'],
141
+ ['nobody', 'Nobody'],
142
+ ]
143
+ const linkType = editor?.link?.value.type
144
+ return (
145
+ <View style={[s.container, style]}>
146
+ {showHeader && <Text style={[s.title, text]}>{t(title)}</Text>}
147
+ {mode === 'links' ? (
148
+ <>
149
+ <Text style={[s.caption, muted]}>{t('Links').toLocaleUpperCase(locale)}</Text>
150
+ {editor?.links.map((link, index) => (
151
+ <View key={index} style={[s.linkRow, { borderBottomColor: theme.border }]}>
152
+ <Pressable
153
+ accessibilityRole="button"
154
+ accessibilityLabel={t('Edit {{title}}', { title: link.title })}
155
+ disabled={flow.busy}
156
+ onPress={() => flow.startLink(link.type, index)}
157
+ style={[s.rowAction, s.grow]}
158
+ >
159
+ {renderLinkIcon?.(link.type)}
160
+ <View style={s.grow}>
161
+ <Text style={[s.label, text]}>{link.title}</Text>
162
+ <Text style={[s.url, muted]} numberOfLines={1}>
163
+ {link.url}
164
+ </Text>
165
+ </View>
166
+ </Pressable>
167
+ <Pressable
168
+ accessibilityRole="button"
169
+ accessibilityLabel={t('Remove {{title}}', { title: link.title })}
170
+ disabled={flow.busy}
171
+ onPress={() => flow.removeLink(index)}
172
+ style={s.remove}
173
+ >
174
+ <Text style={{ color: theme.muted, fontSize: 24 }}>×</Text>
175
+ </Pressable>
176
+ </View>
177
+ ))}
178
+ {types.map(([type, label]) => (
179
+ <Pressable
180
+ key={type}
181
+ accessibilityRole="button"
182
+ disabled={flow.busy || (editor?.links.length ?? 0) >= PROFILE_LINK_LIMIT}
183
+ onPress={() => flow.startLink(type)}
184
+ style={[
185
+ s.linkRow,
186
+ { borderBottomColor: type === 'external' ? 'transparent' : theme.border },
187
+ ]}
188
+ >
189
+ {renderLinkIcon?.(type) ?? (
190
+ <Text style={[muted, s.fallbackIcon]}>
191
+ {type === 'external' ? '+' : type === 'x' ? 'X' : '@'}
192
+ </Text>
193
+ )}
194
+ <Text
195
+ style={[s.label, text, type === 'external' && { color: theme.accent }]}
196
+ >
197
+ {t(label)}
198
+ </Text>
199
+ </Pressable>
200
+ ))}
201
+ </>
202
+ ) : (
203
+ <>
204
+ <Text style={[s.heading, text]}>{t('Who can view your profile?')}</Text>
205
+ {choices.map(([value, label]) => (
206
+ <Pressable
207
+ key={value}
208
+ accessibilityRole="radio"
209
+ accessibilityState={{
210
+ checked: editor?.visibility === value,
211
+ disabled: flow.busy,
212
+ }}
213
+ disabled={flow.busy}
214
+ onPress={() => flow.setVisibility(value)}
215
+ style={[s.choice, { borderBottomColor: theme.border }]}
216
+ >
217
+ <Text style={[s.label, text]}>{t(label)}</Text>
218
+ <View
219
+ style={[
220
+ s.radio,
221
+ {
222
+ borderColor:
223
+ editor?.visibility === value ? theme.accent : theme.muted,
224
+ },
225
+ ]}
226
+ >
227
+ {editor?.visibility === value && (
228
+ <View style={[s.radioDot, { backgroundColor: theme.accent }]} />
229
+ )}
230
+ </View>
231
+ </Pressable>
232
+ ))}
233
+ <Text style={[s.help, muted]}>
234
+ {t(
235
+ editor?.visibility === 'everybody'
236
+ ? 'All signed-in Ticket Fairy members can view your profile.'
237
+ : editor?.visibility === 'contacts'
238
+ ? 'Only your accepted friends can view your profile.'
239
+ : 'Only you can view your profile.'
240
+ )}
241
+ </Text>
242
+ </>
243
+ )}
244
+ {flow.error && (
245
+ <Text accessibilityRole="alert" style={[s.help, { color: theme.error }]}>
246
+ {t(flow.error)}
247
+ </Text>
248
+ )}
249
+ {flow.saved && (
250
+ <Text accessibilityLiveRegion="polite" style={[s.help, text]}>
251
+ {t('Profile settings saved.')}
252
+ </Text>
253
+ )}
254
+ <View style={s.footer}>
255
+ {button('Cancel', cancel)}
256
+ {saveButton(
257
+ () => {
258
+ void flow.save()
259
+ },
260
+ flow.busy || !editor || Boolean(editor.link)
261
+ )}
262
+ </View>
263
+ <Modal
264
+ transparent
265
+ visible={Boolean(editor?.link)}
266
+ animationType="slide"
267
+ onRequestClose={() => {
268
+ flow.cancelLink()
269
+ }}
270
+ >
271
+ <KeyboardAvoidingView
272
+ behavior={Platform.OS === 'ios' ? 'padding' : undefined}
273
+ style={s.backdrop}
274
+ >
275
+ <Pressable
276
+ accessibilityRole="button"
277
+ accessibilityLabel={t('Cancel')}
278
+ onPress={() => flow.cancelLink()}
279
+ style={s.scrim}
280
+ disabled={flow.busy}
281
+ />
282
+ <View style={[s.sheet, { backgroundColor: theme.surface }]}>
283
+ <View style={[s.handle, { backgroundColor: theme.muted }]} />
284
+ <ScrollView
285
+ keyboardShouldPersistTaps="handled"
286
+ contentContainerStyle={s.form}
287
+ >
288
+ <Text style={[s.heading, text]}>
289
+ {t(
290
+ linkType === 'external'
291
+ ? 'Add external link'
292
+ : 'Enter your {{provider}} username',
293
+ {
294
+ provider:
295
+ linkType === 'instagram'
296
+ ? 'Instagram'
297
+ : linkType === 'facebook'
298
+ ? 'Facebook'
299
+ : 'X',
300
+ }
301
+ )}
302
+ </Text>
303
+ {linkType !== 'external' && (
304
+ <Text style={[s.help, muted]}>
305
+ {t(
306
+ 'Share your username so others can easily find and connect with you.'
307
+ )}
308
+ </Text>
309
+ )}
310
+ {linkType === 'external' && (
311
+ <>
312
+ <Text style={muted}>{t('Link title')}</Text>
313
+ <TextInput
314
+ accessibilityLabel={t('Link title')}
315
+ value={editor?.link?.value.title ?? ''}
316
+ onChangeText={(title) => flow.changeLink({ title })}
317
+ editable={!flow.busy}
318
+ maxLength={80}
319
+ style={[
320
+ s.input,
321
+ text,
322
+ { borderColor: theme.border, backgroundColor: theme.background },
323
+ ]}
324
+ />
325
+ </>
326
+ )}
327
+ <Text style={muted}>
328
+ {t(linkType === 'external' ? 'Profile URL' : 'Username')}
329
+ </Text>
330
+ <TextInput
331
+ autoCapitalize="none"
332
+ autoCorrect={false}
333
+ keyboardType={linkType === 'external' ? 'url' : 'default'}
334
+ accessibilityLabel={t(
335
+ linkType === 'external' ? 'Profile URL' : 'Username'
336
+ )}
337
+ placeholder={linkType === 'external' ? 'https://' : '@username'}
338
+ placeholderTextColor={theme.muted}
339
+ value={editor?.link?.value.url ?? ''}
340
+ onChangeText={(url) => flow.changeLink({ url })}
341
+ editable={!flow.busy}
342
+ maxLength={512}
343
+ style={[
344
+ s.input,
345
+ text,
346
+ { borderColor: theme.border, backgroundColor: theme.background },
347
+ ]}
348
+ />
349
+ {flow.error && (
350
+ <Text accessibilityRole="alert" style={{ color: theme.error }}>
351
+ {t(flow.error)}
352
+ </Text>
353
+ )}
354
+ <View style={s.footer}>
355
+ {button('Cancel', () => flow.cancelLink())}
356
+ {saveButton(() => {
357
+ void flow.saveLinkChanges()
358
+ }, flow.busy)}
359
+ </View>
360
+ </ScrollView>
361
+ </View>
362
+ </KeyboardAvoidingView>
363
+ </Modal>
364
+ </View>
365
+ )
366
+ }
367
+ const s = StyleSheet.create({
368
+ container: { gap: 16 },
369
+ title: { fontSize: 20, fontWeight: '500' },
370
+ caption: { fontSize: 12, marginTop: 12 },
371
+ heading: { fontSize: 20, fontWeight: '500', lineHeight: 28 },
372
+ label: { fontSize: 16, lineHeight: 24 },
373
+ url: { fontSize: 12, lineHeight: 18 },
374
+ grow: { flex: 1 },
375
+ linkRow: {
376
+ minHeight: 52,
377
+ flexDirection: 'row',
378
+ alignItems: 'center',
379
+ gap: 12,
380
+ borderBottomWidth: StyleSheet.hairlineWidth,
381
+ },
382
+ rowAction: { minHeight: 52, flexDirection: 'row', alignItems: 'center', gap: 12 },
383
+ remove: { minWidth: 44, minHeight: 44, alignItems: 'center', justifyContent: 'center' },
384
+ fallbackIcon: { width: 20, textAlign: 'center', fontSize: 18 },
385
+ choice: {
386
+ minHeight: 56,
387
+ flexDirection: 'row',
388
+ justifyContent: 'space-between',
389
+ alignItems: 'center',
390
+ gap: 16,
391
+ borderBottomWidth: StyleSheet.hairlineWidth,
392
+ },
393
+ radio: {
394
+ width: 24,
395
+ height: 24,
396
+ borderWidth: 1.5,
397
+ borderRadius: 12,
398
+ alignItems: 'center',
399
+ justifyContent: 'center',
400
+ },
401
+ radioDot: { width: 12, height: 12, borderRadius: 6 },
402
+ help: { fontSize: 14, lineHeight: 21 },
403
+ footer: {
404
+ flexDirection: 'row',
405
+ justifyContent: 'space-between',
406
+ alignItems: 'center',
407
+ gap: 16,
408
+ marginTop: 20,
409
+ },
410
+ button: {
411
+ minHeight: 44,
412
+ minWidth: 80,
413
+ alignItems: 'center',
414
+ justifyContent: 'center',
415
+ borderWidth: 1,
416
+ borderRadius: 24,
417
+ paddingHorizontal: 18,
418
+ },
419
+ disabled: { opacity: 0.5 },
420
+ backdrop: { flex: 1, justifyContent: 'flex-end', backgroundColor: 'rgba(0,0,0,0.7)' },
421
+ scrim: { flex: 1 },
422
+ sheet: {
423
+ width: '100%',
424
+ maxWidth: 580,
425
+ alignSelf: 'center',
426
+ borderTopStartRadius: 24,
427
+ borderTopEndRadius: 24,
428
+ padding: 24,
429
+ maxHeight: '85%',
430
+ },
431
+ handle: {
432
+ width: 36,
433
+ height: 4,
434
+ borderRadius: 2,
435
+ alignSelf: 'center',
436
+ marginBottom: 24,
437
+ },
438
+ form: { gap: 16, paddingBottom: 16 },
439
+ input: {
440
+ borderWidth: 1,
441
+ borderRadius: 28,
442
+ minHeight: 52,
443
+ paddingHorizontal: 20,
444
+ fontSize: 16,
445
+ },
446
+ })