tf-checkout-react 2.0.0-beta.0 → 2.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +16 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.esm.js +550 -287
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +550 -287
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/EmbeddedCheckout.tsx +6 -1
- package/src/auth/AuthModal.tsx +12 -5
- package/src/auth/ForgotPasswordForm.tsx +4 -6
- package/src/auth/LoginForm.tsx +2 -2
- package/src/auth/RegisterForm.tsx +2 -2
- package/src/auth/ResetPasswordForm.tsx +7 -7
- package/src/configure.ts +1 -5
- package/src/confirmer.ts +1 -5
- package/src/sections/AddOns.tsx +14 -7
- package/src/sections/BillingInfo.tsx +107 -53
- package/src/sections/Confirmation.tsx +8 -6
- package/src/sections/OrderSummary.tsx +7 -5
- package/src/sections/Payment.tsx +37 -17
- package/src/sections/Tickets.tsx +32 -15
- package/src/sections/billing/BillingAddressFields.tsx +19 -13
- package/src/sections/billing/TicketHoldersCard.tsx +103 -89
- package/src/sections/billing/fieldRules.ts +43 -0
- package/src/ui/CheckoutProvider.tsx +5 -1
- package/src/ui/InstallmentPicker.tsx +4 -2
- package/src/ui/PaymentMethods.tsx +82 -76
- package/src/ui/TimerBanner.tsx +7 -3
|
@@ -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,96 +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
|
-
|
|
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
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small value rules shared by the billing form's inputs. They live here rather
|
|
3
|
+
* than in `../BillingInfo.tsx` so that file stays inside the checkout
|
|
4
|
+
* architecture budget, and because each one encodes a non-obvious constraint
|
|
5
|
+
* worth stating once.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The phone control keeps the dial code outside the input, so it reports a bare
|
|
10
|
+
* `"+52"` the moment it mounts — before the buyer has typed anything. A string
|
|
11
|
+
* that short is a country prefix, not a number, so treat it as blank: it must
|
|
12
|
+
* not out-rank a saved profile number, and it must never reach the API as a
|
|
13
|
+
* phone in its own right.
|
|
14
|
+
*/
|
|
15
|
+
export const isBlankPhone = (phone?: string) =>
|
|
16
|
+
!phone || /^\+?\d{0,4}$/.test(phone.trim())
|
|
17
|
+
|
|
18
|
+
/** Earliest date of birth the form accepts. Nobody alive predates it. */
|
|
19
|
+
export const DOB_MIN = '1900-01-01'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Today as `yyyy-mm-dd` in the buyer's own timezone. Built from the local
|
|
23
|
+
* calendar parts rather than `toISOString()`, which would roll over to
|
|
24
|
+
* tomorrow's date for anyone east of Greenwich late in the day.
|
|
25
|
+
*/
|
|
26
|
+
export const todayISO = () => {
|
|
27
|
+
const now = new Date()
|
|
28
|
+
return [
|
|
29
|
+
now.getFullYear(),
|
|
30
|
+
String(now.getMonth() + 1).padStart(2, '0'),
|
|
31
|
+
String(now.getDate()).padStart(2, '0'),
|
|
32
|
+
].join('-')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** `dobDay/dobMonth/dobYear` → the `yyyy-mm-dd` a date input expects. */
|
|
36
|
+
export const toDateInputValue = (
|
|
37
|
+
year?: number,
|
|
38
|
+
month?: number,
|
|
39
|
+
day?: number
|
|
40
|
+
): string | undefined =>
|
|
41
|
+
year && month && day
|
|
42
|
+
? `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`
|
|
43
|
+
: undefined
|
|
@@ -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
|
|
@@ -9,15 +9,13 @@
|
|
|
9
9
|
import React, { type FC, type ReactNode } from 'react'
|
|
10
10
|
|
|
11
11
|
import { cx } from './cx'
|
|
12
|
+
import { useT } from './I18nProvider'
|
|
12
13
|
import { Field, Input } from './Field'
|
|
13
14
|
|
|
14
15
|
// The method descriptor + row shape live in the SDK so both platforms render
|
|
15
16
|
// the SAME option list. Imported for local use and re-exported for consumers
|
|
16
17
|
// that already import them from here.
|
|
17
|
-
import type {
|
|
18
|
-
PaymentMethodId,
|
|
19
|
-
PaymentMethodOption,
|
|
20
|
-
} from 'tf-checkout-shared'
|
|
18
|
+
import type { PaymentMethodId, PaymentMethodOption } from 'tf-checkout-shared'
|
|
21
19
|
|
|
22
20
|
export type { PaymentMethodId, PaymentMethodOption }
|
|
23
21
|
|
|
@@ -55,79 +53,87 @@ export const PaymentMethods: FC<PaymentMethodsProps> = ({
|
|
|
55
53
|
children,
|
|
56
54
|
inputSlotFor,
|
|
57
55
|
className,
|
|
58
|
-
}) =>
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
<input
|
|
82
|
-
type="radio"
|
|
83
|
-
name="tf-pay-method"
|
|
84
|
-
className="tf-checkout-payment__method-radio"
|
|
85
|
-
checked={isSelected}
|
|
86
|
-
onChange={() => onSelect(o.value)}
|
|
87
|
-
/>
|
|
88
|
-
{o.iconUrl && (
|
|
89
|
-
<img className="tf-checkout-payment__method-icon" src={o.iconUrl} alt="" />
|
|
56
|
+
}) => {
|
|
57
|
+
const t = useT()
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<div
|
|
61
|
+
className={cx('tf-checkout-payment__methods', className)}
|
|
62
|
+
role="radiogroup"
|
|
63
|
+
aria-label={t('payment.method')}
|
|
64
|
+
>
|
|
65
|
+
{options.map((o) => {
|
|
66
|
+
const isSelected = o.value === selected
|
|
67
|
+
const extraValue =
|
|
68
|
+
o.extra === 'phoneNumber'
|
|
69
|
+
? extraValues[o.value]?.phoneNumber
|
|
70
|
+
: extraValues[o.value]?.cashtag
|
|
71
|
+
const extraInvalid =
|
|
72
|
+
Boolean(isSelected && o.extra && showValidation) && !extraValue?.trim()
|
|
73
|
+
return (
|
|
74
|
+
<label
|
|
75
|
+
key={o.value}
|
|
76
|
+
className={cx(
|
|
77
|
+
'tf-checkout-payment__method',
|
|
78
|
+
isSelected && 'tf-checkout-payment__method--selected'
|
|
90
79
|
)}
|
|
91
|
-
|
|
92
|
-
|
|
80
|
+
>
|
|
81
|
+
<span className="tf-checkout-payment__method-head">
|
|
82
|
+
<input
|
|
83
|
+
type="radio"
|
|
84
|
+
name="tf-pay-method"
|
|
85
|
+
className="tf-checkout-payment__method-radio"
|
|
86
|
+
checked={isSelected}
|
|
87
|
+
onChange={() => onSelect(o.value)}
|
|
88
|
+
/>
|
|
89
|
+
{o.iconUrl && (
|
|
90
|
+
<img
|
|
91
|
+
className="tf-checkout-payment__method-icon"
|
|
92
|
+
src={o.iconUrl}
|
|
93
|
+
alt=""
|
|
94
|
+
/>
|
|
95
|
+
)}
|
|
96
|
+
<span className="tf-checkout-payment__method-label">{o.label}</span>
|
|
97
|
+
</span>
|
|
93
98
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
invalid={extraInvalid}
|
|
102
|
-
>
|
|
103
|
-
<Input
|
|
104
|
-
inputMode={o.extra === 'phoneNumber' ? 'tel' : 'text'}
|
|
105
|
-
required
|
|
99
|
+
{isSelected && (o.value === inputSlotFor || o.extra) && (
|
|
100
|
+
<div className="tf-checkout-payment__method-body">
|
|
101
|
+
{o.value === inputSlotFor && children}
|
|
102
|
+
{o.extra && (
|
|
103
|
+
<Field
|
|
104
|
+
label={`${EXTRA_LABEL[o.extra]} *`}
|
|
105
|
+
error={extraInvalid ? t('billing.fieldRequired') : undefined}
|
|
106
106
|
invalid={extraInvalid}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
o.extra === 'phoneNumber' ? '
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
onExtraChange(
|
|
118
|
-
o.value,
|
|
107
|
+
>
|
|
108
|
+
<Input
|
|
109
|
+
inputMode={o.extra === 'phoneNumber' ? 'tel' : 'text'}
|
|
110
|
+
required
|
|
111
|
+
invalid={extraInvalid}
|
|
112
|
+
aria-invalid={extraInvalid}
|
|
113
|
+
placeholder={
|
|
114
|
+
o.extra === 'phoneNumber' ? 'e.g. 081234567890' : '$cashtag'
|
|
115
|
+
}
|
|
116
|
+
value={
|
|
119
117
|
o.extra === 'phoneNumber'
|
|
120
|
-
?
|
|
121
|
-
:
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
)
|
|
118
|
+
? (extraValues[o.value]?.phoneNumber ?? '')
|
|
119
|
+
: (extraValues[o.value]?.cashtag ?? '')
|
|
120
|
+
}
|
|
121
|
+
onChange={(e) =>
|
|
122
|
+
onExtraChange(
|
|
123
|
+
o.value,
|
|
124
|
+
o.extra === 'phoneNumber'
|
|
125
|
+
? { phoneNumber: e.target.value }
|
|
126
|
+
: { cashtag: e.target.value }
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
/>
|
|
130
|
+
</Field>
|
|
131
|
+
)}
|
|
132
|
+
</div>
|
|
133
|
+
)}
|
|
134
|
+
</label>
|
|
135
|
+
)
|
|
136
|
+
})}
|
|
137
|
+
</div>
|
|
138
|
+
)
|
|
139
|
+
}
|
package/src/ui/TimerBanner.tsx
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import React, { useState, type FC } from 'react'
|
|
8
8
|
|
|
9
9
|
import { cx } from './cx'
|
|
10
|
+
import { useT } from './I18nProvider'
|
|
10
11
|
|
|
11
12
|
const clock = (s: number) => {
|
|
12
13
|
const m = String(Math.floor(s / 60)).padStart(2, '0')
|
|
@@ -25,9 +26,10 @@ export interface TimerBannerProps {
|
|
|
25
26
|
export const TimerBanner: FC<TimerBannerProps> = ({
|
|
26
27
|
seconds,
|
|
27
28
|
urgentAt = 60,
|
|
28
|
-
message
|
|
29
|
+
message,
|
|
29
30
|
className,
|
|
30
31
|
}) => {
|
|
32
|
+
const t = useT()
|
|
31
33
|
const [dismissed, setDismissed] = useState(false)
|
|
32
34
|
if (seconds === undefined || seconds <= 0 || dismissed) return null
|
|
33
35
|
const urgent = seconds <= urgentAt
|
|
@@ -40,12 +42,14 @@ export const TimerBanner: FC<TimerBannerProps> = ({
|
|
|
40
42
|
className
|
|
41
43
|
)}
|
|
42
44
|
>
|
|
43
|
-
<span className="tf-checkout-timerbanner__msg">
|
|
45
|
+
<span className="tf-checkout-timerbanner__msg">
|
|
46
|
+
{message ?? t('timer.completePurchase')}
|
|
47
|
+
</span>
|
|
44
48
|
<span className="tf-checkout-timerbanner__clock">{clock(seconds)}</span>
|
|
45
49
|
<button
|
|
46
50
|
type="button"
|
|
47
51
|
className="tf-checkout-timerbanner__close"
|
|
48
|
-
aria-label=
|
|
52
|
+
aria-label={t('common.dismiss')}
|
|
49
53
|
onClick={() => setDismissed(true)}
|
|
50
54
|
>
|
|
51
55
|
×
|