tf-checkout-react 2.0.0-beta.0 → 2.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +16 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.esm.js +550 -287
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +550 -287
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/EmbeddedCheckout.tsx +6 -1
- package/src/auth/AuthModal.tsx +12 -5
- package/src/auth/ForgotPasswordForm.tsx +4 -6
- package/src/auth/LoginForm.tsx +2 -2
- package/src/auth/RegisterForm.tsx +2 -2
- package/src/auth/ResetPasswordForm.tsx +7 -7
- package/src/configure.ts +1 -5
- package/src/confirmer.ts +1 -5
- package/src/sections/AddOns.tsx +14 -7
- package/src/sections/BillingInfo.tsx +107 -53
- package/src/sections/Confirmation.tsx +8 -6
- package/src/sections/OrderSummary.tsx +7 -5
- package/src/sections/Payment.tsx +37 -17
- package/src/sections/Tickets.tsx +32 -15
- package/src/sections/billing/BillingAddressFields.tsx +19 -13
- package/src/sections/billing/TicketHoldersCard.tsx +103 -89
- package/src/sections/billing/fieldRules.ts +43 -0
- package/src/ui/CheckoutProvider.tsx +5 -1
- package/src/ui/InstallmentPicker.tsx +4 -2
- package/src/ui/PaymentMethods.tsx +82 -76
- package/src/ui/TimerBanner.tsx +7 -3
package/package.json
CHANGED
package/src/EmbeddedCheckout.tsx
CHANGED
|
@@ -372,7 +372,12 @@ export const EmbeddedCheckout: FC<EmbeddedCheckoutProps> = ({
|
|
|
372
372
|
messages,
|
|
373
373
|
className,
|
|
374
374
|
}) => (
|
|
375
|
-
<CheckoutProvider
|
|
375
|
+
<CheckoutProvider
|
|
376
|
+
theme={theme}
|
|
377
|
+
locale={locale}
|
|
378
|
+
messages={messages}
|
|
379
|
+
className={className}
|
|
380
|
+
>
|
|
376
381
|
<Flow
|
|
377
382
|
key={eventId}
|
|
378
383
|
eventId={eventId}
|
package/src/auth/AuthModal.tsx
CHANGED
|
@@ -62,6 +62,13 @@ export const AuthModal: FC<AuthModalProps> = ({
|
|
|
62
62
|
const previouslyFocused = useRef<HTMLElement | null>(null)
|
|
63
63
|
const submittingRef = useRef(auth.isSubmitting)
|
|
64
64
|
submittingRef.current = auth.isSubmitting
|
|
65
|
+
// Consumers pass `onClose` as an inline arrow, so its identity changes on
|
|
66
|
+
// every parent render — and the parent re-renders once a second while the
|
|
67
|
+
// reservation timer counts down. Held in a ref so the setup effect below can
|
|
68
|
+
// depend on `open` alone; with `onClose` in its deps it tore down and re-ran
|
|
69
|
+
// each tick, stealing focus back to the first field mid-typing.
|
|
70
|
+
const onCloseRef = useRef(onClose)
|
|
71
|
+
onCloseRef.current = onClose
|
|
65
72
|
|
|
66
73
|
useEffect(() => {
|
|
67
74
|
if (!open) return
|
|
@@ -83,7 +90,7 @@ export const AuthModal: FC<AuthModalProps> = ({
|
|
|
83
90
|
initialFocus?.focus()
|
|
84
91
|
|
|
85
92
|
const onKeyDown = (event: KeyboardEvent) => {
|
|
86
|
-
if (event.key === 'Escape' && !submittingRef.current)
|
|
93
|
+
if (event.key === 'Escape' && !submittingRef.current) onCloseRef.current()
|
|
87
94
|
if (event.key !== 'Tab') return
|
|
88
95
|
const elements = focusable()
|
|
89
96
|
if (elements.length === 0) {
|
|
@@ -107,7 +114,7 @@ export const AuthModal: FC<AuthModalProps> = ({
|
|
|
107
114
|
document.body.style.overflow = previousOverflow
|
|
108
115
|
previouslyFocused.current?.focus()
|
|
109
116
|
}
|
|
110
|
-
}, [
|
|
117
|
+
}, [open])
|
|
111
118
|
|
|
112
119
|
if (!open) return null
|
|
113
120
|
|
|
@@ -115,7 +122,7 @@ export const AuthModal: FC<AuthModalProps> = ({
|
|
|
115
122
|
if (event.target === event.currentTarget && !auth.isSubmitting) onClose()
|
|
116
123
|
}
|
|
117
124
|
const isPasswordReset = auth.view === 'forgot-password'
|
|
118
|
-
const title = isPasswordReset ? '
|
|
125
|
+
const title = isPasswordReset ? t('auth.passwordReset') : t('auth.title')
|
|
119
126
|
|
|
120
127
|
return (
|
|
121
128
|
<div className={cx('tf-checkout-auth', className)} onMouseDown={closeFromBackdrop}>
|
|
@@ -153,7 +160,7 @@ export const AuthModal: FC<AuthModalProps> = ({
|
|
|
153
160
|
)}
|
|
154
161
|
onClick={() => auth.showLogin(auth.suggestedEmail)}
|
|
155
162
|
>
|
|
156
|
-
|
|
163
|
+
{t('auth.logIn')}
|
|
157
164
|
</button>
|
|
158
165
|
<button
|
|
159
166
|
type="button"
|
|
@@ -165,7 +172,7 @@ export const AuthModal: FC<AuthModalProps> = ({
|
|
|
165
172
|
)}
|
|
166
173
|
onClick={() => auth.showRegister(auth.suggestedEmail)}
|
|
167
174
|
>
|
|
168
|
-
|
|
175
|
+
{t('auth.signUp')}
|
|
169
176
|
</button>
|
|
170
177
|
</div>
|
|
171
178
|
)}
|
|
@@ -47,7 +47,7 @@ export const ForgotPasswordForm: FC<ForgotPasswordFormProps> = ({
|
|
|
47
47
|
{successMessage}
|
|
48
48
|
</div>
|
|
49
49
|
<Button type="button" block onClick={() => onBackToLogin(email)}>
|
|
50
|
-
|
|
50
|
+
{t('auth.backToLogin')}
|
|
51
51
|
</Button>
|
|
52
52
|
</div>
|
|
53
53
|
)
|
|
@@ -55,9 +55,7 @@ export const ForgotPasswordForm: FC<ForgotPasswordFormProps> = ({
|
|
|
55
55
|
|
|
56
56
|
return (
|
|
57
57
|
<form className="tf-checkout-auth__form" onSubmit={submit} noValidate>
|
|
58
|
-
<p className="tf-checkout-auth__intro">
|
|
59
|
-
Enter your email and we’ll send you instructions to reset your password.
|
|
60
|
-
</p>
|
|
58
|
+
<p className="tf-checkout-auth__intro">{t('auth.forgotIntro')}</p>
|
|
61
59
|
{error && (
|
|
62
60
|
<div className="tf-checkout-auth__alert" role="alert">
|
|
63
61
|
{error}
|
|
@@ -79,14 +77,14 @@ export const ForgotPasswordForm: FC<ForgotPasswordFormProps> = ({
|
|
|
79
77
|
/>
|
|
80
78
|
</Field>
|
|
81
79
|
<Button type="submit" block loading={loading}>
|
|
82
|
-
|
|
80
|
+
{t('auth.sendResetInstructions')}
|
|
83
81
|
</Button>
|
|
84
82
|
<button
|
|
85
83
|
type="button"
|
|
86
84
|
className="tf-checkout-auth__text-button tf-checkout-auth__back"
|
|
87
85
|
onClick={() => onBackToLogin(email)}
|
|
88
86
|
>
|
|
89
|
-
|
|
87
|
+
{t('auth.backToLogin')}
|
|
90
88
|
</button>
|
|
91
89
|
</form>
|
|
92
90
|
)
|
package/src/auth/LoginForm.tsx
CHANGED
|
@@ -88,7 +88,7 @@ export const LoginForm: FC<LoginFormProps> = ({
|
|
|
88
88
|
/>
|
|
89
89
|
</Field>
|
|
90
90
|
<Button type="submit" block loading={loading}>
|
|
91
|
-
|
|
91
|
+
{t('auth.logIn')}
|
|
92
92
|
</Button>
|
|
93
93
|
{allowPasswordReset && onForgotPassword && (
|
|
94
94
|
<button
|
|
@@ -96,7 +96,7 @@ export const LoginForm: FC<LoginFormProps> = ({
|
|
|
96
96
|
className="tf-checkout-auth__text-button tf-checkout-auth__forgot"
|
|
97
97
|
onClick={() => onForgotPassword(email)}
|
|
98
98
|
>
|
|
99
|
-
|
|
99
|
+
{t('auth.forgotPassword')}
|
|
100
100
|
</button>
|
|
101
101
|
)}
|
|
102
102
|
</form>
|
|
@@ -236,7 +236,7 @@ export const RegisterForm: FC<RegisterFormProps> = ({
|
|
|
236
236
|
onChange={(event) => setField('stateId', event.target.value)}
|
|
237
237
|
>
|
|
238
238
|
<option value="">
|
|
239
|
-
{states.length > 0 ? '
|
|
239
|
+
{states.length > 0 ? t('auth.selectState') : t('auth.notRequired')}
|
|
240
240
|
</option>
|
|
241
241
|
{states.map((state) => (
|
|
242
242
|
<option key={state.id} value={state.id}>
|
|
@@ -326,7 +326,7 @@ export const RegisterForm: FC<RegisterFormProps> = ({
|
|
|
326
326
|
</Field>
|
|
327
327
|
</FieldRow>
|
|
328
328
|
<Button type="submit" block loading={loading}>
|
|
329
|
-
|
|
329
|
+
{t('auth.createAccount')}
|
|
330
330
|
</Button>
|
|
331
331
|
</form>
|
|
332
332
|
)
|
|
@@ -28,14 +28,14 @@ export const ResetPasswordForm: FC<ResetPasswordFormProps> = ({
|
|
|
28
28
|
const validation = useMemo(() => {
|
|
29
29
|
if (!submitted) return {}
|
|
30
30
|
return {
|
|
31
|
-
token: token ? '' : '
|
|
31
|
+
token: token ? '' : t('auth.missingToken'),
|
|
32
32
|
password:
|
|
33
33
|
password.length < 8
|
|
34
|
-
? '
|
|
34
|
+
? t('auth.passwordTooShort')
|
|
35
35
|
: !/[@$!%*#?&]/.test(password)
|
|
36
|
-
? '
|
|
36
|
+
? t('auth.passwordNeedsSpecial')
|
|
37
37
|
: '',
|
|
38
|
-
confirmation: password !== confirmation ? '
|
|
38
|
+
confirmation: password !== confirmation ? t('auth.passwordsMustMatch') : '',
|
|
39
39
|
}
|
|
40
40
|
}, [confirmation, password, submitted, token])
|
|
41
41
|
|
|
@@ -58,11 +58,11 @@ export const ResetPasswordForm: FC<ResetPasswordFormProps> = ({
|
|
|
58
58
|
return (
|
|
59
59
|
<div className="tf-checkout-auth__form">
|
|
60
60
|
<div className="tf-checkout-auth__success" role="status">
|
|
61
|
-
|
|
61
|
+
{t('auth.passwordChanged')}
|
|
62
62
|
</div>
|
|
63
63
|
{onBackToLogin && (
|
|
64
64
|
<Button type="button" block onClick={onBackToLogin}>
|
|
65
|
-
|
|
65
|
+
{t('auth.logIn')}
|
|
66
66
|
</Button>
|
|
67
67
|
)}
|
|
68
68
|
</div>
|
|
@@ -105,7 +105,7 @@ export const ResetPasswordForm: FC<ResetPasswordFormProps> = ({
|
|
|
105
105
|
/>
|
|
106
106
|
</Field>
|
|
107
107
|
<Button type="submit" block loading={reset.isPending} disabled={!token}>
|
|
108
|
-
|
|
108
|
+
{t('common.submit')}
|
|
109
109
|
</Button>
|
|
110
110
|
</form>
|
|
111
111
|
)
|
package/src/configure.ts
CHANGED
|
@@ -5,11 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* configureCheckout({ env: 'PROD', clientId, clientSecret })
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
9
|
-
configure,
|
|
10
|
-
type StorageAdapter,
|
|
11
|
-
type TtfConfig,
|
|
12
|
-
} from 'tf-checkout-shared'
|
|
8
|
+
import { configure, type StorageAdapter, type TtfConfig } from 'tf-checkout-shared'
|
|
13
9
|
|
|
14
10
|
const isBrowser =
|
|
15
11
|
typeof window !== 'undefined' && typeof window.localStorage !== 'undefined'
|
package/src/confirmer.ts
CHANGED
|
@@ -17,11 +17,7 @@ import {
|
|
|
17
17
|
type StripeCardElement,
|
|
18
18
|
type StripeElements,
|
|
19
19
|
} from '@stripe/stripe-js'
|
|
20
|
-
import type {
|
|
21
|
-
CheckoutBilling,
|
|
22
|
-
CheckoutPorts,
|
|
23
|
-
PaymentConfirmer,
|
|
24
|
-
} from 'tf-checkout-shared'
|
|
20
|
+
import type { CheckoutBilling, CheckoutPorts, PaymentConfirmer } from 'tf-checkout-shared'
|
|
25
21
|
|
|
26
22
|
import type { WebXenditTokenizer } from './xendit'
|
|
27
23
|
|
package/src/sections/AddOns.tsx
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
import { Button } from '../ui/Button'
|
|
22
22
|
import { Card } from '../ui/Card'
|
|
23
23
|
import { useCheckoutContext } from '../ui/CheckoutProvider'
|
|
24
|
+
import { useT } from '../ui/I18nProvider'
|
|
24
25
|
import { CustomFieldGroupFields } from '../ui/CustomFields'
|
|
25
26
|
import { Loader } from '../ui/Loader'
|
|
26
27
|
import { PriceLine } from '../ui/PriceLine'
|
|
@@ -45,12 +46,16 @@ export const AddOns: FC<AddOnsProps> = ({
|
|
|
45
46
|
eventId,
|
|
46
47
|
onContinue,
|
|
47
48
|
onSkip,
|
|
48
|
-
buttonLabel
|
|
49
|
-
eyebrow
|
|
50
|
-
heading
|
|
49
|
+
buttonLabel,
|
|
50
|
+
eyebrow,
|
|
51
|
+
heading,
|
|
51
52
|
className,
|
|
52
53
|
}) => {
|
|
54
|
+
const t = useT()
|
|
53
55
|
const checkout = useCheckoutContext()
|
|
56
|
+
const buttonText = buttonLabel ?? t('addons.confirmSelection')
|
|
57
|
+
const eyebrowText = eyebrow ?? t('addons.eyebrow')
|
|
58
|
+
const headingText = heading ?? t('addons.heading')
|
|
54
59
|
// No onContinue ⇒ combined form (inside <EmbeddedCheckout/>): apply the selection live
|
|
55
60
|
// (no "confirm" click) — the old checkout applied add-ons at checkout too.
|
|
56
61
|
const combined = !onContinue
|
|
@@ -97,8 +102,8 @@ export const AddOns: FC<AddOnsProps> = ({
|
|
|
97
102
|
const hasRequired = (checkout.requiredAddOns ?? []).some((a) => a.required)
|
|
98
103
|
const head = (
|
|
99
104
|
<div className="tf-checkout-addons__head">
|
|
100
|
-
{
|
|
101
|
-
<span className="tf-checkout-addons__heading">{
|
|
105
|
+
{eyebrowText && <span className="tf-checkout-addons__eyebrow">{eyebrowText}</span>}
|
|
106
|
+
<span className="tf-checkout-addons__heading">{headingText}</span>
|
|
102
107
|
{onSkip && !hasRequired && (
|
|
103
108
|
<button type="button" className="tf-checkout-addons__skip" onClick={onSkip}>
|
|
104
109
|
Skip
|
|
@@ -190,7 +195,9 @@ export const AddOns: FC<AddOnsProps> = ({
|
|
|
190
195
|
)}
|
|
191
196
|
</div>
|
|
192
197
|
{soldOut(a) ? (
|
|
193
|
-
<span className="tf-checkout-tickets__soldout">
|
|
198
|
+
<span className="tf-checkout-tickets__soldout">
|
|
199
|
+
{t('tickets.soldOut')}
|
|
200
|
+
</span>
|
|
194
201
|
) : (
|
|
195
202
|
<QuantitySelect
|
|
196
203
|
value={n}
|
|
@@ -227,7 +234,7 @@ export const AddOns: FC<AddOnsProps> = ({
|
|
|
227
234
|
loading={checkout.isBusy}
|
|
228
235
|
disabled={checkout.isBusy || selectedCount === 0}
|
|
229
236
|
>
|
|
230
|
-
{checkout.isBusy ? '
|
|
237
|
+
{checkout.isBusy ? t('addons.updating') : buttonText}
|
|
231
238
|
</Button>
|
|
232
239
|
)}
|
|
233
240
|
{missing.length > 0 && (
|
|
@@ -34,10 +34,12 @@ import { PhoneInput, type CountryIso2 } from 'react-international-phone'
|
|
|
34
34
|
import { Button } from '../ui/Button'
|
|
35
35
|
import { Card, SectionHeader } from '../ui/Card'
|
|
36
36
|
import { useCheckoutContext, useCheckoutValidation } from '../ui/CheckoutProvider'
|
|
37
|
+
import { useT } from '../ui/I18nProvider'
|
|
37
38
|
import { CustomFieldInput } from '../ui/CustomFields'
|
|
38
39
|
import { Checkbox, Field, FieldRow, Input } from '../ui/Field'
|
|
39
40
|
import { BillingAddressFields } from './billing/BillingAddressFields'
|
|
40
41
|
import { BuyerField } from './billing/BuyerField'
|
|
42
|
+
import { DOB_MIN, isBlankPhone, toDateInputValue, todayISO } from './billing/fieldRules'
|
|
41
43
|
import { TicketHoldersCard } from './billing/TicketHoldersCard'
|
|
42
44
|
import type { BillingDraft, HolderDraft } from './billing/types'
|
|
43
45
|
|
|
@@ -84,22 +86,26 @@ export interface CheckoutPrivacyPolicy {
|
|
|
84
86
|
export const BillingInfo: FC<BillingInfoProps> = ({
|
|
85
87
|
eventId,
|
|
86
88
|
onContinue,
|
|
87
|
-
heading
|
|
88
|
-
buttonLabel
|
|
89
|
+
heading,
|
|
90
|
+
buttonLabel,
|
|
89
91
|
onLogin,
|
|
90
92
|
skipWhenProfileComplete = false,
|
|
91
93
|
requireEmailConfirmation = false,
|
|
92
94
|
collectTicketHolders = false,
|
|
93
95
|
collectHolderContact = false,
|
|
94
|
-
ttfOptInLabel
|
|
96
|
+
ttfOptInLabel,
|
|
95
97
|
brandOptInLabel,
|
|
96
98
|
privacyPolicy,
|
|
97
99
|
showPrivacyError = false,
|
|
98
100
|
step,
|
|
99
101
|
className,
|
|
100
102
|
}) => {
|
|
103
|
+
const t = useT()
|
|
101
104
|
const checkout = useCheckoutContext()
|
|
102
105
|
const validation = useCheckoutValidation()
|
|
106
|
+
const headingText = heading ?? t('billing.heading')
|
|
107
|
+
const buttonText = buttonLabel ?? t('billing.continueToPayment')
|
|
108
|
+
const ttfOptInText = ttfOptInLabel ?? t('billing.ttfOptIn')
|
|
103
109
|
const profile = hooks.useProfile({ retry: false })
|
|
104
110
|
const event = hooks.useEvent(eventId)
|
|
105
111
|
// No onContinue ⇒ this is one combined form (inside <EmbeddedCheckout/>): mirror the
|
|
@@ -199,7 +205,8 @@ export const BillingInfo: FC<BillingInfoProps> = ({
|
|
|
199
205
|
email: prev.email || profileBilling.email || '',
|
|
200
206
|
confirmEmail:
|
|
201
207
|
prev.confirmEmail || (checkout.isLoggedIn ? profileBilling.email : '') || '',
|
|
202
|
-
phone:
|
|
208
|
+
phone:
|
|
209
|
+
(isBlankPhone(prev.phone) ? undefined : prev.phone) || profileBilling.phone,
|
|
203
210
|
streetAddress: prev.streetAddress || profileBilling.streetAddress,
|
|
204
211
|
city: prev.city || profileBilling.city,
|
|
205
212
|
zip: prev.zip || profileBilling.zip,
|
|
@@ -209,6 +216,16 @@ export const BillingInfo: FC<BillingInfoProps> = ({
|
|
|
209
216
|
const keys = Object.keys(next) as (keyof BillingDraft)[]
|
|
210
217
|
return keys.every((k) => next[k] === prev[k]) ? prev : next
|
|
211
218
|
})
|
|
219
|
+
// Date of birth lives outside `billing`, so it has to be seeded here too —
|
|
220
|
+
// and it has to be here rather than in its own effect or in the useState
|
|
221
|
+
// initialiser: the initialiser runs before the profile resolves, and a
|
|
222
|
+
// separate effect would re-fire and overwrite the buyer, since the live
|
|
223
|
+
// commit below depends on `dob`. This effect already runs exactly once.
|
|
224
|
+
setDob((prev) => {
|
|
225
|
+
if (prev) return prev
|
|
226
|
+
const { dobYear, dobMonth, dobDay } = profileBilling
|
|
227
|
+
return toDateInputValue(dobYear, dobMonth, dobDay) ?? prev
|
|
228
|
+
})
|
|
212
229
|
}, [profile.data, checkout.isLoggedIn])
|
|
213
230
|
|
|
214
231
|
// Resize holders to the slot count, preserving typed values; tag ticket type.
|
|
@@ -297,13 +314,22 @@ export const BillingInfo: FC<BillingInfoProps> = ({
|
|
|
297
314
|
countries.find((country) => country.code?.toLowerCase() === 'us')
|
|
298
315
|
if (!preferred) return
|
|
299
316
|
countryDefaultApplied.current = true
|
|
300
|
-
setBilling((current) =>
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
317
|
+
setBilling((current) => {
|
|
318
|
+
// The profile-seed effect above can run in this same commit, so the
|
|
319
|
+
// `billing` this effect closed over is stale — it still shows no country
|
|
320
|
+
// even though the seed just supplied one (with its state). Re-check the
|
|
321
|
+
// live value here: without it the default overwrote the seeded country
|
|
322
|
+
// and blanked `stateId`, which is why a returning buyer's State/County
|
|
323
|
+
// came back empty while every other saved field prefilled.
|
|
324
|
+
if (current.countryId) return current
|
|
325
|
+
return {
|
|
326
|
+
...current,
|
|
327
|
+
countryId: String(preferred.id),
|
|
328
|
+
countryCode: preferred.code?.toUpperCase(),
|
|
329
|
+
stateId: undefined,
|
|
330
|
+
stateCode: undefined,
|
|
331
|
+
}
|
|
332
|
+
})
|
|
307
333
|
}, [
|
|
308
334
|
billing.countryId,
|
|
309
335
|
countries,
|
|
@@ -342,28 +368,41 @@ export const BillingInfo: FC<BillingInfoProps> = ({
|
|
|
342
368
|
const patchHolder = (i: number, patch: Partial<HolderDraft>) =>
|
|
343
369
|
setHolders((hs) => hs.map((h, j) => (j === i ? { ...h, ...patch } : h)))
|
|
344
370
|
|
|
371
|
+
const dobMax = useMemo(todayISO, [])
|
|
345
372
|
const [dobYear, dobMonth, dobDay] = dob ? dob.split('-').map(Number) : []
|
|
373
|
+
// A dial code on its own is not a phone number — see `isBlankPhone`.
|
|
374
|
+
const buyerPhone = isBlankPhone(billing.phone) ? undefined : billing.phone
|
|
346
375
|
const committedBilling: CheckoutBilling = {
|
|
347
376
|
...checkout.billing,
|
|
348
377
|
...billing,
|
|
378
|
+
phone: buyerPhone,
|
|
349
379
|
stateRequired: gates.showAddress && Boolean(billing.countryId) && states.length > 0,
|
|
350
380
|
dataCapture,
|
|
351
381
|
dobDay,
|
|
352
382
|
dobMonth,
|
|
353
383
|
dobYear,
|
|
354
384
|
}
|
|
355
|
-
const committedHolders: CheckoutHolder[] = holders.map((holder) =>
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
:
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
385
|
+
const committedHolders: CheckoutHolder[] = holders.map((holder) => {
|
|
386
|
+
const holderPhone = isBlankPhone(holder.phone) ? '' : holder.phone
|
|
387
|
+
return {
|
|
388
|
+
firstName: holderNamesRequired
|
|
389
|
+
? holder.firstName
|
|
390
|
+
: holder.firstName || billing.firstName,
|
|
391
|
+
lastName: holderNamesRequired
|
|
392
|
+
? holder.lastName
|
|
393
|
+
: holder.lastName || billing.lastName,
|
|
394
|
+
email:
|
|
395
|
+
showHolders && collectHolderContact
|
|
396
|
+
? holder.email
|
|
397
|
+
: holder.email || billing.email,
|
|
398
|
+
phone:
|
|
399
|
+
showHolders && collectHolderContact
|
|
400
|
+
? holderPhone
|
|
401
|
+
: holderPhone || buyerPhone || '',
|
|
402
|
+
ticketTypeId: holder.ticketTypeId,
|
|
403
|
+
ticketDataCapture: holder.ticketDataCapture,
|
|
404
|
+
}
|
|
405
|
+
})
|
|
367
406
|
const missingDetails = missingCheckoutDetails(
|
|
368
407
|
{
|
|
369
408
|
billing: committedBilling,
|
|
@@ -383,7 +422,7 @@ export const BillingInfo: FC<BillingInfoProps> = ({
|
|
|
383
422
|
const complete = missingDetails.length === 0
|
|
384
423
|
const showValidation = validation.attempt > 0
|
|
385
424
|
const missingSet = new Set(missingDetails)
|
|
386
|
-
const fieldError = (key: string, message = '
|
|
425
|
+
const fieldError = (key: string, message = t('billing.fieldRequired')) =>
|
|
387
426
|
showValidation && missingSet.has(key) ? message : undefined
|
|
388
427
|
const dobError = showValidation
|
|
389
428
|
? missingDetails.find(
|
|
@@ -399,7 +438,7 @@ export const BillingInfo: FC<BillingInfoProps> = ({
|
|
|
399
438
|
const commit = () => {
|
|
400
439
|
checkout.setBilling(committedBilling)
|
|
401
440
|
checkout.setHolders(committedHolders)
|
|
402
|
-
if (
|
|
441
|
+
if (ttfOptInText || brandOptInLabel) checkout.setConsent(consent)
|
|
403
442
|
}
|
|
404
443
|
|
|
405
444
|
// Combined mode: keep the engine in sync as the form is filled, so <Payment/>
|
|
@@ -451,7 +490,7 @@ export const BillingInfo: FC<BillingInfoProps> = ({
|
|
|
451
490
|
return (
|
|
452
491
|
<>
|
|
453
492
|
<Card className={className}>
|
|
454
|
-
<SectionHeader step={step} title={
|
|
493
|
+
<SectionHeader step={step} title={headingText} />
|
|
455
494
|
|
|
456
495
|
{onLogin && !checkout.isLoggedIn && (
|
|
457
496
|
<div className="tf-checkout-billing__login">
|
|
@@ -472,7 +511,7 @@ export const BillingInfo: FC<BillingInfoProps> = ({
|
|
|
472
511
|
invalid={Boolean(fieldError('First name'))}
|
|
473
512
|
>
|
|
474
513
|
<Input
|
|
475
|
-
placeholder=
|
|
514
|
+
placeholder={t('billing.firstNamePlaceholder')}
|
|
476
515
|
value={billing.firstName}
|
|
477
516
|
required
|
|
478
517
|
invalid={Boolean(fieldError('First name'))}
|
|
@@ -485,7 +524,7 @@ export const BillingInfo: FC<BillingInfoProps> = ({
|
|
|
485
524
|
invalid={Boolean(fieldError('Last name'))}
|
|
486
525
|
>
|
|
487
526
|
<Input
|
|
488
|
-
placeholder=
|
|
527
|
+
placeholder={t('billing.lastNamePlaceholder')}
|
|
489
528
|
value={billing.lastName}
|
|
490
529
|
required
|
|
491
530
|
invalid={Boolean(fieldError('Last name'))}
|
|
@@ -502,7 +541,7 @@ export const BillingInfo: FC<BillingInfoProps> = ({
|
|
|
502
541
|
<Input
|
|
503
542
|
type="email"
|
|
504
543
|
autoComplete="email"
|
|
505
|
-
placeholder=
|
|
544
|
+
placeholder={t('billing.emailPlaceholder')}
|
|
506
545
|
value={billing.email}
|
|
507
546
|
required
|
|
508
547
|
invalid={Boolean(fieldError('Valid email address'))}
|
|
@@ -514,14 +553,14 @@ export const BillingInfo: FC<BillingInfoProps> = ({
|
|
|
514
553
|
<Field
|
|
515
554
|
error={fieldError(
|
|
516
555
|
'Matching email confirmation',
|
|
517
|
-
'
|
|
556
|
+
t('billing.emailMustMatch')
|
|
518
557
|
)}
|
|
519
558
|
invalid={Boolean(fieldError('Matching email confirmation'))}
|
|
520
559
|
>
|
|
521
560
|
<Input
|
|
522
561
|
type="email"
|
|
523
562
|
autoComplete="email"
|
|
524
|
-
placeholder=
|
|
563
|
+
placeholder={t('billing.confirmEmailPlaceholder')}
|
|
525
564
|
value={billing.confirmEmail ?? ''}
|
|
526
565
|
required
|
|
527
566
|
invalid={Boolean(fieldError('Matching email confirmation'))}
|
|
@@ -536,21 +575,21 @@ export const BillingInfo: FC<BillingInfoProps> = ({
|
|
|
536
575
|
billing.confirmEmail &&
|
|
537
576
|
billing.confirmEmail.trim().toLowerCase() !==
|
|
538
577
|
billing.email.trim().toLowerCase() && (
|
|
539
|
-
<p className="tf-checkout-error-text">
|
|
578
|
+
<p className="tf-checkout-error-text">{t('billing.emailMismatch')}</p>
|
|
540
579
|
)}
|
|
541
580
|
{emailTaken && (
|
|
542
581
|
<p className="tf-checkout-note tf-checkout-billing__email-taken">
|
|
543
|
-
|
|
582
|
+
{t('billing.emailRegistered')}{' '}
|
|
544
583
|
{onLogin ? (
|
|
545
584
|
<button
|
|
546
585
|
type="button"
|
|
547
586
|
className="tf-checkout-billing__login-link"
|
|
548
587
|
onClick={() => onLogin(billing.email)}
|
|
549
588
|
>
|
|
550
|
-
|
|
589
|
+
{t('billing.loginAutofill')}
|
|
551
590
|
</button>
|
|
552
591
|
) : (
|
|
553
|
-
'
|
|
592
|
+
t('billing.loginAutofillPlain')
|
|
554
593
|
)}
|
|
555
594
|
</p>
|
|
556
595
|
)}
|
|
@@ -564,18 +603,18 @@ export const BillingInfo: FC<BillingInfoProps> = ({
|
|
|
564
603
|
countries={countries}
|
|
565
604
|
error={fieldError(
|
|
566
605
|
f.kind === 'nationality'
|
|
567
|
-
? '
|
|
606
|
+
? t('billing.nationality')
|
|
568
607
|
: f.kind === 'gender'
|
|
569
|
-
? '
|
|
570
|
-
: '
|
|
608
|
+
? t('billing.gender')
|
|
609
|
+
: t('billing.instagramHandle')
|
|
571
610
|
)}
|
|
572
611
|
/>
|
|
573
612
|
))}
|
|
574
613
|
|
|
575
614
|
{gates.showPhone && (
|
|
576
615
|
<Field
|
|
577
|
-
label={
|
|
578
|
-
error={fieldError('Valid phone number', '
|
|
616
|
+
label={gates.phoneRequired ? t('billing.phone') : t('billing.phoneOptional')}
|
|
617
|
+
error={fieldError('Valid phone number', t('billing.phoneInvalid'))}
|
|
579
618
|
invalid={Boolean(fieldError('Valid phone number'))}
|
|
580
619
|
>
|
|
581
620
|
<PhoneInput
|
|
@@ -584,13 +623,20 @@ export const BillingInfo: FC<BillingInfoProps> = ({
|
|
|
584
623
|
fieldError('Valid phone number') ? ' tf-checkout-input--invalid' : ''
|
|
585
624
|
}`}
|
|
586
625
|
defaultCountry={phoneCountry}
|
|
626
|
+
// The flag owns the country; the input holds the national number
|
|
627
|
+
// only. Without `disableDialCodeAndPrefix` the empty field still
|
|
628
|
+
// expects a full international number, so a Mexican buyer typing
|
|
629
|
+
// "5512345678" has it read as a dial code and the country guess
|
|
630
|
+
// flips them to Brazil (+55) — submitting the wrong code.
|
|
631
|
+
disableDialCodeAndPrefix
|
|
632
|
+
showDisabledDialCodeAndPrefix
|
|
587
633
|
disableDialCodePrefill
|
|
588
634
|
value={billing.phone ?? ''}
|
|
589
635
|
required={gates.phoneRequired}
|
|
590
|
-
placeholder=
|
|
636
|
+
placeholder={t('billing.phonePlaceholder')}
|
|
591
637
|
inputProps={{
|
|
592
638
|
autoComplete: 'tel',
|
|
593
|
-
'aria-label': '
|
|
639
|
+
'aria-label': t('billing.phoneAria'),
|
|
594
640
|
'aria-invalid': Boolean(fieldError('Valid phone number')),
|
|
595
641
|
}}
|
|
596
642
|
onChange={(phone) => setBilling((current) => ({ ...current, phone }))}
|
|
@@ -600,14 +646,16 @@ export const BillingInfo: FC<BillingInfoProps> = ({
|
|
|
600
646
|
|
|
601
647
|
{gates.showDob && (
|
|
602
648
|
<Field
|
|
603
|
-
label={
|
|
604
|
-
gates.minimumAge
|
|
605
|
-
|
|
649
|
+
label={
|
|
650
|
+
gates.minimumAge
|
|
651
|
+
? t('billing.dobWithAge', { age: gates.minimumAge })
|
|
652
|
+
: t('billing.dob')
|
|
653
|
+
}
|
|
606
654
|
error={
|
|
607
655
|
dobError === 'Date of birth'
|
|
608
|
-
? '
|
|
656
|
+
? t('billing.dobRequired')
|
|
609
657
|
: dobError
|
|
610
|
-
?
|
|
658
|
+
? t('billing.ageMinimum', { age: gates.minimumAge ?? '' })
|
|
611
659
|
: undefined
|
|
612
660
|
}
|
|
613
661
|
invalid={Boolean(dobError)}
|
|
@@ -616,6 +664,12 @@ export const BillingInfo: FC<BillingInfoProps> = ({
|
|
|
616
664
|
type="date"
|
|
617
665
|
value={dob}
|
|
618
666
|
required
|
|
667
|
+
// A date input with no bounds lets the browser's year segment run
|
|
668
|
+
// to six digits ("202555") and accept dates in the future. The
|
|
669
|
+
// range both caps the year at four digits and keeps the value a
|
|
670
|
+
// date someone could actually have been born on.
|
|
671
|
+
min={DOB_MIN}
|
|
672
|
+
max={dobMax}
|
|
619
673
|
invalid={Boolean(dobError)}
|
|
620
674
|
aria-invalid={Boolean(dobError)}
|
|
621
675
|
onChange={(e) => setDob(e.target.value)}
|
|
@@ -673,24 +727,24 @@ export const BillingInfo: FC<BillingInfoProps> = ({
|
|
|
673
727
|
className="tf-checkout-checkbox--consent"
|
|
674
728
|
label={
|
|
675
729
|
<>
|
|
676
|
-
|
|
730
|
+
{t('billing.privacyAgree', { brand: privacyPolicy.brandName })}{' '}
|
|
677
731
|
<a href={privacyPolicy.url} target="_blank" rel="noreferrer">
|
|
678
|
-
{privacyPolicy.linkLabel || '
|
|
732
|
+
{privacyPolicy.linkLabel || t('billing.privacyLink')}
|
|
679
733
|
</a>
|
|
680
734
|
{' *'}
|
|
681
735
|
</>
|
|
682
736
|
}
|
|
683
737
|
/>
|
|
684
738
|
{(showPrivacyError || showValidation) && !consent.ttfOptIn && (
|
|
685
|
-
<div className="tf-checkout-consent__error">
|
|
739
|
+
<div className="tf-checkout-consent__error">{t('common.required')}</div>
|
|
686
740
|
)}
|
|
687
741
|
</div>
|
|
688
|
-
) :
|
|
742
|
+
) : ttfOptInText ? (
|
|
689
743
|
<Checkbox
|
|
690
744
|
checked={consent.ttfOptIn}
|
|
691
745
|
onChange={(e) => setConsent((c) => ({ ...c, ttfOptIn: e.target.checked }))}
|
|
692
746
|
className="tf-checkout-checkbox--consent"
|
|
693
|
-
label={
|
|
747
|
+
label={ttfOptInText}
|
|
694
748
|
/>
|
|
695
749
|
) : null}
|
|
696
750
|
{!combined && (
|
|
@@ -706,7 +760,7 @@ export const BillingInfo: FC<BillingInfoProps> = ({
|
|
|
706
760
|
onContinue?.()
|
|
707
761
|
}}
|
|
708
762
|
>
|
|
709
|
-
{
|
|
763
|
+
{buttonText}
|
|
710
764
|
</Button>
|
|
711
765
|
</>
|
|
712
766
|
)}
|