tf-checkout-react-native 2.0.0-beta.0 → 2.0.0-beta.10
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 +8194 -282
- package/dist/index.d.ts +8194 -282
- package/dist/index.js +32308 -7001
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +32547 -7127
- 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/Tickets.tsx
CHANGED
|
@@ -9,16 +9,28 @@
|
|
|
9
9
|
* for seated events. Reads the shared engine via `useCheckoutContext()`; primes
|
|
10
10
|
* the cart with `checkout.setTickets`.
|
|
11
11
|
*/
|
|
12
|
-
import React, {
|
|
12
|
+
import React, {
|
|
13
|
+
Fragment,
|
|
14
|
+
useEffect,
|
|
15
|
+
useMemo,
|
|
16
|
+
useRef,
|
|
17
|
+
useState,
|
|
18
|
+
type FC,
|
|
19
|
+
type ReactNode,
|
|
20
|
+
} from 'react'
|
|
13
21
|
import { Pressable, StyleSheet, Text, View } from 'react-native'
|
|
14
22
|
|
|
15
23
|
import {
|
|
24
|
+
ConfirmerBusyError,
|
|
25
|
+
changeTicketQuantity,
|
|
16
26
|
errorMessage,
|
|
17
27
|
hooks,
|
|
28
|
+
htmlToPlainText,
|
|
18
29
|
ticketQuantityRange,
|
|
19
30
|
type TicketSelection,
|
|
20
31
|
type TicketType,
|
|
21
32
|
type WaitingListEntry,
|
|
33
|
+
createTranslator,
|
|
22
34
|
} from 'tf-checkout-shared'
|
|
23
35
|
|
|
24
36
|
import {
|
|
@@ -30,30 +42,13 @@ import {
|
|
|
30
42
|
MutedText,
|
|
31
43
|
SectionHeader,
|
|
32
44
|
} from '../ui/primitives'
|
|
45
|
+
import { FrozenFields } from '../ui/Frozen'
|
|
33
46
|
import { useCheckoutContext, useTheme } from '../ui/CheckoutProvider'
|
|
34
47
|
import { Field, Input } from '../ui/Field'
|
|
35
48
|
import { QuantitySelect } from '../ui/QuantitySelect'
|
|
36
49
|
import { Select } from '../ui/Select'
|
|
37
50
|
import type { ResolvedTheme } from '../ui/theme'
|
|
38
51
|
|
|
39
|
-
// Web renders the rich description via `dangerouslySetInnerHTML`. React Native
|
|
40
|
-
// has no HTML renderer, so we strip tags + decode a few common entities to show
|
|
41
|
-
// the description as plain text (the adaptation for native).
|
|
42
|
-
const stripHtml = (html: string): string =>
|
|
43
|
-
html
|
|
44
|
-
.replace(/<\s*br\s*\/?\s*>/gi, '\n')
|
|
45
|
-
.replace(/<\/\s*(p|div|li|h[1-6])\s*>/gi, '\n')
|
|
46
|
-
.replace(/<[^>]+>/g, '')
|
|
47
|
-
.replace(/ /gi, ' ')
|
|
48
|
-
.replace(/&/gi, '&')
|
|
49
|
-
.replace(/</gi, '<')
|
|
50
|
-
.replace(/>/gi, '>')
|
|
51
|
-
.replace(/'|'/gi, "'")
|
|
52
|
-
.replace(/"/gi, '"')
|
|
53
|
-
.replace(/\n{3,}/g, '\n\n')
|
|
54
|
-
.replace(/[ \t]+\n/g, '\n')
|
|
55
|
-
.trim()
|
|
56
|
-
|
|
57
52
|
const money = (sym: string, n: number): string => `${sym} ${Number(n).toFixed(2)}`
|
|
58
53
|
|
|
59
54
|
/** Inline waiting-list sign-up for a sold-out type. */
|
|
@@ -118,7 +113,29 @@ const WaitingListForm: FC<{
|
|
|
118
113
|
)
|
|
119
114
|
}
|
|
120
115
|
|
|
116
|
+
/** Host-owned visuals over the shared catalog and reservation flow. */
|
|
117
|
+
export interface TicketRowRenderProps {
|
|
118
|
+
ticket: TicketType
|
|
119
|
+
quantity: number
|
|
120
|
+
range: { min: number; max: number; step: number }
|
|
121
|
+
unavailable: boolean
|
|
122
|
+
seated: boolean
|
|
123
|
+
onQuantityChange: (quantity: number) => void
|
|
124
|
+
}
|
|
125
|
+
|
|
121
126
|
export interface TicketsProps {
|
|
127
|
+
/** Replace a row's visuals; availability, selection and cart rules stay SDK-owned. */
|
|
128
|
+
renderTicket?: (props: TicketRowRenderProps) => ReactNode
|
|
129
|
+
/** Replace the reserve CTA without reimplementing reservation or validation. */
|
|
130
|
+
renderContinue?: (props: {
|
|
131
|
+
onPress: () => void
|
|
132
|
+
disabled: boolean
|
|
133
|
+
loading: boolean
|
|
134
|
+
title: string
|
|
135
|
+
}) => ReactNode
|
|
136
|
+
/** Render the catalog directly in the host's own screen/card. */
|
|
137
|
+
unstyled?: boolean
|
|
138
|
+
|
|
122
139
|
eventId: string
|
|
123
140
|
/** Optional heading above the catalog. The legacy storefront shows none, so
|
|
124
141
|
* the default is intentionally hidden. */
|
|
@@ -153,11 +170,20 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
153
170
|
initialPromoCode,
|
|
154
171
|
referrer,
|
|
155
172
|
readAttributionFromUrl = true,
|
|
173
|
+
renderTicket,
|
|
174
|
+
renderContinue,
|
|
175
|
+
unstyled = false,
|
|
156
176
|
}) => {
|
|
157
177
|
const checkout = useCheckoutContext()
|
|
158
178
|
const theme = useTheme()
|
|
159
179
|
const s = useTicketStyles(theme)
|
|
160
180
|
const ticketsQuery = hooks.useTicketsWithMeta(eventId)
|
|
181
|
+
// A bundle order is starting: wait, so the two flows never start at once.
|
|
182
|
+
// The checkout start lock is held by a bundle start or pay (shown as a
|
|
183
|
+
// wait), or by this checkout's own priming (shown as the busy button).
|
|
184
|
+
// Why a bundle holds the payment SDK (paying, or setting up a card form).
|
|
185
|
+
const bundleWait = hooks.useTicketGatewayWait(checkout.store)
|
|
186
|
+
const bundleStarting = bundleWait !== null
|
|
161
187
|
const promoMeta = ticketsQuery.data?.promo
|
|
162
188
|
const showWaitingList = ticketsQuery.data?.showWaitingList ?? false
|
|
163
189
|
|
|
@@ -184,6 +210,14 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
184
210
|
const [code, setCode] = useState('')
|
|
185
211
|
const [codeState, setCodeState] = useState<'idle' | 'busy'>('idle')
|
|
186
212
|
const [promoOpen, setPromoOpen] = useState(false)
|
|
213
|
+
// Another event: its tickets, date and code start fresh.
|
|
214
|
+
hooks.useOnEventChange(eventId, () => {
|
|
215
|
+
setQty({})
|
|
216
|
+
setSlotDate('')
|
|
217
|
+
setCode('')
|
|
218
|
+
setPromoOpen(false)
|
|
219
|
+
setError('')
|
|
220
|
+
})
|
|
187
221
|
const [wlOpen, setWlOpen] = useState<Record<string, boolean>>({})
|
|
188
222
|
const [visibleDescription, setVisibleDescription] = useState<string | null>(null)
|
|
189
223
|
const attributionEvent = useRef<string | null>(null)
|
|
@@ -253,21 +287,10 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
253
287
|
// into multiple types (`allowMultipleTicketTypePurchases`), a new selection
|
|
254
288
|
// REPLACES the previous one — one ticket type per order. Even when multiple are
|
|
255
289
|
// allowed, switching between a table and a regular ticket clears the rest.
|
|
256
|
-
const selectQty = (
|
|
257
|
-
setQty((
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
? Boolean(tickets.find((x) => x.id === activeIds[0])?.isTable)
|
|
261
|
-
: t.isTable
|
|
262
|
-
const switchingKind = activeIds.length > 0 && prevIsTable !== t.isTable
|
|
263
|
-
|
|
264
|
-
if (!allowMultiple) return value > 0 ? { [t.id]: value } : {}
|
|
265
|
-
if (switchingKind && value > 0) return { [t.id]: value }
|
|
266
|
-
const next = { ...prev }
|
|
267
|
-
if (value > 0) next[t.id] = value
|
|
268
|
-
else delete next[t.id]
|
|
269
|
-
return next
|
|
270
|
-
})
|
|
290
|
+
const selectQty = (ticket: TicketType, value: number) =>
|
|
291
|
+
setQty((previous) =>
|
|
292
|
+
changeTicketQuantity(previous, tickets, ticket, value, allowMultiple)
|
|
293
|
+
)
|
|
271
294
|
|
|
272
295
|
// Seated events: the buyer picks on a seat map, so seat-map ticket types show
|
|
273
296
|
// read-only (no quantity control) and the CTA becomes "Select seats". A type
|
|
@@ -296,10 +319,18 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
296
319
|
quantity: qty[t.id] ?? 0,
|
|
297
320
|
currency: t.currency,
|
|
298
321
|
}))
|
|
322
|
+
// `setTickets` runs in the confirmer gateway: while another flow holds the
|
|
323
|
+
// payment SDK nothing is primed, and the error names that flow.
|
|
299
324
|
checkout
|
|
300
325
|
.setTickets(eventId, selections)
|
|
301
326
|
.then(() => onContinue?.())
|
|
302
|
-
.catch((e) =>
|
|
327
|
+
.catch((e) =>
|
|
328
|
+
setError(
|
|
329
|
+
e instanceof ConfirmerBusyError
|
|
330
|
+
? createTranslator()(e.message)
|
|
331
|
+
: errorMessage(e)
|
|
332
|
+
)
|
|
333
|
+
)
|
|
303
334
|
}
|
|
304
335
|
|
|
305
336
|
const applyCode = () => {
|
|
@@ -345,71 +376,90 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
345
376
|
return (
|
|
346
377
|
<Fragment key={t.id}>
|
|
347
378
|
{groupHeading}
|
|
348
|
-
<View style={s.rowWrap}>
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
379
|
+
<View style={renderTicket ? undefined : s.rowWrap}>
|
|
380
|
+
{renderTicket ? (
|
|
381
|
+
renderTicket({
|
|
382
|
+
ticket: t,
|
|
383
|
+
quantity: qty[t.id] ?? 0,
|
|
384
|
+
range,
|
|
385
|
+
unavailable: unavailable || checkout.orderFixed,
|
|
386
|
+
seated: seatMapRow(t),
|
|
387
|
+
onQuantityChange: (value) => {
|
|
388
|
+
if (!checkout.orderFixed) selectQty(t, value)
|
|
389
|
+
},
|
|
390
|
+
})
|
|
391
|
+
) : (
|
|
392
|
+
<>
|
|
393
|
+
<View style={s.row}>
|
|
394
|
+
<View style={s.info}>
|
|
395
|
+
<View style={s.nameRow}>
|
|
396
|
+
<Text style={s.name}>{t.name}</Text>
|
|
397
|
+
{t.descriptionRich ? (
|
|
398
|
+
<Pressable
|
|
399
|
+
accessibilityRole="button"
|
|
400
|
+
onPress={() =>
|
|
401
|
+
setVisibleDescription((cur) => (cur === t.id ? null : t.id))
|
|
402
|
+
}
|
|
403
|
+
>
|
|
404
|
+
<Text style={s.infoToggle}>
|
|
405
|
+
{visibleDescription === t.id ? 'Hide info' : 'View info'}
|
|
406
|
+
</Text>
|
|
407
|
+
</Pressable>
|
|
408
|
+
) : null}
|
|
409
|
+
</View>
|
|
410
|
+
<View style={s.priceRow}>
|
|
411
|
+
{discounted && original != null ? (
|
|
412
|
+
<Text style={s.oldPrice}>{money(sym, original)}</Text>
|
|
413
|
+
) : null}
|
|
414
|
+
<Text style={[s.amount, isFree && s.amountFree]}>
|
|
415
|
+
{isFree ? 'FREE' : money(sym, t.price)}
|
|
362
416
|
</Text>
|
|
363
|
-
|
|
364
|
-
) : null}
|
|
365
|
-
</View>
|
|
366
|
-
<View style={s.priceRow}>
|
|
367
|
-
{discounted && original != null ? (
|
|
368
|
-
<Text style={s.oldPrice}>{money(sym, original)}</Text>
|
|
369
|
-
) : null}
|
|
370
|
-
<Text style={[s.amount, isFree && s.amountFree]}>
|
|
371
|
-
{isFree ? 'FREE' : money(sym, t.price)}
|
|
372
|
-
</Text>
|
|
373
|
-
{/* Native affordance kept from the RN ticket screen: tables price
|
|
417
|
+
{/* Native affordance kept from the RN ticket screen: tables price
|
|
374
418
|
per table. */}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
419
|
+
{t.isTable ? <Text style={s.perTable}>· per table</Text> : null}
|
|
420
|
+
{/* Fees sub-line — omitted for free / sold-out types. */}
|
|
421
|
+
{!isFree && !t.soldOut ? (
|
|
422
|
+
<Text style={s.fees}>
|
|
423
|
+
({money(sym, t.priceWithFees)} with fees)
|
|
424
|
+
</Text>
|
|
425
|
+
) : null}
|
|
426
|
+
{t.depositPercent != null ? (
|
|
427
|
+
<Text style={s.deposit}>
|
|
428
|
+
{t.depositPercent.toFixed(2)}% DEPOSIT
|
|
429
|
+
</Text>
|
|
430
|
+
) : null}
|
|
431
|
+
</View>
|
|
432
|
+
</View>
|
|
433
|
+
{/* Seat-map types are read-only here — no quantity control; the buyer
|
|
386
434
|
picks them on the map via "Select seats". */}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
435
|
+
{seatMapRow(t) ? null : unavailable ? (
|
|
436
|
+
<Text style={s.soldout}>
|
|
437
|
+
{t.soldOut
|
|
438
|
+
? 'Sold out'
|
|
439
|
+
: notStarted
|
|
440
|
+
? 'Sales not started'
|
|
441
|
+
: 'Sales Ended'}
|
|
442
|
+
</Text>
|
|
443
|
+
) : (
|
|
444
|
+
<View style={s.qtyWrap}>
|
|
445
|
+
{/* Tables pick a number of GUESTS (bounded by minGuests/maxGuests),
|
|
398
446
|
not a count of tickets — the label makes that explicit. */}
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
447
|
+
{t.isTable ? <Text style={s.guests}>Guests</Text> : null}
|
|
448
|
+
<QuantitySelect
|
|
449
|
+
value={qty[t.id] ?? 0}
|
|
450
|
+
min={range.min}
|
|
451
|
+
max={range.max}
|
|
452
|
+
step={range.step}
|
|
453
|
+
onChange={(v) => selectQty(t, v)}
|
|
454
|
+
/>
|
|
455
|
+
</View>
|
|
456
|
+
)}
|
|
407
457
|
</View>
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
)
|
|
458
|
+
{t.descriptionRich && visibleDescription === t.id ? (
|
|
459
|
+
<Text style={s.description}>{htmlToPlainText(t.descriptionRich)}</Text>
|
|
460
|
+
) : null}
|
|
461
|
+
</>
|
|
462
|
+
)}
|
|
413
463
|
{canWaitlist ? (
|
|
414
464
|
wlOpen[t.id] ? (
|
|
415
465
|
<WaitingListForm
|
|
@@ -440,123 +490,154 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
440
490
|
else timeGroups.push({ time, items: [t] })
|
|
441
491
|
}
|
|
442
492
|
|
|
493
|
+
const Container = unstyled ? View : Card
|
|
494
|
+
const continueTitle = checkout.isBusy
|
|
495
|
+
? 'Reserving…'
|
|
496
|
+
: selectionIsTable
|
|
497
|
+
? 'Reserve tables'
|
|
498
|
+
: buttonLabel
|
|
499
|
+
|
|
443
500
|
return (
|
|
444
|
-
<
|
|
445
|
-
|
|
501
|
+
<FrozenFields frozen={checkout.orderFixed}>
|
|
502
|
+
<Container>
|
|
503
|
+
{title ? <SectionHeader title={title} /> : null}
|
|
446
504
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
505
|
+
{listQuery.isLoading ? (
|
|
506
|
+
<View style={s.loading}>
|
|
507
|
+
<Loader />
|
|
508
|
+
</View>
|
|
509
|
+
) : null}
|
|
510
|
+
|
|
511
|
+
{listQuery.error ? (
|
|
512
|
+
<ErrorText>{(listQuery.error as Error).message}</ErrorText>
|
|
513
|
+
) : null}
|
|
514
|
+
|
|
515
|
+
{isTimeSlot && dates.length > 0 ? (
|
|
516
|
+
<View style={s.selectBlock}>
|
|
517
|
+
<Text style={s.selectLabel}>Date</Text>
|
|
518
|
+
<Select
|
|
519
|
+
value={slotDate}
|
|
520
|
+
onChange={(d) => {
|
|
521
|
+
setSlotDate(d)
|
|
522
|
+
setQty({})
|
|
523
|
+
}}
|
|
524
|
+
options={dates.map((d) => ({ label: d, value: d }))}
|
|
525
|
+
/>
|
|
526
|
+
</View>
|
|
527
|
+
) : null}
|
|
528
|
+
|
|
529
|
+
{currency && currency.available.length > 1 ? (
|
|
530
|
+
<View style={s.selectBlock}>
|
|
531
|
+
<Text style={s.selectLabel}>Currency</Text>
|
|
532
|
+
<Select
|
|
533
|
+
value={currency.currency}
|
|
534
|
+
onChange={changeCurrency}
|
|
535
|
+
options={currency.available.map((c) => ({
|
|
536
|
+
label: `${c.name} (${c.code})`,
|
|
537
|
+
value: c.code,
|
|
538
|
+
}))}
|
|
539
|
+
/>
|
|
540
|
+
</View>
|
|
541
|
+
) : null}
|
|
542
|
+
|
|
543
|
+
{isTimeSlot && !slotQuery.isLoading && tickets.length === 0 && slotDate ? (
|
|
544
|
+
<MutedText>No sessions available on this date.</MutedText>
|
|
545
|
+
) : null}
|
|
546
|
+
|
|
547
|
+
{isTimeSlot
|
|
548
|
+
? timeGroups.map((g) => (
|
|
549
|
+
<Fragment key={g.time}>
|
|
550
|
+
{g.time ? <Text style={s.slotTime}>{g.time}</Text> : null}
|
|
551
|
+
{g.items.map(renderRow)}
|
|
552
|
+
</Fragment>
|
|
553
|
+
))
|
|
554
|
+
: tickets.map(renderRow)}
|
|
555
|
+
|
|
556
|
+
{/* Promo / access code */}
|
|
557
|
+
{showCodeInput ? (
|
|
558
|
+
<View style={s.promo}>
|
|
559
|
+
{!isAccess && !promoOpen ? (
|
|
560
|
+
<Pressable accessibilityRole="button" onPress={() => setPromoOpen(true)}>
|
|
561
|
+
<Text style={s.promoToggle}>Got a promo code? Tap here</Text>
|
|
562
|
+
</Pressable>
|
|
563
|
+
) : (
|
|
564
|
+
<>
|
|
565
|
+
<Field>
|
|
566
|
+
<Input
|
|
567
|
+
placeholder={isAccess ? 'Access code' : 'Promo code'}
|
|
568
|
+
value={code}
|
|
569
|
+
autoCapitalize="characters"
|
|
570
|
+
onChangeText={setCode}
|
|
571
|
+
/>
|
|
572
|
+
</Field>
|
|
573
|
+
<Button
|
|
574
|
+
variant="ghost"
|
|
575
|
+
onPress={applyCode}
|
|
576
|
+
loading={codeState === 'busy'}
|
|
577
|
+
disabled={codeState === 'busy' || !code.trim()}
|
|
578
|
+
>
|
|
579
|
+
{isAccess ? 'ENTER' : 'APPLY'}
|
|
580
|
+
</Button>
|
|
581
|
+
{promoMeta?.promoCodeMessage ? (
|
|
582
|
+
promoMeta.promoCodeValid === false ? (
|
|
583
|
+
<ErrorText>{promoMeta.promoCodeMessage}</ErrorText>
|
|
584
|
+
) : (
|
|
585
|
+
<MutedText>{promoMeta.promoCodeMessage}</MutedText>
|
|
586
|
+
)
|
|
587
|
+
) : null}
|
|
588
|
+
</>
|
|
589
|
+
)}
|
|
590
|
+
</View>
|
|
591
|
+
) : null}
|
|
534
592
|
|
|
535
|
-
|
|
593
|
+
{bundleStarting && (directPurchasable.length > 0 || hasSeatMapTickets) ? (
|
|
594
|
+
<MutedText>{createTranslator()(bundleWait ?? 'tickets.waitBundle')}</MutedText>
|
|
595
|
+
) : null}
|
|
596
|
+
{/* Direct-purchase CTA — hidden on a fully-seated event (nothing to buy by
|
|
536
597
|
quantity; the seat map is the only path). */}
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
:
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
598
|
+
{directPurchasable.length > 0 ? (
|
|
599
|
+
renderContinue ? (
|
|
600
|
+
renderContinue({
|
|
601
|
+
onPress: prime,
|
|
602
|
+
loading: checkout.isBusy,
|
|
603
|
+
disabled:
|
|
604
|
+
checkout.orderFixed ||
|
|
605
|
+
checkout.isBusy ||
|
|
606
|
+
bundleStarting ||
|
|
607
|
+
selected.length === 0,
|
|
608
|
+
title: continueTitle,
|
|
609
|
+
})
|
|
610
|
+
) : (
|
|
611
|
+
<Button
|
|
612
|
+
onPress={prime}
|
|
613
|
+
loading={checkout.isBusy}
|
|
614
|
+
disabled={checkout.isBusy || bundleStarting || selected.length === 0}
|
|
615
|
+
>
|
|
616
|
+
{checkout.isBusy
|
|
617
|
+
? 'Reserving…'
|
|
618
|
+
: selectionIsTable
|
|
619
|
+
? 'Reserve tables'
|
|
620
|
+
: buttonLabel}
|
|
621
|
+
</Button>
|
|
622
|
+
)
|
|
623
|
+
) : null}
|
|
624
|
+
|
|
625
|
+
{/* Seated events: hand off to the seat map instead of quantity selection. */}
|
|
626
|
+
{hasSeatMapTickets ? (
|
|
627
|
+
<Button
|
|
628
|
+
variant="ghost"
|
|
629
|
+
disabled={bundleStarting}
|
|
630
|
+
onPress={() => onSelectSeats?.()}
|
|
631
|
+
>
|
|
632
|
+
{seatCtaLabel}
|
|
633
|
+
</Button>
|
|
634
|
+
) : null}
|
|
635
|
+
|
|
636
|
+
{error || checkout.error ? (
|
|
637
|
+
<ErrorText>{error || checkout.error}</ErrorText>
|
|
638
|
+
) : null}
|
|
639
|
+
</Container>
|
|
640
|
+
</FrozenFields>
|
|
560
641
|
)
|
|
561
642
|
}
|
|
562
643
|
|