tf-checkout-react 2.0.0-beta.1 → 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.esm.js +473 -263
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +473 -263
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/EmbeddedCheckout.tsx +6 -1
- package/src/auth/AuthModal.tsx +3 -3
- 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 +40 -33
- package/src/sections/Confirmation.tsx +8 -6
- package/src/sections/OrderSummary.tsx +7 -5
- package/src/sections/Payment.tsx +17 -13
- package/src/sections/Tickets.tsx +32 -15
- package/src/sections/billing/BillingAddressFields.tsx +19 -13
- package/src/sections/billing/TicketHoldersCard.tsx +103 -94
- 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/src/sections/Payment.tsx
CHANGED
|
@@ -24,6 +24,7 @@ import { CardInput, StripePaymentElement, XenditCardInput } from '../CardInput'
|
|
|
24
24
|
import { Button } from '../ui/Button'
|
|
25
25
|
import { Card, SectionHeader } from '../ui/Card'
|
|
26
26
|
import { useCheckoutContext, useCheckoutValidation } from '../ui/CheckoutProvider'
|
|
27
|
+
import { useT } from '../ui/I18nProvider'
|
|
27
28
|
import { Checkbox } from '../ui/Field'
|
|
28
29
|
import { cx } from '../ui/cx'
|
|
29
30
|
import { Loader } from '../ui/Loader'
|
|
@@ -100,6 +101,7 @@ export const Payment: FC<PaymentProps> = ({
|
|
|
100
101
|
showTestCardHint = false,
|
|
101
102
|
className,
|
|
102
103
|
}) => {
|
|
104
|
+
const t = useT()
|
|
103
105
|
const checkout = useCheckoutContext()
|
|
104
106
|
const validation = useCheckoutValidation()
|
|
105
107
|
const [error, setError] = useState('')
|
|
@@ -153,7 +155,11 @@ export const Payment: FC<PaymentProps> = ({
|
|
|
153
155
|
const out: PaymentMethodOption[] = []
|
|
154
156
|
if (isXendit) {
|
|
155
157
|
if (methods.xenditCard)
|
|
156
|
-
out.push({
|
|
158
|
+
out.push({
|
|
159
|
+
id: { kind: 'xenditCard' },
|
|
160
|
+
value: 'xenditCard',
|
|
161
|
+
label: t('payment.card'),
|
|
162
|
+
})
|
|
157
163
|
for (const w of methods.xenditEWallets)
|
|
158
164
|
out.push({
|
|
159
165
|
id: { kind: 'xenditEWallet', walletKey: w.key },
|
|
@@ -166,7 +172,7 @@ export const Payment: FC<PaymentProps> = ({
|
|
|
166
172
|
out.push({ id: { kind: 'razorpay' }, value: 'razorpay', label: 'Razorpay' })
|
|
167
173
|
} else {
|
|
168
174
|
// Stripe (or unknown → fail open to card).
|
|
169
|
-
out.push({ id: { kind: 'card' }, value: 'card', label: '
|
|
175
|
+
out.push({ id: { kind: 'card' }, value: 'card', label: t('payment.card') })
|
|
170
176
|
for (const t of checkout.redirectPaymentMethods ?? []) {
|
|
171
177
|
if (t === 'paypal' && methods.paypal) continue // dedupe vs PayPal Standard
|
|
172
178
|
out.push({
|
|
@@ -179,12 +185,12 @@ export const Payment: FC<PaymentProps> = ({
|
|
|
179
185
|
out.push({
|
|
180
186
|
id: { kind: 'paymentPlan' },
|
|
181
187
|
value: 'paymentPlan',
|
|
182
|
-
label: '
|
|
188
|
+
label: t('payment.paymentPlan'),
|
|
183
189
|
})
|
|
184
190
|
// OXXO cash voucher. `oxxo` is deliberately absent from
|
|
185
191
|
// SUPPORTED_REDIRECT_METHODS, so it never arrives via the loop above.
|
|
186
192
|
if (offersOxxo)
|
|
187
|
-
out.push({ id: { kind: 'oxxo' }, value: 'oxxo', label: '
|
|
193
|
+
out.push({ id: { kind: 'oxxo' }, value: 'oxxo', label: t('payment.oxxo') })
|
|
188
194
|
}
|
|
189
195
|
if (methods.paypal)
|
|
190
196
|
out.push({ id: { kind: 'paypal' }, value: 'paypal', label: 'PayPal' })
|
|
@@ -290,10 +296,10 @@ export const Payment: FC<PaymentProps> = ({
|
|
|
290
296
|
// The plan CTA carries the brand's terminology ("Confirm Layaway"), and the
|
|
291
297
|
// terms sentence quotes this same label — they must agree.
|
|
292
298
|
const payLabel = isFree
|
|
293
|
-
? '
|
|
299
|
+
? t('payment.completeRegistration')
|
|
294
300
|
: isPlanSelected
|
|
295
301
|
? planConfirmLabel(checkout.paymentPlan?.configuration)
|
|
296
|
-
: '
|
|
302
|
+
: t('payment.submitPayment')
|
|
297
303
|
const planTerms = paymentPlanTerms(checkout.paymentPlan?.configuration)
|
|
298
304
|
// 'placing' → order in flight; 'succeeded' → placed, host navigating away.
|
|
299
305
|
// Both keep the CTA busy so it can't be re-clicked during the handoff.
|
|
@@ -422,9 +428,7 @@ export const Payment: FC<PaymentProps> = ({
|
|
|
422
428
|
)}
|
|
423
429
|
{showTestCardHint && (
|
|
424
430
|
<p className="tf-checkout-note">
|
|
425
|
-
{isXendit
|
|
426
|
-
? 'Test card: 4000 0000 0000 0002 · any future date · any CVC'
|
|
427
|
-
: 'Test card: 4242 4242 4242 4242 · any future date · any CVC'}
|
|
431
|
+
{isXendit ? t('payment.testCardXendit') : t('payment.testCard')}
|
|
428
432
|
</p>
|
|
429
433
|
)}
|
|
430
434
|
</>
|
|
@@ -454,7 +458,7 @@ export const Payment: FC<PaymentProps> = ({
|
|
|
454
458
|
<Checkbox
|
|
455
459
|
checked={useSavedCard}
|
|
456
460
|
onChange={(e) => setUseSavedCard(e.target.checked)}
|
|
457
|
-
label=
|
|
461
|
+
label={t('payment.useThisCard')}
|
|
458
462
|
/>
|
|
459
463
|
</div>
|
|
460
464
|
)}
|
|
@@ -464,13 +468,13 @@ export const Payment: FC<PaymentProps> = ({
|
|
|
464
468
|
return (
|
|
465
469
|
<div className={['tf-checkout-payment-stack', className].filter(Boolean).join(' ')}>
|
|
466
470
|
<Card>
|
|
467
|
-
<SectionHeader title=
|
|
471
|
+
<SectionHeader title={t('payment.title')} />
|
|
468
472
|
|
|
469
473
|
{/* Payment method */}
|
|
470
474
|
{!isFree && (
|
|
471
475
|
<>
|
|
472
476
|
{!singleCard && (
|
|
473
|
-
<div className="tf-checkout-payment__label">
|
|
477
|
+
<div className="tf-checkout-payment__label">{t('payment.method')}</div>
|
|
474
478
|
)}
|
|
475
479
|
{!methods ? (
|
|
476
480
|
<div className="tf-checkout-payment__cardbox">
|
|
@@ -548,7 +552,7 @@ export const Payment: FC<PaymentProps> = ({
|
|
|
548
552
|
loading={finalizing}
|
|
549
553
|
disabled={checkout.isBusy || finalizing || (!isFree && !methods)}
|
|
550
554
|
>
|
|
551
|
-
{finalizing ? '
|
|
555
|
+
{finalizing ? t('payment.placing') : payLabel}
|
|
552
556
|
</Button>
|
|
553
557
|
</div>
|
|
554
558
|
{bottomError && <p className="tf-checkout-error-text">{bottomError}</p>}
|
package/src/sections/Tickets.tsx
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
import { Button } from '../ui/Button'
|
|
25
25
|
import { Card, SectionHeader } from '../ui/Card'
|
|
26
26
|
import { useCheckoutContext } from '../ui/CheckoutProvider'
|
|
27
|
+
import { useT } from '../ui/I18nProvider'
|
|
27
28
|
import { cx } from '../ui/cx'
|
|
28
29
|
import { Field, FieldRow, Input } from '../ui/Field'
|
|
29
30
|
import { Loader } from '../ui/Loader'
|
|
@@ -124,7 +125,7 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
124
125
|
eventId,
|
|
125
126
|
title = false,
|
|
126
127
|
onContinue,
|
|
127
|
-
buttonLabel
|
|
128
|
+
buttonLabel,
|
|
128
129
|
timeSlotDates,
|
|
129
130
|
step,
|
|
130
131
|
onSelectSeats,
|
|
@@ -133,7 +134,9 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
133
134
|
readAttributionFromUrl = true,
|
|
134
135
|
className,
|
|
135
136
|
}) => {
|
|
137
|
+
const tr = useT()
|
|
136
138
|
const checkout = useCheckoutContext()
|
|
139
|
+
const buttonText = buttonLabel ?? tr('tickets.getTickets')
|
|
137
140
|
const ticketsQuery = hooks.useTicketsWithMeta(eventId)
|
|
138
141
|
const promoMeta = ticketsQuery.data?.promo
|
|
139
142
|
const showWaitingList = ticketsQuery.data?.showWaitingList ?? false
|
|
@@ -256,7 +259,9 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
256
259
|
// Types that can still be bought with a quantity dropdown (all of them for a
|
|
257
260
|
// non-seated event). When empty on a seated event, only "Select seats" shows.
|
|
258
261
|
const directPurchasable = tickets.filter((t) => !seatMapRow(t))
|
|
259
|
-
const seatCtaLabel = eventInfo?.tableMapEnabled
|
|
262
|
+
const seatCtaLabel = eventInfo?.tableMapEnabled
|
|
263
|
+
? tr('tickets.selectOnMap')
|
|
264
|
+
: tr('tickets.selectSeats')
|
|
260
265
|
|
|
261
266
|
// A "Tables" divider before the first table type — only when regular tickets
|
|
262
267
|
// actually precede it (a tables-first list gets no stray divider).
|
|
@@ -312,7 +317,9 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
312
317
|
<div className="tf-checkout-tickets__group">{t.groupName}</div>
|
|
313
318
|
) : (
|
|
314
319
|
t.id === firstTableId && (
|
|
315
|
-
<div className="tf-checkout-tickets__group">
|
|
320
|
+
<div className="tf-checkout-tickets__group">
|
|
321
|
+
{tr('tickets.reserveTables')}
|
|
322
|
+
</div>
|
|
316
323
|
)
|
|
317
324
|
)}
|
|
318
325
|
<div className="tf-checkout-tickets__row-wrap">
|
|
@@ -330,7 +337,9 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
330
337
|
setVisibleDescription((current) => (current === t.id ? null : t.id))
|
|
331
338
|
}
|
|
332
339
|
>
|
|
333
|
-
{visibleDescription === t.id
|
|
340
|
+
{visibleDescription === t.id
|
|
341
|
+
? tr('tickets.hideInfo')
|
|
342
|
+
: tr('tickets.viewInfo')}
|
|
334
343
|
</button>
|
|
335
344
|
)}
|
|
336
345
|
</div>
|
|
@@ -367,17 +376,21 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
367
376
|
{seatMapRow(t) ? null : unavailable ? (
|
|
368
377
|
<span className="tf-checkout-tickets__soldout">
|
|
369
378
|
{t.soldOut
|
|
370
|
-
? '
|
|
379
|
+
? tr('tickets.soldOut')
|
|
371
380
|
: notStarted
|
|
372
|
-
? '
|
|
373
|
-
: '
|
|
381
|
+
? tr('tickets.salesNotStarted')
|
|
382
|
+
: tr('tickets.salesEnded')}
|
|
374
383
|
</span>
|
|
375
384
|
) : (
|
|
376
385
|
<div className="tf-checkout-tickets__qty">
|
|
377
386
|
{/* Tables pick a number of GUESTS (bounded by minGuests/maxGuests),
|
|
378
387
|
not a count of tickets — the label makes that explicit, matching
|
|
379
388
|
the old checkout's "GUESTS" affordance. */}
|
|
380
|
-
{t.isTable &&
|
|
389
|
+
{t.isTable && (
|
|
390
|
+
<span className="tf-checkout-tickets__guests">
|
|
391
|
+
{tr('tickets.guests')}
|
|
392
|
+
</span>
|
|
393
|
+
)}
|
|
381
394
|
<QuantitySelect
|
|
382
395
|
value={qty[t.id] ?? 0}
|
|
383
396
|
min={range.min}
|
|
@@ -447,7 +460,7 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
447
460
|
|
|
448
461
|
{isTimeSlot && dates.length > 0 && (
|
|
449
462
|
<div className="tf-checkout-tickets__currency">
|
|
450
|
-
<span className="tf-checkout-tickets__currency-label">
|
|
463
|
+
<span className="tf-checkout-tickets__currency-label">{tr('common.date')}</span>
|
|
451
464
|
<select
|
|
452
465
|
className="tf-checkout-select"
|
|
453
466
|
value={slotDate}
|
|
@@ -467,7 +480,9 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
467
480
|
|
|
468
481
|
{checkout.currency && checkout.currency.available.length > 1 && (
|
|
469
482
|
<div className="tf-checkout-tickets__currency">
|
|
470
|
-
<span className="tf-checkout-tickets__currency-label">
|
|
483
|
+
<span className="tf-checkout-tickets__currency-label">
|
|
484
|
+
{tr('common.currency')}
|
|
485
|
+
</span>
|
|
471
486
|
<select
|
|
472
487
|
className="tf-checkout-select"
|
|
473
488
|
value={checkout.currency.currency}
|
|
@@ -483,7 +498,7 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
483
498
|
)}
|
|
484
499
|
|
|
485
500
|
{isTimeSlot && !slotQuery.isLoading && tickets.length === 0 && slotDate && (
|
|
486
|
-
<p className="tf-checkout-note">
|
|
501
|
+
<p className="tf-checkout-note">{tr('tickets.noSessions')}</p>
|
|
487
502
|
)}
|
|
488
503
|
|
|
489
504
|
{isTimeSlot
|
|
@@ -511,7 +526,9 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
511
526
|
<FieldRow>
|
|
512
527
|
<Field>
|
|
513
528
|
<Input
|
|
514
|
-
placeholder={
|
|
529
|
+
placeholder={
|
|
530
|
+
isAccess ? tr('tickets.accessCode') : tr('tickets.promoCode')
|
|
531
|
+
}
|
|
515
532
|
value={code}
|
|
516
533
|
onChange={(e) => setCode(e.target.value)}
|
|
517
534
|
onKeyDown={(e) => e.key === 'Enter' && applyCode()}
|
|
@@ -551,10 +568,10 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
551
568
|
disabled={checkout.isBusy || selected.length === 0}
|
|
552
569
|
>
|
|
553
570
|
{checkout.isBusy
|
|
554
|
-
? '
|
|
571
|
+
? tr('tickets.reserving')
|
|
555
572
|
: selectionIsTable
|
|
556
|
-
? '
|
|
557
|
-
:
|
|
573
|
+
? tr('tickets.reserveTables')
|
|
574
|
+
: buttonText}
|
|
558
575
|
</Button>
|
|
559
576
|
)}
|
|
560
577
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { type Dispatch, type FC, type SetStateAction } from 'react'
|
|
2
2
|
|
|
3
3
|
import { Field, FieldRow, Input, Select } from '../../ui/Field'
|
|
4
|
+
import { useT } from '../../ui/I18nProvider'
|
|
4
5
|
import type { BillingDraft, BillingFieldError, LocationOption } from './types'
|
|
5
6
|
|
|
6
7
|
interface BillingAddressFieldsProps {
|
|
@@ -19,19 +20,20 @@ export const BillingAddressFields: FC<BillingAddressFieldsProps> = ({
|
|
|
19
20
|
fieldError,
|
|
20
21
|
setBilling,
|
|
21
22
|
}) => {
|
|
23
|
+
const t = useT()
|
|
22
24
|
const setValue =
|
|
23
25
|
(key: keyof BillingDraft) => (event: React.ChangeEvent<HTMLInputElement>) =>
|
|
24
26
|
setBilling((current) => ({ ...current, [key]: event.target.value }))
|
|
25
27
|
|
|
26
28
|
return (
|
|
27
29
|
<>
|
|
28
|
-
<div className="tf-checkout-billing__section-label">
|
|
30
|
+
<div className="tf-checkout-billing__section-label">{t('billing.address')}</div>
|
|
29
31
|
<Field
|
|
30
|
-
error={fieldError('Street address', '
|
|
32
|
+
error={fieldError('Street address', t('billing.streetRequired'))}
|
|
31
33
|
invalid={Boolean(fieldError('Street address'))}
|
|
32
34
|
>
|
|
33
35
|
<Input
|
|
34
|
-
placeholder=
|
|
36
|
+
placeholder={t('billing.streetPlaceholder')}
|
|
35
37
|
autoComplete="street-address"
|
|
36
38
|
value={billing.streetAddress ?? ''}
|
|
37
39
|
required
|
|
@@ -42,11 +44,11 @@ export const BillingAddressFields: FC<BillingAddressFieldsProps> = ({
|
|
|
42
44
|
</Field>
|
|
43
45
|
<FieldRow>
|
|
44
46
|
<Field
|
|
45
|
-
error={fieldError('City', '
|
|
47
|
+
error={fieldError('City', t('billing.cityRequired'))}
|
|
46
48
|
invalid={Boolean(fieldError('City'))}
|
|
47
49
|
>
|
|
48
50
|
<Input
|
|
49
|
-
placeholder=
|
|
51
|
+
placeholder={t('billing.cityPlaceholder')}
|
|
50
52
|
autoComplete="address-level2"
|
|
51
53
|
value={billing.city ?? ''}
|
|
52
54
|
required
|
|
@@ -56,11 +58,11 @@ export const BillingAddressFields: FC<BillingAddressFieldsProps> = ({
|
|
|
56
58
|
/>
|
|
57
59
|
</Field>
|
|
58
60
|
<Field
|
|
59
|
-
error={fieldError('ZIP / postcode', '
|
|
61
|
+
error={fieldError('ZIP / postcode', t('billing.postcodeRequired'))}
|
|
60
62
|
invalid={Boolean(fieldError('ZIP / postcode'))}
|
|
61
63
|
>
|
|
62
64
|
<Input
|
|
63
|
-
placeholder=
|
|
65
|
+
placeholder={t('billing.postcodePlaceholder')}
|
|
64
66
|
autoComplete="postal-code"
|
|
65
67
|
value={billing.zip ?? ''}
|
|
66
68
|
required
|
|
@@ -72,12 +74,12 @@ export const BillingAddressFields: FC<BillingAddressFieldsProps> = ({
|
|
|
72
74
|
</FieldRow>
|
|
73
75
|
<FieldRow>
|
|
74
76
|
<Field
|
|
75
|
-
error={fieldError('Country', '
|
|
77
|
+
error={fieldError('Country', t('billing.countryRequired'))}
|
|
76
78
|
invalid={Boolean(fieldError('Country'))}
|
|
77
79
|
>
|
|
78
80
|
<Select
|
|
79
81
|
value={billing.countryId ?? ''}
|
|
80
|
-
aria-label=
|
|
82
|
+
aria-label={t('billing.countryLabel')}
|
|
81
83
|
required
|
|
82
84
|
invalid={Boolean(fieldError('Country'))}
|
|
83
85
|
aria-invalid={Boolean(fieldError('Country'))}
|
|
@@ -94,7 +96,7 @@ export const BillingAddressFields: FC<BillingAddressFieldsProps> = ({
|
|
|
94
96
|
}))
|
|
95
97
|
}}
|
|
96
98
|
>
|
|
97
|
-
<option value=""
|
|
99
|
+
<option value="">{t('billing.countryEmpty')}</option>
|
|
98
100
|
{countries.map((country) => (
|
|
99
101
|
<option key={country.id} value={country.id}>
|
|
100
102
|
{country.name}
|
|
@@ -103,13 +105,13 @@ export const BillingAddressFields: FC<BillingAddressFieldsProps> = ({
|
|
|
103
105
|
</Select>
|
|
104
106
|
</Field>
|
|
105
107
|
<Field
|
|
106
|
-
error={fieldError('State / county', '
|
|
108
|
+
error={fieldError('State / county', t('billing.stateRequired'))}
|
|
107
109
|
invalid={Boolean(fieldError('State / county'))}
|
|
108
110
|
>
|
|
109
111
|
<Select
|
|
110
112
|
value={billing.stateId ?? ''}
|
|
111
113
|
disabled={!billing.countryId || states.length === 0}
|
|
112
|
-
aria-label=
|
|
114
|
+
aria-label={t('billing.stateLabel')}
|
|
113
115
|
required={states.length > 0}
|
|
114
116
|
invalid={Boolean(fieldError('State / county'))}
|
|
115
117
|
aria-invalid={Boolean(fieldError('State / county'))}
|
|
@@ -124,7 +126,11 @@ export const BillingAddressFields: FC<BillingAddressFieldsProps> = ({
|
|
|
124
126
|
}))
|
|
125
127
|
}}
|
|
126
128
|
>
|
|
127
|
-
<option value=""
|
|
129
|
+
<option value="">
|
|
130
|
+
{states.length > 0
|
|
131
|
+
? t('billing.stateEmptyRequired')
|
|
132
|
+
: t('billing.stateEmpty')}
|
|
133
|
+
</option>
|
|
128
134
|
{states.map((state) => (
|
|
129
135
|
<option key={state.id} value={state.id}>
|
|
130
136
|
{state.name}
|
|
@@ -3,6 +3,7 @@ import { PhoneInput, type CountryIso2 } from 'react-international-phone'
|
|
|
3
3
|
|
|
4
4
|
import { Card, SectionHeader } from '../../ui/Card'
|
|
5
5
|
import { Field, FieldRow, Input } from '../../ui/Field'
|
|
6
|
+
import { useT } from '../../ui/I18nProvider'
|
|
6
7
|
import type { BillingFieldError, HolderDraft } from './types'
|
|
7
8
|
|
|
8
9
|
interface TicketHoldersCardProps {
|
|
@@ -24,101 +25,109 @@ export const TicketHoldersCard: FC<TicketHoldersCardProps> = ({
|
|
|
24
25
|
fieldError,
|
|
25
26
|
renderCustomFields,
|
|
26
27
|
patchHolder,
|
|
27
|
-
}) =>
|
|
28
|
-
|
|
29
|
-
<SectionHeader title="Ticket Holders" />
|
|
30
|
-
{holders.map((holder, index) => {
|
|
31
|
-
const firstNameKey = `Ticket ${index + 1} holder first name`
|
|
32
|
-
const lastNameKey = `Ticket ${index + 1} holder last name`
|
|
33
|
-
const emailKey = `Ticket ${index + 1} holder email`
|
|
34
|
-
const phoneKey = `Ticket ${index + 1} holder phone`
|
|
28
|
+
}) => {
|
|
29
|
+
const t = useT()
|
|
35
30
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
31
|
+
return (
|
|
32
|
+
<Card className="tf-checkout-billing__holders-card">
|
|
33
|
+
<SectionHeader title={t('billing.ticketHolders')} />
|
|
34
|
+
{holders.map((holder, index) => {
|
|
35
|
+
const firstNameKey = `Ticket ${index + 1} holder first name`
|
|
36
|
+
const lastNameKey = `Ticket ${index + 1} holder last name`
|
|
37
|
+
const emailKey = `Ticket ${index + 1} holder email`
|
|
38
|
+
const phoneKey = `Ticket ${index + 1} holder phone`
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div key={index} className="tf-checkout-billing__holder">
|
|
42
|
+
<div className="tf-checkout-billing__holder-title">
|
|
43
|
+
{t('billing.holderTitle', { number: index + 1 })}
|
|
44
|
+
</div>
|
|
45
|
+
{holderNamesRequired && (
|
|
46
|
+
<FieldRow>
|
|
47
|
+
<Field
|
|
48
|
+
label={t('billing.holderFirstName')}
|
|
49
|
+
error={fieldError(firstNameKey, t('billing.firstNameRequired'))}
|
|
49
50
|
invalid={Boolean(fieldError(firstNameKey))}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
<
|
|
62
|
-
|
|
63
|
-
|
|
51
|
+
>
|
|
52
|
+
<Input
|
|
53
|
+
value={holder.firstName}
|
|
54
|
+
required
|
|
55
|
+
invalid={Boolean(fieldError(firstNameKey))}
|
|
56
|
+
aria-invalid={Boolean(fieldError(firstNameKey))}
|
|
57
|
+
onChange={(event) =>
|
|
58
|
+
patchHolder(index, { firstName: event.target.value })
|
|
59
|
+
}
|
|
60
|
+
/>
|
|
61
|
+
</Field>
|
|
62
|
+
<Field
|
|
63
|
+
label={t('billing.holderLastName')}
|
|
64
|
+
error={fieldError(lastNameKey, t('billing.lastNameRequired'))}
|
|
64
65
|
invalid={Boolean(fieldError(lastNameKey))}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
>
|
|
80
|
-
<
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
value={holder.email}
|
|
84
|
-
required
|
|
66
|
+
>
|
|
67
|
+
<Input
|
|
68
|
+
value={holder.lastName}
|
|
69
|
+
required
|
|
70
|
+
invalid={Boolean(fieldError(lastNameKey))}
|
|
71
|
+
aria-invalid={Boolean(fieldError(lastNameKey))}
|
|
72
|
+
onChange={(event) =>
|
|
73
|
+
patchHolder(index, { lastName: event.target.value })
|
|
74
|
+
}
|
|
75
|
+
/>
|
|
76
|
+
</Field>
|
|
77
|
+
</FieldRow>
|
|
78
|
+
)}
|
|
79
|
+
{collectHolderContact && (
|
|
80
|
+
<FieldRow>
|
|
81
|
+
<Field
|
|
82
|
+
label={t('billing.email')}
|
|
83
|
+
error={fieldError(emailKey, t('billing.emailInvalid'))}
|
|
85
84
|
invalid={Boolean(fieldError(emailKey))}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
85
|
+
>
|
|
86
|
+
<Input
|
|
87
|
+
type="email"
|
|
88
|
+
autoComplete="email"
|
|
89
|
+
value={holder.email}
|
|
90
|
+
required
|
|
91
|
+
invalid={Boolean(fieldError(emailKey))}
|
|
92
|
+
aria-invalid={Boolean(fieldError(emailKey))}
|
|
93
|
+
onChange={(event) =>
|
|
94
|
+
patchHolder(index, { email: event.target.value })
|
|
95
|
+
}
|
|
96
|
+
/>
|
|
97
|
+
</Field>
|
|
98
|
+
<Field
|
|
99
|
+
label={t('billing.phone')}
|
|
100
|
+
error={fieldError(phoneKey, t('billing.phoneInvalid'))}
|
|
101
|
+
invalid={Boolean(fieldError(phoneKey))}
|
|
102
|
+
>
|
|
103
|
+
<PhoneInput
|
|
104
|
+
className="tf-checkout-phone"
|
|
105
|
+
inputClassName={`tf-checkout-input tf-checkout-phone__input${
|
|
106
|
+
fieldError(phoneKey) ? ' tf-checkout-input--invalid' : ''
|
|
107
|
+
}`}
|
|
108
|
+
defaultCountry={phoneCountry}
|
|
109
|
+
// Matches the buyer phone field in ../BillingInfo.tsx — see
|
|
110
|
+
// the note there on why the dial code must stay out of the
|
|
111
|
+
// input.
|
|
112
|
+
disableDialCodeAndPrefix
|
|
113
|
+
showDisabledDialCodeAndPrefix
|
|
114
|
+
disableDialCodePrefill
|
|
115
|
+
value={holder.phone}
|
|
116
|
+
required
|
|
117
|
+
inputProps={{
|
|
118
|
+
autoComplete: 'tel',
|
|
119
|
+
'aria-label': phoneKey,
|
|
120
|
+
'aria-invalid': Boolean(fieldError(phoneKey)),
|
|
121
|
+
}}
|
|
122
|
+
onChange={(phone) => patchHolder(index, { phone })}
|
|
123
|
+
/>
|
|
124
|
+
</Field>
|
|
125
|
+
</FieldRow>
|
|
126
|
+
)}
|
|
127
|
+
{renderCustomFields(holder, index)}
|
|
128
|
+
</div>
|
|
129
|
+
)
|
|
130
|
+
})}
|
|
131
|
+
</Card>
|
|
132
|
+
)
|
|
133
|
+
}
|
|
@@ -148,7 +148,11 @@ export const CheckoutProvider: FC<CheckoutProviderProps> = ({
|
|
|
148
148
|
// binding); absent ⇒ the self-providing branch owns it. The choice is
|
|
149
149
|
// static per mount, so the two branches never swap and violate the rules
|
|
150
150
|
// of hooks.
|
|
151
|
-
value ?
|
|
151
|
+
value ? (
|
|
152
|
+
<CheckoutRoot checkout={value} {...rest} />
|
|
153
|
+
) : (
|
|
154
|
+
<SelfProvidedCheckout {...rest} />
|
|
155
|
+
)
|
|
152
156
|
}
|
|
153
157
|
</I18nProvider>
|
|
154
158
|
)
|
|
@@ -10,8 +10,10 @@ import React, { useCallback, useState } from 'react'
|
|
|
10
10
|
import type { InstallmentOffer } from 'tf-checkout-shared'
|
|
11
11
|
|
|
12
12
|
import { Button } from './Button'
|
|
13
|
+
import { useT } from './I18nProvider'
|
|
13
14
|
|
|
14
15
|
export const useInstallmentPicker = () => {
|
|
16
|
+
const t = useT()
|
|
15
17
|
const [pending, setPending] = useState<{
|
|
16
18
|
offers: InstallmentOffer[]
|
|
17
19
|
resolve: (o: InstallmentOffer | null) => void
|
|
@@ -34,10 +36,10 @@ export const useInstallmentPicker = () => {
|
|
|
34
36
|
<div
|
|
35
37
|
className="tf-checkout-installments"
|
|
36
38
|
role="dialog"
|
|
37
|
-
aria-label=
|
|
39
|
+
aria-label={t('payment.chooseInstallments')}
|
|
38
40
|
>
|
|
39
41
|
<div className="tf-checkout-installments__box">
|
|
40
|
-
<strong>
|
|
42
|
+
<strong>{t('payment.payInInstallments')}</strong>
|
|
41
43
|
<div className="tf-checkout-installments__list">
|
|
42
44
|
{pending.offers.map((o, i) => (
|
|
43
45
|
<button
|