tf-checkout-react 2.0.0-beta.2 → 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 +192 -81
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +192 -81
- package/dist/index.js.map +1 -1
- package/dist/styles/tickets.css +18 -1
- package/package.json +1 -1
- package/src/EmbeddedCheckout.tsx +3 -5
- package/src/sections/Confirmation.tsx +2 -2
- package/src/sections/IDVerification.tsx +16 -9
- package/src/sections/OrderConfirmation.tsx +32 -23
- package/src/sections/OrderIDVerification.tsx +23 -11
- package/src/sections/Payment.tsx +1 -1
- package/src/sections/Tickets.tsx +16 -10
- package/src/styles/tickets.css +18 -1
- package/src/ui/InstallmentPicker.tsx +4 -2
- package/src/ui/Loader.tsx +11 -7
- package/src/ui/Stepper.tsx +27 -23
package/dist/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));
|
package/package.json
CHANGED
package/src/EmbeddedCheckout.tsx
CHANGED
|
@@ -28,6 +28,7 @@ import { Payment } from './sections/Payment'
|
|
|
28
28
|
import { SeatMap } from './sections/SeatMap'
|
|
29
29
|
import { Tickets } from './sections/Tickets'
|
|
30
30
|
import { CheckoutProvider, useCheckoutContext } from './ui/CheckoutProvider'
|
|
31
|
+
import { useT } from './ui/I18nProvider'
|
|
31
32
|
import { TimerBanner } from './ui/TimerBanner'
|
|
32
33
|
import type { CheckoutTheme } from './ui/theme'
|
|
33
34
|
|
|
@@ -148,6 +149,7 @@ const Flow: FC<{
|
|
|
148
149
|
readAttributionFromUrl,
|
|
149
150
|
showTestCardHint,
|
|
150
151
|
}) => {
|
|
152
|
+
const t = useT()
|
|
151
153
|
const checkout = useCheckoutContext()
|
|
152
154
|
const [view, setView] = useState<FlowView>(initialView)
|
|
153
155
|
const [selectionView, setSelectionView] = useState<SelectionView>(initialView)
|
|
@@ -329,11 +331,7 @@ const Flow: FC<{
|
|
|
329
331
|
key={authRequest.key}
|
|
330
332
|
open
|
|
331
333
|
initialEmail={authRequest.email}
|
|
332
|
-
message={
|
|
333
|
-
authRequest.email
|
|
334
|
-
? 'This email already has an account. Log in to autofill your checkout details.'
|
|
335
|
-
: undefined
|
|
336
|
-
}
|
|
334
|
+
message={authRequest.email ? t('auth.existingAccount') : undefined}
|
|
337
335
|
allowRegistration={auth?.allowRegistration}
|
|
338
336
|
allowPasswordReset={auth?.allowPasswordReset}
|
|
339
337
|
requireRegistrationAddress={auth?.requireRegistrationAddress}
|
|
@@ -100,7 +100,7 @@ export const Confirmation: FC<ConfirmationProps> = ({
|
|
|
100
100
|
target="_blank"
|
|
101
101
|
rel="noopener noreferrer"
|
|
102
102
|
>
|
|
103
|
-
|
|
103
|
+
{t('confirmation.viewVoucher')}
|
|
104
104
|
</a>
|
|
105
105
|
)}
|
|
106
106
|
|
|
@@ -109,7 +109,7 @@ export const Confirmation: FC<ConfirmationProps> = ({
|
|
|
109
109
|
className="tf-checkout-button tf-checkout-button--secondary tf-checkout-button--block"
|
|
110
110
|
href={viewOrderUrl}
|
|
111
111
|
>
|
|
112
|
-
|
|
112
|
+
{t('confirmation.viewTickets')}
|
|
113
113
|
</a>
|
|
114
114
|
)}
|
|
115
115
|
|
|
@@ -15,16 +15,18 @@ import { Button } from '../ui/Button'
|
|
|
15
15
|
import { Card, SectionHeader } from '../ui/Card'
|
|
16
16
|
import { useCheckoutContext } from '../ui/CheckoutProvider'
|
|
17
17
|
import { cx } from '../ui/cx'
|
|
18
|
+
import { useT } from '../ui/I18nProvider'
|
|
18
19
|
|
|
19
20
|
type UiPhase = IdVerificationPhase | 'starting'
|
|
20
21
|
|
|
22
|
+
// Translation keys, not copy: resolved at render so the text follows the locale.
|
|
21
23
|
const MESSAGE: Record<UiPhase, string> = {
|
|
22
|
-
idle: '
|
|
23
|
-
starting: '
|
|
24
|
-
polling: '
|
|
25
|
-
verified: '
|
|
26
|
-
timeout: '
|
|
27
|
-
error: '
|
|
24
|
+
idle: 'verification.idle',
|
|
25
|
+
starting: 'verification.starting',
|
|
26
|
+
polling: 'verification.polling',
|
|
27
|
+
verified: 'verification.verified',
|
|
28
|
+
timeout: 'verification.timeoutCheckAgain',
|
|
29
|
+
error: 'verification.error',
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
export interface IDVerificationProps {
|
|
@@ -32,6 +34,7 @@ export interface IDVerificationProps {
|
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
export const IDVerification: FC<IDVerificationProps> = ({ className }) => {
|
|
37
|
+
const t = useT()
|
|
35
38
|
const checkout = useCheckoutContext()
|
|
36
39
|
const order = checkout.order
|
|
37
40
|
const [phase, setPhase] = useState<UiPhase>('idle')
|
|
@@ -61,13 +64,17 @@ export const IDVerification: FC<IDVerificationProps> = ({ className }) => {
|
|
|
61
64
|
|
|
62
65
|
return (
|
|
63
66
|
<Card className={cx('tf-checkout-idverify', className)}>
|
|
64
|
-
<SectionHeader title=
|
|
67
|
+
<SectionHeader title={t('verification.title')} />
|
|
65
68
|
<p className={cx('tf-checkout-note', done && 'tf-checkout-idverify__done')}>
|
|
66
|
-
{MESSAGE[phase]}
|
|
69
|
+
{t(MESSAGE[phase])}
|
|
67
70
|
</p>
|
|
68
71
|
{!done && (
|
|
69
72
|
<Button block onClick={run} loading={running} disabled={running}>
|
|
70
|
-
{phase === 'idle'
|
|
73
|
+
{phase === 'idle'
|
|
74
|
+
? t('verification.verify')
|
|
75
|
+
: running
|
|
76
|
+
? t('verification.verifying')
|
|
77
|
+
: t('verification.checkAgain')}
|
|
71
78
|
</Button>
|
|
72
79
|
)}
|
|
73
80
|
{error && <p className="tf-checkout-error-text">{error}</p>}
|
|
@@ -3,6 +3,7 @@ import React, { useEffect, useRef, useState, type FC, type ReactNode } from 'rea
|
|
|
3
3
|
import { errorMessage, ttf } from 'tf-checkout-shared'
|
|
4
4
|
|
|
5
5
|
import { cx } from '../ui/cx'
|
|
6
|
+
import { useT } from '../ui/I18nProvider'
|
|
6
7
|
import { Loader } from '../ui/Loader'
|
|
7
8
|
|
|
8
9
|
type ConfirmationData = Record<string, any>
|
|
@@ -36,6 +37,9 @@ export interface OrderConfirmationProps {
|
|
|
36
37
|
className?: string
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
// Marks "no order hash" in `failure` state; any other value is a server message.
|
|
41
|
+
const MISSING_REFERENCE = '\u0000missing-reference'
|
|
42
|
+
|
|
39
43
|
/**
|
|
40
44
|
* Reloadable post-purchase confirmation. Its markup intentionally preserves
|
|
41
45
|
* the proven legacy checkout class contract so existing white-label themes do
|
|
@@ -43,7 +47,7 @@ export interface OrderConfirmationProps {
|
|
|
43
47
|
*/
|
|
44
48
|
export const OrderConfirmation: FC<OrderConfirmationProps> = ({
|
|
45
49
|
orderHash,
|
|
46
|
-
title
|
|
50
|
+
title,
|
|
47
51
|
message,
|
|
48
52
|
helper,
|
|
49
53
|
eventTitle,
|
|
@@ -56,6 +60,7 @@ export const OrderConfirmation: FC<OrderConfirmationProps> = ({
|
|
|
56
60
|
onError,
|
|
57
61
|
className,
|
|
58
62
|
}) => {
|
|
63
|
+
const t = useT()
|
|
59
64
|
const [data, setData] = useState<ConfirmationData | null>(null)
|
|
60
65
|
const [failure, setFailure] = useState('')
|
|
61
66
|
const [copied, setCopied] = useState(false)
|
|
@@ -69,7 +74,7 @@ export const OrderConfirmation: FC<OrderConfirmationProps> = ({
|
|
|
69
74
|
|
|
70
75
|
useEffect(() => {
|
|
71
76
|
if (!orderHash) {
|
|
72
|
-
setFailure(
|
|
77
|
+
setFailure(MISSING_REFERENCE)
|
|
73
78
|
return
|
|
74
79
|
}
|
|
75
80
|
|
|
@@ -98,7 +103,7 @@ export const OrderConfirmation: FC<OrderConfirmationProps> = ({
|
|
|
98
103
|
return (
|
|
99
104
|
<div className={cx('confirmation-page tf-checkout-confirmation-page', className)}>
|
|
100
105
|
<div className="tf-checkout-auth__alert" role="alert">
|
|
101
|
-
{failure}
|
|
106
|
+
{failure === MISSING_REFERENCE ? t('confirmation.missingReference') : failure}
|
|
102
107
|
</div>
|
|
103
108
|
</div>
|
|
104
109
|
)
|
|
@@ -124,17 +129,19 @@ export const OrderConfirmation: FC<OrderConfirmationProps> = ({
|
|
|
124
129
|
...(Number.isFinite(currentPrice)
|
|
125
130
|
? [
|
|
126
131
|
{
|
|
127
|
-
label: '
|
|
132
|
+
label: t('referral.currentPrice'),
|
|
128
133
|
price: `${symbol}${currentPrice.toFixed(2)}`,
|
|
129
134
|
},
|
|
130
135
|
]
|
|
131
136
|
: []),
|
|
132
137
|
...sales.map((sale: ConfirmationData) => ({
|
|
133
|
-
label:
|
|
138
|
+
label: t('referral.ifFriendsBuy', { count: sale.sales }),
|
|
134
139
|
subLabel:
|
|
135
|
-
Number(sale.price) === 0 ? '
|
|
140
|
+
Number(sale.price) === 0 ? t('referral.becomes') : t('referral.goesDownTo'),
|
|
136
141
|
price:
|
|
137
|
-
Number(sale.price) === 0
|
|
142
|
+
Number(sale.price) === 0
|
|
143
|
+
? t('referral.titleFree')
|
|
144
|
+
: `${symbol}${Number(sale.price).toFixed(2)}`,
|
|
138
145
|
})),
|
|
139
146
|
]
|
|
140
147
|
|
|
@@ -150,7 +157,7 @@ export const OrderConfirmation: FC<OrderConfirmationProps> = ({
|
|
|
150
157
|
<div className={cx('confirmation-page tf-checkout-confirmation-page', className)}>
|
|
151
158
|
<div className="header-container">
|
|
152
159
|
<div className="header-product-text">
|
|
153
|
-
<p className="title">{title}</p>
|
|
160
|
+
<p className="title">{title ?? t('confirmation.orderConfirmed')}</p>
|
|
154
161
|
{(eventTitle || eventDate || eventLocation) && (
|
|
155
162
|
<div className="event-details">
|
|
156
163
|
{eventTitle && <div className="event-title">{eventTitle}</div>}
|
|
@@ -165,11 +172,11 @@ export const OrderConfirmation: FC<OrderConfirmationProps> = ({
|
|
|
165
172
|
<>
|
|
166
173
|
<span className="main">
|
|
167
174
|
{attachTickets
|
|
168
|
-
? '
|
|
169
|
-
: (message ?? '
|
|
175
|
+
? t('confirmation.emailed')
|
|
176
|
+
: (message ?? t('confirmation.inMyTickets'))}
|
|
170
177
|
</span>
|
|
171
178
|
<span className="helper">
|
|
172
|
-
{attachTickets ? '
|
|
179
|
+
{attachTickets ? t('confirmation.bringThem') : helper}
|
|
173
180
|
</span>
|
|
174
181
|
</>
|
|
175
182
|
)}
|
|
@@ -187,7 +194,7 @@ export const OrderConfirmation: FC<OrderConfirmationProps> = ({
|
|
|
187
194
|
{orderUrl && (
|
|
188
195
|
<div className="order-details-button-container">
|
|
189
196
|
<a className="view-order-button" href={orderUrl}>
|
|
190
|
-
|
|
197
|
+
{t('confirmation.viewOrder')}
|
|
191
198
|
</a>
|
|
192
199
|
</div>
|
|
193
200
|
)}
|
|
@@ -197,26 +204,26 @@ export const OrderConfirmation: FC<OrderConfirmationProps> = ({
|
|
|
197
204
|
<div className="referral_text_image_section">
|
|
198
205
|
<div className="referral_text_section">
|
|
199
206
|
<div className="referral_title_text">
|
|
200
|
-
|
|
201
|
-
<span className="strong-text">
|
|
202
|
-
|
|
203
|
-
<span className="strong-text">
|
|
207
|
+
{t('referral.titleStart')}
|
|
208
|
+
<span className="strong-text"> {t('referral.titleCheaper')} </span>
|
|
209
|
+
{t('referral.titleOr')}
|
|
210
|
+
<span className="strong-text"> {t('referral.titleFree')}</span>
|
|
204
211
|
</div>
|
|
205
212
|
<div className="referral_text">
|
|
206
|
-
<span className="strong-text">
|
|
207
|
-
|
|
213
|
+
<span className="strong-text">{t('referral.inviteFriends')} </span>
|
|
214
|
+
{t('referral.inviteMiddle')}
|
|
208
215
|
<span className="strong-text"> 100% </span>
|
|
209
|
-
|
|
216
|
+
{t('referral.inviteEnd')}
|
|
210
217
|
</div>
|
|
211
218
|
</div>
|
|
212
219
|
</div>
|
|
213
220
|
<div className="share_wrapper">
|
|
214
221
|
<div className="share_section">
|
|
215
222
|
<div className="invitation_section">
|
|
216
|
-
<div className="invitation_title">
|
|
223
|
+
<div className="invitation_title">{t('referral.howToInvite')}</div>
|
|
217
224
|
<div className="share_buttons">
|
|
218
225
|
<div className="share-by-link">
|
|
219
|
-
<h5 className="share-by-link label">
|
|
226
|
+
<h5 className="share-by-link label">{t('referral.sendThisLink')}</h5>
|
|
220
227
|
<div className="share-btn-inner">
|
|
221
228
|
<input className="share-input" readOnly value={shareUrl} />
|
|
222
229
|
<button
|
|
@@ -224,7 +231,9 @@ export const OrderConfirmation: FC<OrderConfirmationProps> = ({
|
|
|
224
231
|
type="button"
|
|
225
232
|
onClick={copy}
|
|
226
233
|
>
|
|
227
|
-
<span className="copy-icon">
|
|
234
|
+
<span className="copy-icon">
|
|
235
|
+
{copied ? t('referral.copied') : t('referral.copy')}
|
|
236
|
+
</span>
|
|
228
237
|
</button>
|
|
229
238
|
</div>
|
|
230
239
|
</div>
|
|
@@ -255,7 +264,7 @@ export const OrderConfirmation: FC<OrderConfirmationProps> = ({
|
|
|
255
264
|
</div>
|
|
256
265
|
{pricing.length > 0 && (
|
|
257
266
|
<div className="pricing-section">
|
|
258
|
-
<div className="invitation_title">
|
|
267
|
+
<div className="invitation_title">{t('referral.howMuchCheaper')}</div>
|
|
259
268
|
{pricing.map((item, index) => (
|
|
260
269
|
<div className="pricing-section_wrapper" key={`${item.label}-${index}`}>
|
|
261
270
|
<div className="pricing-section_label">
|
|
@@ -10,19 +10,24 @@ import {
|
|
|
10
10
|
import { Button } from '../ui/Button'
|
|
11
11
|
import { Card, SectionHeader } from '../ui/Card'
|
|
12
12
|
import { cx } from '../ui/cx'
|
|
13
|
+
import { useT } from '../ui/I18nProvider'
|
|
13
14
|
|
|
14
15
|
type UiPhase = IdVerificationPhase | 'checking' | 'starting'
|
|
15
16
|
|
|
17
|
+
// Translation keys, not copy: resolved at render so the text follows the locale.
|
|
16
18
|
const PHASE_MESSAGE: Record<UiPhase, string> = {
|
|
17
|
-
checking: '
|
|
18
|
-
idle: '
|
|
19
|
-
starting: '
|
|
20
|
-
polling: '
|
|
21
|
-
verified: '
|
|
22
|
-
timeout: '
|
|
23
|
-
error: '
|
|
19
|
+
checking: 'verification.checking',
|
|
20
|
+
idle: 'verification.idle',
|
|
21
|
+
starting: 'verification.starting',
|
|
22
|
+
polling: 'verification.polling',
|
|
23
|
+
verified: 'verification.verified',
|
|
24
|
+
timeout: 'verification.timeoutSafe',
|
|
25
|
+
error: 'verification.error',
|
|
24
26
|
}
|
|
25
27
|
|
|
28
|
+
// Marks "no order hash" in `failure` state; any other value is a server message.
|
|
29
|
+
const MISSING_REFERENCE = '\u0000missing-reference'
|
|
30
|
+
|
|
26
31
|
export interface OrderIDVerificationProps {
|
|
27
32
|
orderHash: string
|
|
28
33
|
onVerified?: () => void
|
|
@@ -35,13 +40,14 @@ export const OrderIDVerification: FC<OrderIDVerificationProps> = ({
|
|
|
35
40
|
onVerified,
|
|
36
41
|
className,
|
|
37
42
|
}) => {
|
|
43
|
+
const t = useT()
|
|
38
44
|
const [phase, setPhase] = useState<UiPhase>('checking')
|
|
39
45
|
const [failure, setFailure] = useState('')
|
|
40
46
|
|
|
41
47
|
useEffect(() => {
|
|
42
48
|
if (!orderHash) {
|
|
43
49
|
setPhase('error')
|
|
44
|
-
setFailure(
|
|
50
|
+
setFailure(MISSING_REFERENCE)
|
|
45
51
|
return
|
|
46
52
|
}
|
|
47
53
|
let active = true
|
|
@@ -98,18 +104,24 @@ export const OrderIDVerification: FC<OrderIDVerificationProps> = ({
|
|
|
98
104
|
|
|
99
105
|
return (
|
|
100
106
|
<Card className={cx('tf-checkout-idverify', className)}>
|
|
101
|
-
<SectionHeader title=
|
|
107
|
+
<SectionHeader title={t('verification.title')} />
|
|
102
108
|
<p
|
|
103
109
|
className={cx(
|
|
104
110
|
'tf-checkout-note',
|
|
105
111
|
phase === 'verified' && 'tf-checkout-idverify__done'
|
|
106
112
|
)}
|
|
107
113
|
>
|
|
108
|
-
{failure
|
|
114
|
+
{failure === MISSING_REFERENCE
|
|
115
|
+
? t('confirmation.missingReference')
|
|
116
|
+
: failure || t(PHASE_MESSAGE[phase])}
|
|
109
117
|
</p>
|
|
110
118
|
{phase !== 'verified' && orderHash && (
|
|
111
119
|
<Button block onClick={run} loading={running} disabled={running || !orderHash}>
|
|
112
|
-
{phase === 'idle'
|
|
120
|
+
{phase === 'idle'
|
|
121
|
+
? t('verification.verify')
|
|
122
|
+
: running
|
|
123
|
+
? t('verification.verifying')
|
|
124
|
+
: t('verification.tryAgain')}
|
|
113
125
|
</Button>
|
|
114
126
|
)}
|
|
115
127
|
</Card>
|
package/src/sections/Payment.tsx
CHANGED
package/src/sections/Tickets.tsx
CHANGED
|
@@ -35,6 +35,7 @@ const WaitingListForm: FC<{
|
|
|
35
35
|
ticket: TicketType
|
|
36
36
|
onJoin: (entry: WaitingListEntry) => Promise<void>
|
|
37
37
|
}> = ({ ticket, onJoin }) => {
|
|
38
|
+
const tr = useT()
|
|
38
39
|
const [form, setForm] = useState({ firstName: '', lastName: '', email: '' })
|
|
39
40
|
const [state, setState] = useState<'idle' | 'busy' | 'done' | 'error'>('idle')
|
|
40
41
|
const [error, setError] = useState('')
|
|
@@ -43,7 +44,7 @@ const WaitingListForm: FC<{
|
|
|
43
44
|
if (state === 'done')
|
|
44
45
|
return (
|
|
45
46
|
<p className="tf-checkout-note tf-checkout-tickets__wl-done">
|
|
46
|
-
|
|
47
|
+
{tr('tickets.waitingListJoined')}
|
|
47
48
|
</p>
|
|
48
49
|
)
|
|
49
50
|
|
|
@@ -52,14 +53,14 @@ const WaitingListForm: FC<{
|
|
|
52
53
|
<FieldRow>
|
|
53
54
|
<Field>
|
|
54
55
|
<Input
|
|
55
|
-
placeholder=
|
|
56
|
+
placeholder={tr('tickets.firstName')}
|
|
56
57
|
value={form.firstName}
|
|
57
58
|
onChange={(e) => setForm((f) => ({ ...f, firstName: e.target.value }))}
|
|
58
59
|
/>
|
|
59
60
|
</Field>
|
|
60
61
|
<Field>
|
|
61
62
|
<Input
|
|
62
|
-
placeholder=
|
|
63
|
+
placeholder={tr('tickets.lastName')}
|
|
63
64
|
value={form.lastName}
|
|
64
65
|
onChange={(e) => setForm((f) => ({ ...f, lastName: e.target.value }))}
|
|
65
66
|
/>
|
|
@@ -67,7 +68,7 @@ const WaitingListForm: FC<{
|
|
|
67
68
|
</FieldRow>
|
|
68
69
|
<Field>
|
|
69
70
|
<Input
|
|
70
|
-
placeholder=
|
|
71
|
+
placeholder={tr('tickets.email')}
|
|
71
72
|
value={form.email}
|
|
72
73
|
onChange={(e) => setForm((f) => ({ ...f, email: e.target.value }))}
|
|
73
74
|
/>
|
|
@@ -87,7 +88,7 @@ const WaitingListForm: FC<{
|
|
|
87
88
|
disabled={!ok || state === 'busy'}
|
|
88
89
|
variant="secondary"
|
|
89
90
|
>
|
|
90
|
-
|
|
91
|
+
{tr('tickets.joinWaitingList')}
|
|
91
92
|
</Button>
|
|
92
93
|
{error && <p className="tf-checkout-error-text">{error}</p>}
|
|
93
94
|
</div>
|
|
@@ -343,6 +344,11 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
343
344
|
</button>
|
|
344
345
|
)}
|
|
345
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
|
+
)}
|
|
346
352
|
<div className="tf-checkout-tickets__price">
|
|
347
353
|
{discounted && (
|
|
348
354
|
<span className="tf-checkout-tickets__old">
|
|
@@ -355,18 +361,18 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
355
361
|
isFree && 'tf-checkout-tickets__amount--free'
|
|
356
362
|
)}
|
|
357
363
|
>
|
|
358
|
-
{isFree ? '
|
|
364
|
+
{isFree ? tr('tickets.free') : money(sym, t.price)}
|
|
359
365
|
</span>
|
|
360
366
|
{/* Fees sub-line (the "(… with fees)" the old checkout shows) —
|
|
361
367
|
omitted for free/sold-out types. */}
|
|
362
368
|
{!isFree && !t.soldOut && (
|
|
363
369
|
<span className="tf-checkout-tickets__fees">
|
|
364
|
-
({money(sym, t.priceWithFees)}
|
|
370
|
+
{tr('tickets.withFees', { price: money(sym, t.priceWithFees) })}
|
|
365
371
|
</span>
|
|
366
372
|
)}
|
|
367
373
|
{t.depositPercent != null && (
|
|
368
374
|
<span className="tf-checkout-tickets__deposit">
|
|
369
|
-
{t.depositPercent.toFixed(2)}
|
|
375
|
+
{tr('tickets.deposit', { percent: t.depositPercent.toFixed(2) })}
|
|
370
376
|
</span>
|
|
371
377
|
)}
|
|
372
378
|
</div>
|
|
@@ -420,7 +426,7 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
420
426
|
className="tf-checkout-tickets__wl-toggle"
|
|
421
427
|
onClick={() => setWlOpen((w) => ({ ...w, [t.id]: true }))}
|
|
422
428
|
>
|
|
423
|
-
|
|
429
|
+
{tr('tickets.joinWaitingList')}
|
|
424
430
|
</button>
|
|
425
431
|
))}
|
|
426
432
|
</div>
|
|
@@ -519,7 +525,7 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
519
525
|
className="tf-checkout-tickets__promo-toggle"
|
|
520
526
|
onClick={() => setPromoOpen(true)}
|
|
521
527
|
>
|
|
522
|
-
|
|
528
|
+
{tr('tickets.gotPromoCode')}
|
|
523
529
|
</button>
|
|
524
530
|
) : (
|
|
525
531
|
<>
|
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));
|
|
@@ -52,7 +52,9 @@ export const useInstallmentPicker = () => {
|
|
|
52
52
|
{o.count} × {o.per_installment_formatted ?? o.per_installment}
|
|
53
53
|
</span>
|
|
54
54
|
<span className="tf-checkout-installments__meta">
|
|
55
|
-
{o.total_formatted
|
|
55
|
+
{o.total_formatted
|
|
56
|
+
? t('payment.installmentTotal', { amount: o.total_formatted })
|
|
57
|
+
: ''}
|
|
56
58
|
{o.diff_from_original_formatted
|
|
57
59
|
? ` · ${o.diff_from_original_formatted}`
|
|
58
60
|
: ''}
|
|
@@ -61,7 +63,7 @@ export const useInstallmentPicker = () => {
|
|
|
61
63
|
))}
|
|
62
64
|
</div>
|
|
63
65
|
<Button variant="ghost" block onClick={() => choose(null)}>
|
|
64
|
-
|
|
66
|
+
{t('payment.payInFull')}
|
|
65
67
|
</Button>
|
|
66
68
|
</div>
|
|
67
69
|
</div>
|
package/src/ui/Loader.tsx
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import React, { type FC } from 'react'
|
|
2
2
|
|
|
3
3
|
import { cx } from './cx'
|
|
4
|
+
import { useT } from './I18nProvider'
|
|
4
5
|
|
|
5
6
|
/** A spinner that inherits `currentColor` (so it works inside buttons/text). */
|
|
6
|
-
export const Loader: FC<{ className?: string }> = ({ className }) =>
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
)
|
|
7
|
+
export const Loader: FC<{ className?: string }> = ({ className }) => {
|
|
8
|
+
const t = useT()
|
|
9
|
+
return (
|
|
10
|
+
<span
|
|
11
|
+
className={cx('tf-checkout-loader', className)}
|
|
12
|
+
role="status"
|
|
13
|
+
aria-label={t('common.loading')}
|
|
14
|
+
/>
|
|
15
|
+
)
|
|
16
|
+
}
|
package/src/ui/Stepper.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { type FC } from 'react'
|
|
2
2
|
|
|
3
3
|
import { cx } from './cx'
|
|
4
|
+
import { useT } from './I18nProvider'
|
|
4
5
|
|
|
5
6
|
/** Quantity ± control (ticket / add-on quantities). */
|
|
6
7
|
export const Stepper: FC<{
|
|
@@ -11,26 +12,29 @@ export const Stepper: FC<{
|
|
|
11
12
|
step?: number
|
|
12
13
|
disabled?: boolean
|
|
13
14
|
className?: string
|
|
14
|
-
}> = ({ value, onChange, min = 0, max = Infinity, step = 1, disabled, className }) =>
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
15
|
+
}> = ({ value, onChange, min = 0, max = Infinity, step = 1, disabled, className }) => {
|
|
16
|
+
const t = useT()
|
|
17
|
+
return (
|
|
18
|
+
<div className={cx('tf-checkout-stepper', className)}>
|
|
19
|
+
<button
|
|
20
|
+
type="button"
|
|
21
|
+
className="tf-checkout-stepper__btn"
|
|
22
|
+
aria-label={t('common.decrease')}
|
|
23
|
+
disabled={disabled || value <= min}
|
|
24
|
+
onClick={() => onChange(Math.max(min, value - step))}
|
|
25
|
+
>
|
|
26
|
+
−
|
|
27
|
+
</button>
|
|
28
|
+
<span className="tf-checkout-stepper__value">{value}</span>
|
|
29
|
+
<button
|
|
30
|
+
type="button"
|
|
31
|
+
className="tf-checkout-stepper__btn"
|
|
32
|
+
aria-label={t('common.increase')}
|
|
33
|
+
disabled={disabled || value >= max}
|
|
34
|
+
onClick={() => onChange(Math.min(max, value + step))}
|
|
35
|
+
>
|
|
36
|
+
+
|
|
37
|
+
</button>
|
|
38
|
+
</div>
|
|
39
|
+
)
|
|
40
|
+
}
|