tf-checkout-react-native 2.0.0-beta.0 → 2.0.0-beta.11
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/README.md +115 -24
- package/dist/index.d.mts +8195 -282
- package/dist/index.d.ts +8195 -282
- package/dist/index.js +32346 -7001
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +32586 -7128
- package/dist/index.mjs.map +1 -1
- package/package.json +23 -11
- package/scripts/build-seat-map.mjs +31 -0
- package/scripts/seat-map-entry.jsx +22 -0
- package/scripts/verify-discussion-support.cjs +131 -0
- package/src/CardInput.tsx +64 -3
- package/src/EmbeddedCheckout.tsx +140 -38
- package/src/auth/AuthModal.tsx +186 -9
- package/src/auth/EmailCodeForm.tsx +91 -0
- package/src/auth/RegisterForm.tsx +91 -5
- package/src/auth/TwoFactorForm.tsx +74 -0
- package/src/auth/index.ts +4 -0
- package/src/configure.ts +19 -14
- package/src/confirmer.ts +112 -20
- package/src/generated/seatMapWidget.ts +3 -0
- package/src/index.tsx +1 -1
- package/src/sections/Accommodation.tsx +57 -0
- package/src/sections/AccountDeletion.tsx +347 -0
- package/src/sections/AccountEmailEnrichment.tsx +122 -0
- package/src/sections/AccountInformation.tsx +139 -0
- package/src/sections/AccountPaymentPlan.tsx +105 -0
- package/src/sections/AccountRewards.tsx +108 -0
- package/src/sections/AddOns.tsx +99 -148
- package/src/sections/BillingInfo.tsx +80 -78
- package/src/sections/Bundles.tsx +467 -0
- package/src/sections/EventDiscussionView.tsx +655 -0
- package/src/sections/NotificationPreferences.tsx +244 -0
- package/src/sections/OrderSummary.tsx +29 -2
- package/src/sections/Passes.tsx +177 -0
- package/src/sections/Payment.tsx +170 -53
- package/src/sections/PhoneAuthentication.tsx +371 -0
- package/src/sections/PostReaction.tsx +122 -0
- package/src/sections/ProfileSettings.tsx +446 -0
- package/src/sections/Rewards.tsx +320 -0
- package/src/sections/SeatMap.tsx +225 -0
- package/src/sections/SeatsIoMap.tsx +110 -0
- package/src/sections/Streaming.tsx +278 -0
- package/src/sections/TicketChange.tsx +379 -0
- package/src/sections/TicketGift.tsx +206 -0
- package/src/sections/TicketRefund.tsx +104 -0
- package/src/sections/Tickets.tsx +289 -208
- package/src/sections/Trips.tsx +87 -0
- package/src/sections/WaiverSigning.tsx +429 -0
- package/src/sections/billing/PhoneIdentity.tsx +169 -0
- package/src/sections/billing/TicketHoldersCard.tsx +4 -0
- package/src/sections/discussion/CommentImages.tsx +366 -0
- package/src/sections/discussion/SwipeReply.tsx +88 -0
- package/src/sections/index.ts +46 -0
- package/src/sections/rewards/RewardsLadder.tsx +167 -0
- package/src/sections/rewards/RewardsTree.tsx +117 -0
- package/src/storage.ts +8 -9
- package/src/ui/CheckoutProvider.tsx +51 -15
- package/src/ui/CustomFields.tsx +48 -12
- package/src/ui/Field.tsx +21 -3
- package/src/ui/Frozen.tsx +22 -0
- package/src/ui/InstallmentPicker.tsx +15 -18
- package/src/ui/PaymentMethods.tsx +6 -3
- package/src/ui/ProviderPages.tsx +214 -0
- package/src/ui/QuantitySelect.tsx +28 -3
- package/src/ui/Select.tsx +4 -1
- package/src/ui/SignaturePad.tsx +218 -0
- package/src/ui/index.ts +3 -0
- package/src/ui/primitives.tsx +13 -2
- package/src/ui/useAccountT.ts +16 -0
- package/src/useCheckout.ts +3 -1
package/src/sections/Payment.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Accommodation } from './Accommodation'
|
|
1
2
|
/**
|
|
2
3
|
* `<Payment/>` — order review + payment-method selection + T&C, then place the
|
|
3
4
|
* order. The RN analogue of the web `Payment` section. Buyer details come from
|
|
@@ -7,9 +8,8 @@
|
|
|
7
8
|
*
|
|
8
9
|
* Native adaptations vs web (all behaviour-preserving where the platform allows):
|
|
9
10
|
* - No Stripe Payment Element / wallet widget: web folds Apple/Google Pay into
|
|
10
|
-
* the Element
|
|
11
|
-
*
|
|
12
|
-
* wallet button would slot in as an extra row triggering that request.
|
|
11
|
+
* the Element. Native offers Apple/Google Pay as explicit rows only when
|
|
12
|
+
* the backend enables the wallet and Stripe confirms device support.
|
|
13
13
|
* - The card slot renders the native `<CardInput/>` (Stripe `<CardField/>`) for
|
|
14
14
|
* the Stripe card + payment-plan-new-card paths. Xendit card entry tokenizes
|
|
15
15
|
* in a WebView sheet (deferred in `confirmer.ts`), so the Xendit card row
|
|
@@ -21,23 +21,39 @@
|
|
|
21
21
|
* - Conditions render as plain text (HTML stripped) — no DOM to inject markup.
|
|
22
22
|
*/
|
|
23
23
|
import React, { useEffect, useMemo, useState, type FC } from 'react'
|
|
24
|
-
import { View } from 'react-native'
|
|
24
|
+
import { Platform, View } from 'react-native'
|
|
25
|
+
import { isPlatformPaySupported } from '@stripe/stripe-react-native'
|
|
25
26
|
|
|
26
27
|
import {
|
|
28
|
+
ConfirmerBusyError,
|
|
29
|
+
CardClaimError,
|
|
30
|
+
cardClaimStep,
|
|
31
|
+
CartChangedError,
|
|
27
32
|
XENDIT_EWALLET_EXTRA_FIELD,
|
|
33
|
+
cartTicketCount,
|
|
34
|
+
getConfig,
|
|
28
35
|
errorMessage,
|
|
36
|
+
htmlToPlainText,
|
|
29
37
|
missingCheckoutDetails,
|
|
38
|
+
paymentAcknowledgementsMet,
|
|
30
39
|
paymentPlanTerms,
|
|
31
40
|
planConfirmLabel,
|
|
32
41
|
planTermsTitle,
|
|
33
|
-
|
|
42
|
+
stripeOffersOxxo,
|
|
43
|
+
planUsesSavedCard,
|
|
34
44
|
type CheckoutPaymentPlanPreview,
|
|
35
45
|
type EventCondition,
|
|
46
|
+
type LocaleCode,
|
|
47
|
+
type Messages,
|
|
48
|
+
type Translate,
|
|
36
49
|
type PaymentRequest,
|
|
37
50
|
type PlaceOrderResult,
|
|
51
|
+
hooks,
|
|
52
|
+
createTranslator,
|
|
38
53
|
} from 'tf-checkout-shared'
|
|
39
54
|
|
|
40
|
-
import { CardInput } from '../CardInput'
|
|
55
|
+
import { CardInput, claimCardField } from '../CardInput'
|
|
56
|
+
import { rnConfirmer } from '../instance'
|
|
41
57
|
import { useCheckoutContext, useCheckoutValidation } from '../ui/CheckoutProvider'
|
|
42
58
|
import { Checkbox } from '../ui/Field'
|
|
43
59
|
import { PaymentMethods, type PaymentMethodOption } from '../ui/PaymentMethods'
|
|
@@ -50,9 +66,9 @@ import {
|
|
|
50
66
|
MutedText,
|
|
51
67
|
SectionHeader,
|
|
52
68
|
} from '../ui/primitives'
|
|
69
|
+
import { FrozenFields } from '../ui/Frozen'
|
|
53
70
|
import { OrderSummary } from './OrderSummary'
|
|
54
71
|
|
|
55
|
-
type PreOrderMethods = Awaited<ReturnType<typeof ttf.cart.preOrderMethods>>
|
|
56
72
|
|
|
57
73
|
/** Server spellings → display labels for Stripe redirect rails. */
|
|
58
74
|
const REDIRECT_LABELS: Record<string, string> = {
|
|
@@ -72,17 +88,9 @@ const REDIRECT_LABELS: Record<string, string> = {
|
|
|
72
88
|
const redirectLabel = (t: string) =>
|
|
73
89
|
REDIRECT_LABELS[t] ?? t.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase())
|
|
74
90
|
|
|
75
|
-
/**
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
.replace(/<[^>]*>/g, '')
|
|
79
|
-
.replace(/&/g, '&')
|
|
80
|
-
.replace(/</g, '<')
|
|
81
|
-
.replace(/>/g, '>')
|
|
82
|
-
.replace(/ /g, ' ')
|
|
83
|
-
.replace(/'/g, "'")
|
|
84
|
-
.replace(/"/g, '"')
|
|
85
|
-
.trim()
|
|
91
|
+
/** Condition copy as one line of plain text: native has no DOM to render
|
|
92
|
+
* markup into, and the copy is a checkbox label. */
|
|
93
|
+
const conditionText = (html: string) => htmlToPlainText(html).replace(/\s*\n\s*/g, ' ')
|
|
86
94
|
|
|
87
95
|
export interface PaymentProps {
|
|
88
96
|
onComplete?: (order: PlaceOrderResult) => void
|
|
@@ -103,6 +111,10 @@ export interface PaymentProps {
|
|
|
103
111
|
onPrivacyRequired?: () => void
|
|
104
112
|
/** Test card copy is useful in examples, but should never appear by default. */
|
|
105
113
|
showTestCardHint?: boolean
|
|
114
|
+
/** The language of the section's messages (en by default). */
|
|
115
|
+
locale?: LocaleCode
|
|
116
|
+
messages?: Partial<Messages>
|
|
117
|
+
translate?: Translate
|
|
106
118
|
}
|
|
107
119
|
|
|
108
120
|
/**
|
|
@@ -135,31 +147,50 @@ export const Payment: FC<PaymentProps> = ({
|
|
|
135
147
|
requirePrivacyPolicy = false,
|
|
136
148
|
onPrivacyRequired,
|
|
137
149
|
showTestCardHint = false,
|
|
150
|
+
locale,
|
|
151
|
+
messages,
|
|
152
|
+
translate,
|
|
138
153
|
}) => {
|
|
154
|
+
const t = translate ?? createTranslator({ locale, messages })
|
|
139
155
|
const checkout = useCheckoutContext()
|
|
156
|
+
// This checkout's own card field: another checkout on the page has its own.
|
|
157
|
+
const cardOwner = hooks.useTicketCardOwner()
|
|
158
|
+
// A bundle purchase holds the payment SDK: Pay would do nothing now.
|
|
159
|
+
// Why a bundle holds the payment SDK (paying, or setting up a card form).
|
|
160
|
+
const bundleWait = hooks.useTicketGatewayWait(checkout.store)
|
|
161
|
+
const bundleStarting = bundleWait !== null
|
|
140
162
|
const validation = useCheckoutValidation()
|
|
141
163
|
const [error, setError] = useState('')
|
|
142
|
-
const
|
|
164
|
+
const paymentMethods = hooks.useCheckoutPaymentMethods(checkout.store)
|
|
165
|
+
const methods = paymentMethods.data ?? null
|
|
143
166
|
const [selected, setSelected] = useState('')
|
|
144
167
|
const [extras, setExtras] = useState<
|
|
145
168
|
Record<string, { cashtag?: string; phoneNumber?: string }>
|
|
146
169
|
>({})
|
|
147
170
|
const [useSavedCard, setUseSavedCard] = useState(true)
|
|
171
|
+
const [nativeWalletSupported, setNativeWalletSupported] = useState(false)
|
|
148
172
|
|
|
149
173
|
useEffect(() => {
|
|
150
|
-
let
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
.then((
|
|
154
|
-
|
|
174
|
+
let mounted = true
|
|
175
|
+
const env = getConfig().env
|
|
176
|
+
isPlatformPaySupported({ googlePay: { testEnv: env === 'STAG' || env === 'DEV' } })
|
|
177
|
+
.then((supported) => {
|
|
178
|
+
if (mounted) setNativeWalletSupported(supported)
|
|
179
|
+
})
|
|
180
|
+
.catch(() => {
|
|
181
|
+
if (mounted) setNativeWalletSupported(false)
|
|
182
|
+
})
|
|
155
183
|
return () => {
|
|
156
|
-
|
|
184
|
+
mounted = false
|
|
157
185
|
}
|
|
158
186
|
}, [])
|
|
159
187
|
|
|
188
|
+
|
|
189
|
+
|
|
160
190
|
const bd = checkout.breakdown
|
|
161
191
|
const isFree = bd !== undefined && Number(bd.total) === 0
|
|
162
192
|
const isXendit = checkout.gateway === 'xendit'
|
|
193
|
+
const offersOxxo = stripeOffersOxxo(checkout.stripeElementsConfig)
|
|
163
194
|
|
|
164
195
|
// Build the selectable method rows from gateway + probe + snapshot.
|
|
165
196
|
const options = useMemo<PaymentMethodOption[]>(() => {
|
|
@@ -178,9 +209,25 @@ export const Payment: FC<PaymentProps> = ({
|
|
|
178
209
|
})
|
|
179
210
|
} else if (checkout.gateway === 'razorpay') {
|
|
180
211
|
out.push({ id: { kind: 'razorpay' }, value: 'razorpay', label: 'Razorpay' })
|
|
212
|
+
} else if (checkout.gateway === 'adyen') {
|
|
213
|
+
out.push({ id: { kind: 'adyen' }, value: 'adyen', label: 'Adyen' })
|
|
181
214
|
} else {
|
|
182
215
|
// Stripe (or unknown → fail open to card).
|
|
183
|
-
out.push({ id: { kind: 'card' }, value: 'card', label: '
|
|
216
|
+
out.push({ id: { kind: 'card' }, value: 'card', label: 'Credit or debit card' })
|
|
217
|
+
// A device wallet is offered only when Stripe's server configuration AND
|
|
218
|
+
// the current device allow it. Never infer availability from the OS alone.
|
|
219
|
+
if (
|
|
220
|
+
nativeWalletSupported &&
|
|
221
|
+
checkout.walletAvailability?.applePay &&
|
|
222
|
+
Platform.OS === 'ios'
|
|
223
|
+
)
|
|
224
|
+
out.push({ id: { kind: 'applePay' }, value: 'applePay', label: 'Apple Pay' })
|
|
225
|
+
if (
|
|
226
|
+
nativeWalletSupported &&
|
|
227
|
+
checkout.walletAvailability?.googlePay &&
|
|
228
|
+
Platform.OS === 'android'
|
|
229
|
+
)
|
|
230
|
+
out.push({ id: { kind: 'googlePay' }, value: 'googlePay', label: 'Google Pay' })
|
|
184
231
|
for (const t of checkout.redirectPaymentMethods ?? []) {
|
|
185
232
|
if (t === 'paypal' && methods.paypal) continue // dedupe vs PayPal Standard
|
|
186
233
|
out.push({
|
|
@@ -195,8 +242,8 @@ export const Payment: FC<PaymentProps> = ({
|
|
|
195
242
|
value: 'paymentPlan',
|
|
196
243
|
label: 'Payment plan',
|
|
197
244
|
})
|
|
198
|
-
//
|
|
199
|
-
if (
|
|
245
|
+
// The server applies merchant, event, date and capture restrictions.
|
|
246
|
+
if (offersOxxo)
|
|
200
247
|
out.push({ id: { kind: 'oxxo' }, value: 'oxxo', label: 'OXXO (pay in cash)' })
|
|
201
248
|
}
|
|
202
249
|
if (methods.paypal)
|
|
@@ -210,6 +257,9 @@ export const Payment: FC<PaymentProps> = ({
|
|
|
210
257
|
checkout.redirectPaymentMethods,
|
|
211
258
|
checkout.paymentPlan,
|
|
212
259
|
checkout.breakdown,
|
|
260
|
+
checkout.walletAvailability,
|
|
261
|
+
nativeWalletSupported,
|
|
262
|
+
offersOxxo,
|
|
213
263
|
])
|
|
214
264
|
|
|
215
265
|
// Default the selection to the first row once options exist.
|
|
@@ -222,11 +272,21 @@ export const Payment: FC<PaymentProps> = ({
|
|
|
222
272
|
const selectedOption = options.find((o) => o.value === selected)
|
|
223
273
|
const isCardSelected = selected === cardValue
|
|
224
274
|
const isPlanSelected = selectedOption?.id.kind === 'paymentPlan'
|
|
275
|
+
const savedLast4 = methods?.savedPlanCardLast4
|
|
276
|
+
const reuseSavedCard = planUsesSavedCard(savedLast4, useSavedCard)
|
|
225
277
|
// The native Stripe card field: shown for the Stripe card path and for a
|
|
226
278
|
// payment-plan NEW card (SetupIntent). Xendit card is a WebView sheet (below).
|
|
227
279
|
const showStripeCardField =
|
|
228
|
-
!isXendit && (isCardSelected || (isPlanSelected && !
|
|
280
|
+
!isXendit && (isCardSelected || (isPlanSelected && !reuseSavedCard))
|
|
229
281
|
const showXenditCardNote = isXendit && isCardSelected
|
|
282
|
+
// One card form at a time owns the shared payment SDK (bundles and tickets
|
|
283
|
+
// alike): while another one does, this form offers "Continue paying here".
|
|
284
|
+
const ticketCard = hooks.useTicketCardForm({
|
|
285
|
+
confirmer: rnConfirmer,
|
|
286
|
+
store: checkout.store,
|
|
287
|
+
owner: cardOwner,
|
|
288
|
+
showsStripeField: showStripeCardField && !isFree,
|
|
289
|
+
})
|
|
230
290
|
|
|
231
291
|
// When there's only ONE way to pay and it's a card, there's nothing to
|
|
232
292
|
// *choose* — render the field directly, no radio wrapper (matching web).
|
|
@@ -251,17 +311,19 @@ export const Payment: FC<PaymentProps> = ({
|
|
|
251
311
|
)
|
|
252
312
|
const billingReady = missingDetails.length === 0
|
|
253
313
|
const conditions = [...(checkout.conditions ?? []), ...additionalConditions]
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
314
|
+
const { conditionsOk, privacyOk } = paymentAcknowledgementsMet({
|
|
315
|
+
eventConditions: checkout.conditions,
|
|
316
|
+
additionalConditions,
|
|
317
|
+
acceptedConditions: checkout.acceptedConditions,
|
|
318
|
+
requirePrivacyPolicy,
|
|
319
|
+
privacyAccepted: checkout.consent.ttfOptIn,
|
|
320
|
+
})
|
|
258
321
|
const extra = selectedOption?.extra
|
|
259
322
|
const extrasOk =
|
|
260
323
|
!extra ||
|
|
261
324
|
(extra === 'cashtag'
|
|
262
325
|
? Boolean(extras[selected]?.cashtag?.trim())
|
|
263
326
|
: Boolean(extras[selected]?.phoneNumber?.trim()))
|
|
264
|
-
const privacyOk = !requirePrivacyPolicy || checkout.consent.ttfOptIn
|
|
265
327
|
// The plan CTA carries the brand's terminology ("Confirm Layaway"), and the
|
|
266
328
|
// terms sentence quotes this same label — they must agree.
|
|
267
329
|
const payLabel = isFree
|
|
@@ -280,15 +342,22 @@ export const Payment: FC<PaymentProps> = ({
|
|
|
280
342
|
validation.requestValidation()
|
|
281
343
|
return
|
|
282
344
|
}
|
|
345
|
+
// This checkout's own card field is claimed inside the confirmer gateway,
|
|
346
|
+
// together with the order, so a bundle holding the payment SDK never
|
|
347
|
+
// loses its field to a click that cannot proceed.
|
|
348
|
+
const usesOwnCardField = showStripeCardField && !isFree
|
|
283
349
|
// Backfill one holder per ticket if billing was set without holders.
|
|
284
350
|
if (checkout.holders.length === 0 && b) {
|
|
285
|
-
const n = checkout
|
|
351
|
+
const n = cartTicketCount(checkout)
|
|
352
|
+
// Marked as filled in, not entered: a guardian's signature may count
|
|
353
|
+
// under the buyer's name only on these (`holderBackfilledFrom`).
|
|
286
354
|
checkout.setHolders(
|
|
287
355
|
Array.from({ length: n }, () => ({
|
|
288
356
|
firstName: b.firstName,
|
|
289
357
|
lastName: b.lastName,
|
|
290
358
|
email: b.email,
|
|
291
|
-
}))
|
|
359
|
+
})),
|
|
360
|
+
{ backfilled: true }
|
|
292
361
|
)
|
|
293
362
|
}
|
|
294
363
|
let req: PaymentRequest = { method: 'auto' }
|
|
@@ -298,18 +367,40 @@ export const Payment: FC<PaymentProps> = ({
|
|
|
298
367
|
req = { method: 'xenditEWallet', walletKey: id.walletKey, extra: extras[selected] }
|
|
299
368
|
else if (id.kind === 'redirect')
|
|
300
369
|
req = { method: 'redirect', methodType: id.methodType }
|
|
301
|
-
else if (id.kind === 'paymentPlan') req = { method: 'paymentPlan', useSavedCard }
|
|
370
|
+
else if (id.kind === 'paymentPlan') req = { method: 'paymentPlan', useSavedCard: reuseSavedCard }
|
|
302
371
|
else req = { method: id.kind } as PaymentRequest
|
|
303
372
|
// Called synchronously in the press handler — popup rails (e-wallet,
|
|
304
373
|
// redirect, razorpay, paypal) want the user gesture on their WebView host.
|
|
374
|
+
// `placeOrder` runs in the confirmer gateway, so a bundle or another
|
|
375
|
+
// checkout cannot point the one payment SDK elsewhere mid-order. It still
|
|
376
|
+
// starts inside this press.
|
|
377
|
+
// The card field is claimed inside the order, after the payment SDK holds
|
|
378
|
+
// the ticket order's keys and right before the card is confirmed. Whether
|
|
379
|
+
// a key change empties the field is decided in shared (`cardClaimStep`).
|
|
305
380
|
checkout
|
|
306
|
-
.placeOrder(
|
|
381
|
+
.placeOrder(
|
|
382
|
+
req,
|
|
383
|
+
usesOwnCardField
|
|
384
|
+
? {
|
|
385
|
+
claimCard: cardClaimStep('nativeCard', () => claimCardField(cardOwner)),
|
|
386
|
+
}
|
|
387
|
+
: undefined
|
|
388
|
+
)
|
|
307
389
|
.then((order) => onComplete?.(order))
|
|
308
|
-
|
|
390
|
+
// Cart ownership and native field ownership each fail before a charge.
|
|
391
|
+
.catch((e) =>
|
|
392
|
+
setError(
|
|
393
|
+
e instanceof CartChangedError
|
|
394
|
+
? t('checkout.error.cartChanged')
|
|
395
|
+
: e instanceof CardClaimError || e instanceof ConfirmerBusyError
|
|
396
|
+
? createTranslator()(e.message)
|
|
397
|
+
: errorMessage(e)
|
|
398
|
+
)
|
|
399
|
+
)
|
|
309
400
|
}
|
|
310
401
|
|
|
311
|
-
|
|
312
|
-
const checkoutError = checkout.error
|
|
402
|
+
// A card claim stores its translation key; a server message passes through.
|
|
403
|
+
const checkoutError = checkout.error ? createTranslator()(checkout.error) : ''
|
|
313
404
|
|
|
314
405
|
// The card/plan field + its note + plan terms. Rendered inside the selected
|
|
315
406
|
// method row when there's a choice, or directly when there's only a card.
|
|
@@ -318,7 +409,16 @@ export const Payment: FC<PaymentProps> = ({
|
|
|
318
409
|
: 'Test card: 4242 4242 4242 4242 · any future date · any CVC'
|
|
319
410
|
const paymentInput = (
|
|
320
411
|
<>
|
|
321
|
-
{showStripeCardField
|
|
412
|
+
{showStripeCardField && ticketCard.cardElsewhere ? (
|
|
413
|
+
<>
|
|
414
|
+
<MutedText>{createTranslator()('bundles.payingElsewhere')}</MutedText>
|
|
415
|
+
<Button variant="ghost" onPress={ticketCard.payHere}>
|
|
416
|
+
{createTranslator()('bundles.payHere')}
|
|
417
|
+
</Button>
|
|
418
|
+
</>
|
|
419
|
+
) : showStripeCardField ? (
|
|
420
|
+
<CardInput owner={cardOwner} />
|
|
421
|
+
) : null}
|
|
322
422
|
{showXenditCardNote ? (
|
|
323
423
|
// Xendit card tokenizes in a WebView sheet (deferred — see confirmer.ts).
|
|
324
424
|
<MutedText>
|
|
@@ -339,7 +439,11 @@ export const Payment: FC<PaymentProps> = ({
|
|
|
339
439
|
<MutedText>
|
|
340
440
|
Your payment will proceed with the card ending in **** {savedLast4}
|
|
341
441
|
</MutedText>
|
|
342
|
-
<Checkbox
|
|
442
|
+
<Checkbox
|
|
443
|
+
checked={useSavedCard}
|
|
444
|
+
onChange={setUseSavedCard}
|
|
445
|
+
label="Use this card"
|
|
446
|
+
/>
|
|
343
447
|
</View>
|
|
344
448
|
) : null}
|
|
345
449
|
</>
|
|
@@ -347,6 +451,7 @@ export const Payment: FC<PaymentProps> = ({
|
|
|
347
451
|
|
|
348
452
|
return (
|
|
349
453
|
<View style={{ gap: 12 }}>
|
|
454
|
+
{checkout.eventId && <Accommodation eventId={checkout.eventId} />}
|
|
350
455
|
<Card>
|
|
351
456
|
<SectionHeader title="Payment" />
|
|
352
457
|
|
|
@@ -369,7 +474,7 @@ export const Payment: FC<PaymentProps> = ({
|
|
|
369
474
|
}
|
|
370
475
|
showValidation={validation.attempt > 0}
|
|
371
476
|
inputSlotFor={
|
|
372
|
-
isPlanSelected ? (
|
|
477
|
+
isPlanSelected ? (reuseSavedCard ? undefined : 'paymentPlan') : cardValue
|
|
373
478
|
}
|
|
374
479
|
>
|
|
375
480
|
{paymentInput}
|
|
@@ -384,12 +489,14 @@ export const Payment: FC<PaymentProps> = ({
|
|
|
384
489
|
validation.attempt > 0 && c.isRequired && !checkout.acceptedConditions?.[c.id]
|
|
385
490
|
return (
|
|
386
491
|
<View key={c.id} style={{ gap: 4 }}>
|
|
387
|
-
<
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
492
|
+
<FrozenFields frozen={checkout.orderFixed}>
|
|
493
|
+
<Checkbox
|
|
494
|
+
checked={Boolean(checkout.acceptedConditions?.[c.id])}
|
|
495
|
+
invalid={invalid}
|
|
496
|
+
onChange={(next) => checkout.acceptCondition(c.id, next)}
|
|
497
|
+
label={`${conditionText(c.content || c.name)}${c.isRequired ? ' *' : ''}`}
|
|
498
|
+
/>
|
|
499
|
+
</FrozenFields>
|
|
393
500
|
{invalid ? (
|
|
394
501
|
<ErrorText>You must accept this condition to continue.</ErrorText>
|
|
395
502
|
) : null}
|
|
@@ -398,18 +505,28 @@ export const Payment: FC<PaymentProps> = ({
|
|
|
398
505
|
})}
|
|
399
506
|
</Card>
|
|
400
507
|
|
|
401
|
-
<
|
|
508
|
+
{!isFree && paymentMethods.error ? <Card><ErrorText>{paymentMethods.error.message}</ErrorText><Button onPress={()=>void paymentMethods.refetch()}>Try again</Button></Card> : null}
|
|
509
|
+
<OrderSummary translate={t} />
|
|
402
510
|
|
|
403
511
|
<Button
|
|
404
|
-
onPress={pay}
|
|
512
|
+
onPress={() => pay()}
|
|
405
513
|
// Stay busy through 'succeeded' too: in redirect mode the checkout
|
|
406
514
|
// remains mounted while the host navigates away, so the button must not
|
|
407
515
|
// flip back to a clickable state before the redirect lands.
|
|
408
516
|
loading={finalizing}
|
|
409
|
-
disabled={
|
|
517
|
+
disabled={
|
|
518
|
+
checkout.isBusy ||
|
|
519
|
+
finalizing ||
|
|
520
|
+
bundleStarting ||
|
|
521
|
+
ticketCard.cardElsewhere ||
|
|
522
|
+
(!isFree && !methods)
|
|
523
|
+
}
|
|
410
524
|
>
|
|
411
525
|
{finalizing ? 'Placing…' : payLabel}
|
|
412
526
|
</Button>
|
|
527
|
+
{bundleStarting ? (
|
|
528
|
+
<MutedText>{createTranslator()(bundleWait ?? 'tickets.waitBundle')}</MutedText>
|
|
529
|
+
) : null}
|
|
413
530
|
|
|
414
531
|
{error || checkoutError ? <ErrorText>{error || checkoutError}</ErrorText> : null}
|
|
415
532
|
</View>
|