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.
@@ -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({ id: { kind: 'xenditCard' }, value: 'xenditCard', label: 'Card' })
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: 'Card' })
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: 'Payment plan',
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: 'OXXO (pay in cash)' })
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
- ? 'Complete Registration'
299
+ ? t('payment.completeRegistration')
294
300
  : isPlanSelected
295
301
  ? planConfirmLabel(checkout.paymentPlan?.configuration)
296
- : 'Submit Payment'
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="Use this card"
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="Payment" />
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">Payment method</div>
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 ? 'Placing…' : payLabel}
555
+ {finalizing ? t('payment.placing') : payLabel}
552
556
  </Button>
553
557
  </div>
554
558
  {bottomError && <p className="tf-checkout-error-text">{bottomError}</p>}
@@ -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 = 'Get tickets',
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 ? 'Select on map' : 'Select seats'
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">Reserve tables</div>
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 ? 'Hide info' : 'View info'}
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
- ? 'Sold out'
379
+ ? tr('tickets.soldOut')
371
380
  : notStarted
372
- ? 'Sales not started'
373
- : 'Sales Ended'}
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 && <span className="tf-checkout-tickets__guests">Guests</span>}
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">Date</span>
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">Currency</span>
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">No sessions available on this date.</p>
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={isAccess ? 'Access code' : 'Promo code'}
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
- ? 'Reserving…'
571
+ ? tr('tickets.reserving')
555
572
  : selectionIsTable
556
- ? 'Reserve tables'
557
- : buttonLabel}
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">Billing address *</div>
30
+ <div className="tf-checkout-billing__section-label">{t('billing.address')}</div>
29
31
  <Field
30
- error={fieldError('Street address', 'Street address is required.')}
32
+ error={fieldError('Street address', t('billing.streetRequired'))}
31
33
  invalid={Boolean(fieldError('Street address'))}
32
34
  >
33
35
  <Input
34
- placeholder="Street address *"
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', 'City is required.')}
47
+ error={fieldError('City', t('billing.cityRequired'))}
46
48
  invalid={Boolean(fieldError('City'))}
47
49
  >
48
50
  <Input
49
- placeholder="City *"
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', 'ZIP / postcode is required.')}
61
+ error={fieldError('ZIP / postcode', t('billing.postcodeRequired'))}
60
62
  invalid={Boolean(fieldError('ZIP / postcode'))}
61
63
  >
62
64
  <Input
63
- placeholder="ZIP / Postcode *"
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', 'Country is required.')}
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="Country"
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="">— country * —</option>
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', 'State / county is required.')}
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="State / region"
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="">— state / county{states.length > 0 ? ' *' : ''} —</option>
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
- <Card className="tf-checkout-billing__holders-card">
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
- return (
37
- <div key={index} className="tf-checkout-billing__holder">
38
- <div className="tf-checkout-billing__holder-title">Ticket {index + 1}</div>
39
- {holderNamesRequired && (
40
- <FieldRow>
41
- <Field
42
- label="First Name *"
43
- error={fieldError(firstNameKey, 'First name is required.')}
44
- invalid={Boolean(fieldError(firstNameKey))}
45
- >
46
- <Input
47
- value={holder.firstName}
48
- required
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
- aria-invalid={Boolean(fieldError(firstNameKey))}
51
- onChange={(event) =>
52
- patchHolder(index, { firstName: event.target.value })
53
- }
54
- />
55
- </Field>
56
- <Field
57
- label="Last Name *"
58
- error={fieldError(lastNameKey, 'Last name is required.')}
59
- invalid={Boolean(fieldError(lastNameKey))}
60
- >
61
- <Input
62
- value={holder.lastName}
63
- required
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
- aria-invalid={Boolean(fieldError(lastNameKey))}
66
- onChange={(event) =>
67
- patchHolder(index, { lastName: event.target.value })
68
- }
69
- />
70
- </Field>
71
- </FieldRow>
72
- )}
73
- {collectHolderContact && (
74
- <FieldRow>
75
- <Field
76
- label="Email Address *"
77
- error={fieldError(emailKey, 'Enter a valid email address.')}
78
- invalid={Boolean(fieldError(emailKey))}
79
- >
80
- <Input
81
- type="email"
82
- autoComplete="email"
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
- aria-invalid={Boolean(fieldError(emailKey))}
87
- onChange={(event) => patchHolder(index, { email: event.target.value })}
88
- />
89
- </Field>
90
- <Field
91
- label="Phone *"
92
- error={fieldError(phoneKey, 'Enter a valid phone number.')}
93
- invalid={Boolean(fieldError(phoneKey))}
94
- >
95
- <PhoneInput
96
- className="tf-checkout-phone"
97
- inputClassName={`tf-checkout-input tf-checkout-phone__input${
98
- fieldError(phoneKey) ? ' tf-checkout-input--invalid' : ''
99
- }`}
100
- defaultCountry={phoneCountry}
101
- // Matches the buyer phone field in ../BillingInfo.tsx — see
102
- // the note there on why the dial code must stay out of the
103
- // input.
104
- disableDialCodeAndPrefix
105
- showDisabledDialCodeAndPrefix
106
- disableDialCodePrefill
107
- value={holder.phone}
108
- required
109
- inputProps={{
110
- autoComplete: 'tel',
111
- 'aria-label': phoneKey,
112
- 'aria-invalid': Boolean(fieldError(phoneKey)),
113
- }}
114
- onChange={(phone) => patchHolder(index, { phone })}
115
- />
116
- </Field>
117
- </FieldRow>
118
- )}
119
- {renderCustomFields(holder, index)}
120
- </div>
121
- )
122
- })}
123
- </Card>
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 ? <CheckoutRoot checkout={value} {...rest} /> : <SelfProvidedCheckout {...rest} />
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="Choose installments"
39
+ aria-label={t('payment.chooseInstallments')}
38
40
  >
39
41
  <div className="tf-checkout-installments__box">
40
- <strong>Pay in installments?</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