tf-checkout-react 2.0.0-beta.1 → 2.0.0-beta.3
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 +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.esm.js +662 -341
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +662 -341
- package/dist/index.js.map +1 -1
- package/dist/styles/tickets.css +18 -1
- package/package.json +1 -1
- package/src/EmbeddedCheckout.tsx +9 -6
- 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 +10 -8
- package/src/sections/IDVerification.tsx +16 -9
- package/src/sections/OrderConfirmation.tsx +32 -23
- package/src/sections/OrderIDVerification.tsx +23 -11
- package/src/sections/OrderSummary.tsx +7 -5
- package/src/sections/Payment.tsx +18 -14
- package/src/sections/Tickets.tsx +48 -25
- package/src/sections/billing/BillingAddressFields.tsx +19 -13
- package/src/sections/billing/TicketHoldersCard.tsx +103 -94
- package/src/styles/tickets.css +18 -1
- package/src/ui/CheckoutProvider.tsx +5 -1
- package/src/ui/InstallmentPicker.tsx +8 -4
- package/src/ui/Loader.tsx +11 -7
- package/src/ui/PaymentMethods.tsx +82 -76
- package/src/ui/Stepper.tsx +27 -23
- package/src/ui/TimerBanner.tsx +7 -3
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'
|
|
@@ -34,6 +35,7 @@ const WaitingListForm: FC<{
|
|
|
34
35
|
ticket: TicketType
|
|
35
36
|
onJoin: (entry: WaitingListEntry) => Promise<void>
|
|
36
37
|
}> = ({ ticket, onJoin }) => {
|
|
38
|
+
const tr = useT()
|
|
37
39
|
const [form, setForm] = useState({ firstName: '', lastName: '', email: '' })
|
|
38
40
|
const [state, setState] = useState<'idle' | 'busy' | 'done' | 'error'>('idle')
|
|
39
41
|
const [error, setError] = useState('')
|
|
@@ -42,7 +44,7 @@ const WaitingListForm: FC<{
|
|
|
42
44
|
if (state === 'done')
|
|
43
45
|
return (
|
|
44
46
|
<p className="tf-checkout-note tf-checkout-tickets__wl-done">
|
|
45
|
-
|
|
47
|
+
{tr('tickets.waitingListJoined')}
|
|
46
48
|
</p>
|
|
47
49
|
)
|
|
48
50
|
|
|
@@ -51,14 +53,14 @@ const WaitingListForm: FC<{
|
|
|
51
53
|
<FieldRow>
|
|
52
54
|
<Field>
|
|
53
55
|
<Input
|
|
54
|
-
placeholder=
|
|
56
|
+
placeholder={tr('tickets.firstName')}
|
|
55
57
|
value={form.firstName}
|
|
56
58
|
onChange={(e) => setForm((f) => ({ ...f, firstName: e.target.value }))}
|
|
57
59
|
/>
|
|
58
60
|
</Field>
|
|
59
61
|
<Field>
|
|
60
62
|
<Input
|
|
61
|
-
placeholder=
|
|
63
|
+
placeholder={tr('tickets.lastName')}
|
|
62
64
|
value={form.lastName}
|
|
63
65
|
onChange={(e) => setForm((f) => ({ ...f, lastName: e.target.value }))}
|
|
64
66
|
/>
|
|
@@ -66,7 +68,7 @@ const WaitingListForm: FC<{
|
|
|
66
68
|
</FieldRow>
|
|
67
69
|
<Field>
|
|
68
70
|
<Input
|
|
69
|
-
placeholder=
|
|
71
|
+
placeholder={tr('tickets.email')}
|
|
70
72
|
value={form.email}
|
|
71
73
|
onChange={(e) => setForm((f) => ({ ...f, email: e.target.value }))}
|
|
72
74
|
/>
|
|
@@ -86,7 +88,7 @@ const WaitingListForm: FC<{
|
|
|
86
88
|
disabled={!ok || state === 'busy'}
|
|
87
89
|
variant="secondary"
|
|
88
90
|
>
|
|
89
|
-
|
|
91
|
+
{tr('tickets.joinWaitingList')}
|
|
90
92
|
</Button>
|
|
91
93
|
{error && <p className="tf-checkout-error-text">{error}</p>}
|
|
92
94
|
</div>
|
|
@@ -124,7 +126,7 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
124
126
|
eventId,
|
|
125
127
|
title = false,
|
|
126
128
|
onContinue,
|
|
127
|
-
buttonLabel
|
|
129
|
+
buttonLabel,
|
|
128
130
|
timeSlotDates,
|
|
129
131
|
step,
|
|
130
132
|
onSelectSeats,
|
|
@@ -133,7 +135,9 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
133
135
|
readAttributionFromUrl = true,
|
|
134
136
|
className,
|
|
135
137
|
}) => {
|
|
138
|
+
const tr = useT()
|
|
136
139
|
const checkout = useCheckoutContext()
|
|
140
|
+
const buttonText = buttonLabel ?? tr('tickets.getTickets')
|
|
137
141
|
const ticketsQuery = hooks.useTicketsWithMeta(eventId)
|
|
138
142
|
const promoMeta = ticketsQuery.data?.promo
|
|
139
143
|
const showWaitingList = ticketsQuery.data?.showWaitingList ?? false
|
|
@@ -256,7 +260,9 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
256
260
|
// Types that can still be bought with a quantity dropdown (all of them for a
|
|
257
261
|
// non-seated event). When empty on a seated event, only "Select seats" shows.
|
|
258
262
|
const directPurchasable = tickets.filter((t) => !seatMapRow(t))
|
|
259
|
-
const seatCtaLabel = eventInfo?.tableMapEnabled
|
|
263
|
+
const seatCtaLabel = eventInfo?.tableMapEnabled
|
|
264
|
+
? tr('tickets.selectOnMap')
|
|
265
|
+
: tr('tickets.selectSeats')
|
|
260
266
|
|
|
261
267
|
// A "Tables" divider before the first table type — only when regular tickets
|
|
262
268
|
// actually precede it (a tables-first list gets no stray divider).
|
|
@@ -312,7 +318,9 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
312
318
|
<div className="tf-checkout-tickets__group">{t.groupName}</div>
|
|
313
319
|
) : (
|
|
314
320
|
t.id === firstTableId && (
|
|
315
|
-
<div className="tf-checkout-tickets__group">
|
|
321
|
+
<div className="tf-checkout-tickets__group">
|
|
322
|
+
{tr('tickets.reserveTables')}
|
|
323
|
+
</div>
|
|
316
324
|
)
|
|
317
325
|
)}
|
|
318
326
|
<div className="tf-checkout-tickets__row-wrap">
|
|
@@ -330,10 +338,17 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
330
338
|
setVisibleDescription((current) => (current === t.id ? null : t.id))
|
|
331
339
|
}
|
|
332
340
|
>
|
|
333
|
-
{visibleDescription === t.id
|
|
341
|
+
{visibleDescription === t.id
|
|
342
|
+
? tr('tickets.hideInfo')
|
|
343
|
+
: tr('tickets.viewInfo')}
|
|
334
344
|
</button>
|
|
335
345
|
)}
|
|
336
346
|
</div>
|
|
347
|
+
{/* Only on a type that can be bought, as the old checkout does —
|
|
348
|
+
"Selling fast" beside "Sold out" contradicts itself. */}
|
|
349
|
+
{t.badgeText && !unavailable && (
|
|
350
|
+
<span className="tf-checkout-tickets__badge">{t.badgeText}</span>
|
|
351
|
+
)}
|
|
337
352
|
<div className="tf-checkout-tickets__price">
|
|
338
353
|
{discounted && (
|
|
339
354
|
<span className="tf-checkout-tickets__old">
|
|
@@ -346,18 +361,18 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
346
361
|
isFree && 'tf-checkout-tickets__amount--free'
|
|
347
362
|
)}
|
|
348
363
|
>
|
|
349
|
-
{isFree ? '
|
|
364
|
+
{isFree ? tr('tickets.free') : money(sym, t.price)}
|
|
350
365
|
</span>
|
|
351
366
|
{/* Fees sub-line (the "(… with fees)" the old checkout shows) —
|
|
352
367
|
omitted for free/sold-out types. */}
|
|
353
368
|
{!isFree && !t.soldOut && (
|
|
354
369
|
<span className="tf-checkout-tickets__fees">
|
|
355
|
-
({money(sym, t.priceWithFees)}
|
|
370
|
+
{tr('tickets.withFees', { price: money(sym, t.priceWithFees) })}
|
|
356
371
|
</span>
|
|
357
372
|
)}
|
|
358
373
|
{t.depositPercent != null && (
|
|
359
374
|
<span className="tf-checkout-tickets__deposit">
|
|
360
|
-
{t.depositPercent.toFixed(2)}
|
|
375
|
+
{tr('tickets.deposit', { percent: t.depositPercent.toFixed(2) })}
|
|
361
376
|
</span>
|
|
362
377
|
)}
|
|
363
378
|
</div>
|
|
@@ -367,17 +382,21 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
367
382
|
{seatMapRow(t) ? null : unavailable ? (
|
|
368
383
|
<span className="tf-checkout-tickets__soldout">
|
|
369
384
|
{t.soldOut
|
|
370
|
-
? '
|
|
385
|
+
? tr('tickets.soldOut')
|
|
371
386
|
: notStarted
|
|
372
|
-
? '
|
|
373
|
-
: '
|
|
387
|
+
? tr('tickets.salesNotStarted')
|
|
388
|
+
: tr('tickets.salesEnded')}
|
|
374
389
|
</span>
|
|
375
390
|
) : (
|
|
376
391
|
<div className="tf-checkout-tickets__qty">
|
|
377
392
|
{/* Tables pick a number of GUESTS (bounded by minGuests/maxGuests),
|
|
378
393
|
not a count of tickets — the label makes that explicit, matching
|
|
379
394
|
the old checkout's "GUESTS" affordance. */}
|
|
380
|
-
{t.isTable &&
|
|
395
|
+
{t.isTable && (
|
|
396
|
+
<span className="tf-checkout-tickets__guests">
|
|
397
|
+
{tr('tickets.guests')}
|
|
398
|
+
</span>
|
|
399
|
+
)}
|
|
381
400
|
<QuantitySelect
|
|
382
401
|
value={qty[t.id] ?? 0}
|
|
383
402
|
min={range.min}
|
|
@@ -407,7 +426,7 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
407
426
|
className="tf-checkout-tickets__wl-toggle"
|
|
408
427
|
onClick={() => setWlOpen((w) => ({ ...w, [t.id]: true }))}
|
|
409
428
|
>
|
|
410
|
-
|
|
429
|
+
{tr('tickets.joinWaitingList')}
|
|
411
430
|
</button>
|
|
412
431
|
))}
|
|
413
432
|
</div>
|
|
@@ -447,7 +466,7 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
447
466
|
|
|
448
467
|
{isTimeSlot && dates.length > 0 && (
|
|
449
468
|
<div className="tf-checkout-tickets__currency">
|
|
450
|
-
<span className="tf-checkout-tickets__currency-label">
|
|
469
|
+
<span className="tf-checkout-tickets__currency-label">{tr('common.date')}</span>
|
|
451
470
|
<select
|
|
452
471
|
className="tf-checkout-select"
|
|
453
472
|
value={slotDate}
|
|
@@ -467,7 +486,9 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
467
486
|
|
|
468
487
|
{checkout.currency && checkout.currency.available.length > 1 && (
|
|
469
488
|
<div className="tf-checkout-tickets__currency">
|
|
470
|
-
<span className="tf-checkout-tickets__currency-label">
|
|
489
|
+
<span className="tf-checkout-tickets__currency-label">
|
|
490
|
+
{tr('common.currency')}
|
|
491
|
+
</span>
|
|
471
492
|
<select
|
|
472
493
|
className="tf-checkout-select"
|
|
473
494
|
value={checkout.currency.currency}
|
|
@@ -483,7 +504,7 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
483
504
|
)}
|
|
484
505
|
|
|
485
506
|
{isTimeSlot && !slotQuery.isLoading && tickets.length === 0 && slotDate && (
|
|
486
|
-
<p className="tf-checkout-note">
|
|
507
|
+
<p className="tf-checkout-note">{tr('tickets.noSessions')}</p>
|
|
487
508
|
)}
|
|
488
509
|
|
|
489
510
|
{isTimeSlot
|
|
@@ -504,14 +525,16 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
504
525
|
className="tf-checkout-tickets__promo-toggle"
|
|
505
526
|
onClick={() => setPromoOpen(true)}
|
|
506
527
|
>
|
|
507
|
-
|
|
528
|
+
{tr('tickets.gotPromoCode')}
|
|
508
529
|
</button>
|
|
509
530
|
) : (
|
|
510
531
|
<>
|
|
511
532
|
<FieldRow>
|
|
512
533
|
<Field>
|
|
513
534
|
<Input
|
|
514
|
-
placeholder={
|
|
535
|
+
placeholder={
|
|
536
|
+
isAccess ? tr('tickets.accessCode') : tr('tickets.promoCode')
|
|
537
|
+
}
|
|
515
538
|
value={code}
|
|
516
539
|
onChange={(e) => setCode(e.target.value)}
|
|
517
540
|
onKeyDown={(e) => e.key === 'Enter' && applyCode()}
|
|
@@ -551,10 +574,10 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
551
574
|
disabled={checkout.isBusy || selected.length === 0}
|
|
552
575
|
>
|
|
553
576
|
{checkout.isBusy
|
|
554
|
-
? '
|
|
577
|
+
? tr('tickets.reserving')
|
|
555
578
|
: selectionIsTable
|
|
556
|
-
? '
|
|
557
|
-
:
|
|
579
|
+
? tr('tickets.reserveTables')
|
|
580
|
+
: buttonText}
|
|
558
581
|
</Button>
|
|
559
582
|
)}
|
|
560
583
|
|
|
@@ -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
|
+
}
|
package/src/styles/tickets.css
CHANGED
|
@@ -39,6 +39,21 @@
|
|
|
39
39
|
font-size: 14px;
|
|
40
40
|
line-height: 1.25;
|
|
41
41
|
}
|
|
42
|
+
/* Organiser's scarcity badge ("Selling fast"). Its own token so a host can
|
|
43
|
+
recolour it without also recolouring validation errors. */
|
|
44
|
+
.tf-checkout-tickets__badge {
|
|
45
|
+
display: inline-block;
|
|
46
|
+
max-width: 100%;
|
|
47
|
+
margin-top: 4px;
|
|
48
|
+
padding: 2px 7px;
|
|
49
|
+
border-radius: 3px;
|
|
50
|
+
background: var(--tf-checkout-badge, var(--tf-checkout-error));
|
|
51
|
+
color: var(--tf-checkout-badge-contrast, #fff);
|
|
52
|
+
font-size: 11px;
|
|
53
|
+
font-weight: 600;
|
|
54
|
+
line-height: 1.4;
|
|
55
|
+
overflow-wrap: anywhere;
|
|
56
|
+
}
|
|
42
57
|
.tf-checkout-tickets__description-toggle {
|
|
43
58
|
appearance: none;
|
|
44
59
|
border: 0;
|
|
@@ -145,7 +160,9 @@
|
|
|
145
160
|
font-weight: 400;
|
|
146
161
|
text-align: center;
|
|
147
162
|
cursor: pointer;
|
|
148
|
-
transition:
|
|
163
|
+
transition:
|
|
164
|
+
border-color 0.12s,
|
|
165
|
+
background 0.12s;
|
|
149
166
|
}
|
|
150
167
|
.tf-checkout-tickets__promo-toggle:hover {
|
|
151
168
|
background: color-mix(in srgb, var(--tf-checkout-text) 78%, var(--tf-checkout-surface));
|
|
@@ -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
|
|
@@ -50,7 +52,9 @@ export const useInstallmentPicker = () => {
|
|
|
50
52
|
{o.count} × {o.per_installment_formatted ?? o.per_installment}
|
|
51
53
|
</span>
|
|
52
54
|
<span className="tf-checkout-installments__meta">
|
|
53
|
-
{o.total_formatted
|
|
55
|
+
{o.total_formatted
|
|
56
|
+
? t('payment.installmentTotal', { amount: o.total_formatted })
|
|
57
|
+
: ''}
|
|
54
58
|
{o.diff_from_original_formatted
|
|
55
59
|
? ` · ${o.diff_from_original_formatted}`
|
|
56
60
|
: ''}
|
|
@@ -59,7 +63,7 @@ export const useInstallmentPicker = () => {
|
|
|
59
63
|
))}
|
|
60
64
|
</div>
|
|
61
65
|
<Button variant="ghost" block onClick={() => choose(null)}>
|
|
62
|
-
|
|
66
|
+
{t('payment.payInFull')}
|
|
63
67
|
</Button>
|
|
64
68
|
</div>
|
|
65
69
|
</div>
|