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,371 @@
1
+ import React from 'react'
2
+ import {
3
+ Linking,
4
+ StyleSheet,
5
+ Text,
6
+ View,
7
+ type StyleProp,
8
+ type ViewStyle,
9
+ } from 'react-native'
10
+ import { getConfig, hooks, phoneCallingCodeForInput, phoneCountryLabel, type PhoneAuthenticationFlow } from 'tf-checkout-shared'
11
+ import { LoginForm } from '../auth/LoginForm'
12
+ import { EmailCodeForm } from '../auth/EmailCodeForm'
13
+ import { ForgotPasswordForm } from '../auth/ForgotPasswordForm'
14
+ import { RegisterForm } from '../auth/RegisterForm'
15
+ import { TwoFactorForm } from '../auth/TwoFactorForm'
16
+ import { Button, ErrorText, MutedText } from '../ui/primitives'
17
+ import { Checkbox, Field, Input } from '../ui/Field'
18
+ import { Select, type SelectOption } from '../ui/Select'
19
+ import { FrozenFields } from '../ui/Frozen'
20
+ import { useTheme } from '../ui/CheckoutProvider'
21
+ import { useAccountT } from '../ui/useAccountT'
22
+
23
+ export interface PhoneAuthenticationProps {
24
+ flow: PhoneAuthenticationFlow
25
+ locale?: string
26
+ showHeader?: boolean
27
+ allowCreation?: boolean
28
+ allowPasswordReset?: boolean
29
+ countryOptions?: SelectOption<string>[]
30
+ onOpenTerms?: () => void
31
+ onOpenPrivacy?: () => void
32
+ style?: StyleProp<ViewStyle>
33
+ }
34
+
35
+ /** Native presentation over one common owner; no OTP, admission or linking policy here. */
36
+ export function PhoneAuthentication({
37
+ flow,
38
+ locale,
39
+ showHeader = true,
40
+ allowCreation = true,
41
+ allowPasswordReset = true,
42
+ countryOptions,
43
+ onOpenTerms,
44
+ onOpenPrivacy,
45
+ style,
46
+ }: PhoneAuthenticationProps) {
47
+ const t = useAccountT(locale)
48
+ const theme = useTheme()
49
+ const { state, emailAuth: auth } = flow
50
+ const countries = hooks.useCountries({
51
+ enabled: !countryOptions && state.phase === 'phone',
52
+ })
53
+ const choices =
54
+ countryOptions ??
55
+ (countries.data ?? []).flatMap((country) =>
56
+ typeof country.code === 'string' && /^[a-z]{2}$/i.test(country.code)
57
+ ? [{ value: country.code.toUpperCase(), label: phoneCountryLabel(country.name, country.code) }]
58
+ : []
59
+ )
60
+ const busy = state.busy || auth.isSubmitting
61
+ const callingCode = phoneCallingCodeForInput(state.phone)
62
+ const hasSelectedCountry = choices.some((choice) => choice.value === state.country)
63
+ const secondFactor = auth.view === 'two-factor' && Boolean(auth.twoFactorChallenge)
64
+ const heading = (label: string) =>
65
+ showHeader ? (
66
+ <Text style={[s.heading, { color: theme.text, fontFamily: theme.fontFamily }]}>
67
+ {t(label)}
68
+ </Text>
69
+ ) : null
70
+ const button = (label: string, onPress: () => void, disabled = busy) => (
71
+ <Button variant="ghost" onPress={onPress} disabled={disabled}>
72
+ {t(label)}
73
+ </Button>
74
+ )
75
+ const emailView = () => {
76
+ const completion = auth.accountCompletion
77
+ if (auth.view === 'two-factor' && auth.twoFactorChallenge)
78
+ return (
79
+ <TwoFactorForm
80
+ loading={auth.isSubmitting}
81
+ error={auth.error}
82
+ successMessage={auth.successMessage}
83
+ smsAvailable={auth.twoFactorChallenge.smsAvailable}
84
+ smsRetryAfter={auth.smsRetryAfter}
85
+ onSubmit={auth.submitTwoFactor}
86
+ onSendSms={auth.sendTwoFactorSms}
87
+ onCancel={auth.cancelTwoFactor}
88
+ />
89
+ )
90
+ if (auth.view === 'email-code')
91
+ return (
92
+ <EmailCodeForm
93
+ initialEmail={auth.suggestedEmail}
94
+ sent={auth.emailCodeSent}
95
+ target={auth.emailCodeTarget}
96
+ retryAfter={auth.emailRetryAfter}
97
+ loading={auth.isSubmitting}
98
+ error={auth.error}
99
+ successMessage={auth.successMessage}
100
+ onRequest={auth.requestEmailCode}
101
+ onVerify={auth.submitEmailCode}
102
+ onBack={() => auth.showLogin(auth.suggestedEmail)}
103
+ />
104
+ )
105
+ if (auth.view === 'forgot-password')
106
+ return (
107
+ <ForgotPasswordForm
108
+ initialEmail={auth.suggestedEmail}
109
+ errors={auth.fieldErrors}
110
+ error={auth.error}
111
+ successMessage={auth.successMessage}
112
+ loading={auth.isSubmitting}
113
+ onSubmit={auth.submitPasswordReset}
114
+ onBackToLogin={auth.showLogin}
115
+ onFieldChange={auth.clearFeedback}
116
+ />
117
+ )
118
+ if (auth.view === 'register' && allowCreation)
119
+ return (
120
+ <RegisterForm
121
+ initialEmail={auth.suggestedEmail}
122
+ errors={auth.fieldErrors}
123
+ error={auth.error}
124
+ loading={auth.isSubmitting}
125
+ requireAddress={auth.requireRegistrationAddress}
126
+ requireTerms={Boolean(auth.accountCompletion)}
127
+ completionRefresh={
128
+ completion
129
+ ? {
130
+ retryAfter: auth.emailRetryAfter,
131
+ onRequest: () => auth.requestEmailCode(completion.email),
132
+ onVerify: auth.submitEmailCode,
133
+ }
134
+ : undefined
135
+ }
136
+ onSubmit={auth.submitRegistration}
137
+ onFieldChange={auth.clearFieldError}
138
+ />
139
+ )
140
+ return (
141
+ <View style={s.panel}>
142
+ <LoginForm
143
+ initialEmail={auth.suggestedEmail}
144
+ errors={auth.fieldErrors}
145
+ error={auth.error}
146
+ loading={auth.isSubmitting}
147
+ allowPasswordReset={allowPasswordReset}
148
+ onSubmit={auth.submitLogin}
149
+ onForgotPassword={auth.showForgotPassword}
150
+ onFieldChange={auth.clearFieldError}
151
+ />
152
+ {auth.emailCodeAvailable
153
+ ? button('Sign in with an email code', () =>
154
+ auth.showEmailCode(auth.suggestedEmail)
155
+ )
156
+ : null}
157
+ {allowCreation
158
+ ? button('Create an account with email', flow.chooseEmailRegistration)
159
+ : null}
160
+ {auth.browserSignInAvailable
161
+ ? (['google', 'apple', 'facebook', 'passkey'] as const).map((provider) => (
162
+ <Button
163
+ key={provider}
164
+ variant="ghost"
165
+ disabled={busy}
166
+ onPress={() => void auth.submitBrowserSignIn(provider)}
167
+ >
168
+ {t(
169
+ provider === 'passkey'
170
+ ? 'Sign in with a passkey'
171
+ : provider === 'google'
172
+ ? 'Sign in with Google'
173
+ : provider === 'apple'
174
+ ? 'Sign in with Apple'
175
+ : 'Sign in with Facebook'
176
+ )}
177
+ </Button>
178
+ ))
179
+ : null}
180
+ </View>
181
+ )
182
+ }
183
+ return (
184
+ <View style={[s.panel, style]}>
185
+ {state.error ? <ErrorText>{t(state.error)}</ErrorText> : null}
186
+ {state.successMessage ? <MutedText>{t(state.successMessage)}</MutedText> : null}
187
+ {!secondFactor && state.phase === 'phone' ? (
188
+ <View style={s.panel}>
189
+ {heading('Sign in or sign up')}
190
+ <MutedText>
191
+ {t(
192
+ 'We’ll send a one-time SMS code. You can still use email to access your existing account.'
193
+ )}
194
+ </MutedText>
195
+ <Field label={t('Country')}>
196
+ <Select
197
+ value={state.country}
198
+ options={choices}
199
+ disabled={busy}
200
+ onChange={flow.setCountry}
201
+ placeholder={!hasSelectedCountry && callingCode ? `+${callingCode}` : t('Country')}
202
+ />
203
+ </Field>
204
+ {countries.isError && !countryOptions ? (
205
+ <View>
206
+ <ErrorText>{t('Unable to load countries. Please try again.')}</ErrorText>
207
+ {button('Try again', () => void countries.refetch())}
208
+ </View>
209
+ ) : null}
210
+ <Field label={t('Phone number')} required>
211
+ <Input
212
+ value={state.phone}
213
+ onChangeText={flow.setPhone}
214
+ accessibilityLabel={t('Phone number')}
215
+ placeholder="+1 202 555 0123"
216
+ keyboardType="phone-pad"
217
+ textContentType="telephoneNumber"
218
+ autoComplete="tel"
219
+ editable={!busy}
220
+ onSubmitEditing={() => void flow.requestCode()}
221
+ />
222
+ </Field>
223
+ <Button
224
+ disabled={busy || !state.phone.trim()}
225
+ loading={state.busy}
226
+ onPress={() => void flow.requestCode()}
227
+ >
228
+ {t('Send code')}
229
+ </Button>
230
+ {button('Continue with Email', flow.chooseEmail)}
231
+ </View>
232
+ ) : null}
233
+ {!secondFactor && state.phase === 'code' ? (
234
+ <View style={s.panel}>
235
+ {heading('Verify your phone')}
236
+ <MutedText>
237
+ {t('Enter the six-digit code sent to {{phone}}.', { phone: state.phone })}
238
+ </MutedText>
239
+ <Field label={t('Verification code')} required>
240
+ <Input
241
+ value={state.code}
242
+ onChangeText={flow.setCode}
243
+ accessibilityLabel={t('Verification code')}
244
+ keyboardType="number-pad"
245
+ textContentType="oneTimeCode"
246
+ autoComplete="one-time-code"
247
+ maxLength={6}
248
+ editable={!busy}
249
+ onSubmitEditing={() => void flow.verifyCode()}
250
+ />
251
+ </Field>
252
+ <Button
253
+ loading={state.busy}
254
+ disabled={busy || !/^\d{6}$/.test(state.code)}
255
+ onPress={() => void flow.verifyCode()}
256
+ >
257
+ {t('Verify code')}
258
+ </Button>
259
+ {button(
260
+ state.retryAfter > 0
261
+ ? t('Resend in {{seconds}}s', { seconds: state.retryAfter })
262
+ : 'Resend code',
263
+ () => void flow.requestCode(),
264
+ busy || state.retryAfter > 0
265
+ )}
266
+ {button('Change phone number', flow.back)}
267
+ {button('Continue with Email', flow.chooseEmail)}
268
+ </View>
269
+ ) : null}
270
+ {!secondFactor && state.phase === 'choose_account' ? (
271
+ <View style={s.panel}>
272
+ {heading('Your number is verified')}
273
+ <MutedText>
274
+ {t(
275
+ 'Already have a Ticket Fairy account? Sign in with email to keep your tickets and account together.'
276
+ )}
277
+ </MutedText>
278
+ <Button disabled={busy} onPress={flow.chooseEmail}>
279
+ {t('Sign in to my existing account')}
280
+ </Button>
281
+ {allowCreation ? (
282
+ <>
283
+ <MutedText>
284
+ {t(
285
+ 'New to Ticket Fairy? Create an account with this verified number. You can add email later.'
286
+ )}
287
+ </MutedText>
288
+ <FrozenFields frozen={busy}>
289
+ <Checkbox
290
+ checked={state.termsAccepted}
291
+ onChange={flow.setTermsAccepted}
292
+ label={t(
293
+ 'By creating an account, I accept the Terms of Service and Privacy Policy.'
294
+ )}
295
+ />
296
+ </FrozenFields>
297
+ <View style={s.links}>
298
+ {button('Terms of Service', () =>
299
+ onOpenTerms
300
+ ? onOpenTerms()
301
+ : void Linking.openURL(getConfig().host + '/terms-of-service')
302
+ )}
303
+ {button('Privacy Policy', () =>
304
+ onOpenPrivacy
305
+ ? onOpenPrivacy()
306
+ : void Linking.openURL(getConfig().host + '/privacy')
307
+ )}
308
+ </View>
309
+ <Button
310
+ disabled={busy || !state.termsAccepted}
311
+ loading={state.busy}
312
+ onPress={() => void flow.createAccount()}
313
+ >
314
+ {t('Create a new account')}
315
+ </Button>
316
+ </>
317
+ ) : null}
318
+ </View>
319
+ ) : null}
320
+ {secondFactor || state.phase === 'email' ? (
321
+ <View style={s.panel}>
322
+ {heading(
323
+ secondFactor
324
+ ? 'Verify your sign in'
325
+ : auth.view === 'forgot-password'
326
+ ? 'Password reset'
327
+ : auth.view === 'register'
328
+ ? 'Create an account with email'
329
+ : 'Sign in to your existing account'
330
+ )}
331
+ {emailView()}
332
+ {!secondFactor ? button('Continue with phone', flow.back) : null}
333
+ </View>
334
+ ) : null}
335
+ {!secondFactor && state.phase === 'confirm_link' ? (
336
+ <View style={s.panel}>
337
+ {heading('Link your phone number')}
338
+ <MutedText>
339
+ {state.linkAvailable === false
340
+ ? t('You are signed in. Verify your phone again to link this number.')
341
+ : t(
342
+ 'Link {{phone}} to {{account}} so you can sign in to the same account with your phone.',
343
+ { phone: state.phone, account: state.linkAccountLabel ?? '' }
344
+ )}
345
+ </MutedText>
346
+ {state.linkAvailable !== false ? (
347
+ <Button
348
+ disabled={busy}
349
+ loading={state.busy}
350
+ onPress={() => void flow.confirmLink()}
351
+ >
352
+ {t('Link this number')}
353
+ </Button>
354
+ ) : (
355
+ button('Verify your phone again', flow.back)
356
+ )}
357
+ {button('Not now', flow.skipLink)}
358
+ </View>
359
+ ) : null}
360
+ {!secondFactor && state.phase === 'signed_in' ? (
361
+ <MutedText>{t('You are signed in.')}</MutedText>
362
+ ) : null}
363
+ {state.phase !== 'signed_in' ? button('Cancel', flow.cancel) : null}
364
+ </View>
365
+ )
366
+ }
367
+ const s = StyleSheet.create({
368
+ panel: { gap: 16 },
369
+ heading: { fontSize: 26, fontWeight: '500' },
370
+ links: { flexDirection: 'row', flexWrap: 'wrap', gap: 12 },
371
+ })
@@ -0,0 +1,122 @@
1
+ import React, { type ReactNode } from 'react'
2
+ import { Pressable, StyleSheet, Text, View } from 'react-native'
3
+ import { hooks, type WordPostReactionFlow } from 'tf-checkout-shared'
4
+ import { useAccountT } from '../ui/useAccountT'
5
+
6
+ export interface PostReactionControlsProps {
7
+ flow: WordPostReactionFlow
8
+ onSignIn?: () => void
9
+ renderIcon?: (choice: 'up' | 'down') => ReactNode
10
+ translate?: (english: string) => string
11
+ showFeedback?: boolean
12
+ }
13
+
14
+ /** Native hosts may supply their own icon assets without introducing a native dependency. */
15
+ export const PostReactionControls = ({
16
+ flow,
17
+ onSignIn,
18
+ renderIcon,
19
+ translate,
20
+ showFeedback = true,
21
+ }: PostReactionControlsProps) => {
22
+ const accountT = useAccountT()
23
+ const t = translate ?? accountT
24
+ const unavailable = flow.signedIn && !flow.query.data
25
+ return (
26
+ <View style={styles.root}>
27
+ <View style={styles.buttons} accessibilityLabel={t('Article reaction')}>
28
+ {(['up', 'down'] as const).map((choice) => (
29
+ <Pressable
30
+ key={choice}
31
+ accessibilityRole="button"
32
+ accessibilityLabel={t(choice === 'up' ? 'Like article' : 'Dislike article')}
33
+ accessibilityState={{
34
+ selected: flow.reaction === choice,
35
+ disabled: flow.busy || unavailable,
36
+ }}
37
+ disabled={flow.busy || unavailable}
38
+ style={[
39
+ styles.button,
40
+ flow.reaction === choice && styles.selected,
41
+ (flow.busy || unavailable) && styles.disabled,
42
+ ]}
43
+ onPress={() =>
44
+ flow.signedIn ? void flow.toggleReaction(choice) : onSignIn?.()
45
+ }
46
+ >
47
+ {renderIcon ? (
48
+ renderIcon(choice)
49
+ ) : (
50
+ <Text style={styles.icon}>{choice === 'up' ? '👍' : '👎'}</Text>
51
+ )}
52
+ </Pressable>
53
+ ))}
54
+ </View>
55
+ {showFeedback && <PostReactionFeedback flow={flow} translate={translate} />}
56
+ </View>
57
+ )
58
+ }
59
+
60
+ export const PostReactionFeedback = ({
61
+ flow,
62
+ translate,
63
+ }: Pick<PostReactionControlsProps, 'flow' | 'translate'>) => {
64
+ const accountT = useAccountT()
65
+ const t = translate ?? accountT
66
+ return (
67
+ <>
68
+ {flow.busy && (
69
+ <Text style={styles.message} accessibilityLiveRegion="polite">
70
+ {t('Saving reaction…')}
71
+ </Text>
72
+ )}
73
+ {(flow.error || flow.query.error) && (
74
+ <View style={styles.error}>
75
+ <Text style={styles.message} accessibilityRole="alert">
76
+ {t(
77
+ flow.error ||
78
+ flow.query.error?.message ||
79
+ 'Your reaction could not be saved. Please try again.'
80
+ )}
81
+ </Text>
82
+ <Pressable
83
+ accessibilityRole="button"
84
+ disabled={flow.busy}
85
+ onPress={() => void flow.retry()}
86
+ >
87
+ <Text style={styles.retry}>{t('Try again')}</Text>
88
+ </Pressable>
89
+ </View>
90
+ )}
91
+ </>
92
+ )
93
+ }
94
+
95
+ export const PostReaction = ({
96
+ postId,
97
+ ...props
98
+ }: Omit<PostReactionControlsProps, 'flow'> & { postId: string | number }) => {
99
+ const flow = hooks.useWordPostReaction(postId)
100
+ return <PostReactionControls {...props} flow={flow} />
101
+ }
102
+
103
+ const styles = StyleSheet.create({
104
+ root: { gap: 8 },
105
+ buttons: { flexDirection: 'row', gap: 8 },
106
+ button: {
107
+ width: 42,
108
+ height: 42,
109
+ borderRadius: 999,
110
+ alignItems: 'center',
111
+ justifyContent: 'center',
112
+ backgroundColor: 'rgba(11,10,21,.6)',
113
+ borderWidth: 1,
114
+ borderColor: 'rgba(196,181,255,.12)',
115
+ },
116
+ selected: { backgroundColor: '#6040B8', borderColor: '#C4B5FF' },
117
+ disabled: { opacity: 0.5 },
118
+ icon: { fontSize: 18 },
119
+ message: { color: '#BBBFF4', fontSize: 13 },
120
+ error: { gap: 6, maxWidth: 260 },
121
+ retry: { color: '#C4B5FF', textDecorationLine: 'underline' },
122
+ })