tf-checkout-react-native 2.0.0-beta.18 → 2.0.0-beta.21
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 +33 -14
- package/dist/index.d.mts +49 -2
- package/dist/index.d.ts +49 -2
- package/dist/index.js +660 -369
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +569 -271
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/sections/Tickets.tsx +104 -53
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tf-checkout-react-native",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.21",
|
|
4
4
|
"description": "TheTicketFairy ReactNative Checkout library",
|
|
5
5
|
"homepage": "https://github.com/theticketfairy/tf-shared-libraries#readme",
|
|
6
6
|
"author": "The Ticket Fairy",
|
package/src/sections/Tickets.tsx
CHANGED
|
@@ -23,6 +23,7 @@ import { Pressable, StyleSheet, Text, View } from 'react-native'
|
|
|
23
23
|
import {
|
|
24
24
|
ConfirmerBusyError,
|
|
25
25
|
changeTicketQuantity,
|
|
26
|
+
ticketPriceContext,
|
|
26
27
|
errorMessage,
|
|
27
28
|
hooks,
|
|
28
29
|
ticketQuantityRange,
|
|
@@ -116,7 +117,20 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
116
117
|
const checkout = useCheckoutContext()
|
|
117
118
|
const theme = useTheme()
|
|
118
119
|
const s = useTicketStyles(theme)
|
|
119
|
-
const
|
|
120
|
+
const catalog = hooks.useTicketCatalog(eventId, timeSlotDates)
|
|
121
|
+
const {
|
|
122
|
+
ticketsQuery,
|
|
123
|
+
eventInfo,
|
|
124
|
+
slotQuery,
|
|
125
|
+
isTimeSlot,
|
|
126
|
+
dates,
|
|
127
|
+
slotDate,
|
|
128
|
+
setSlotDate,
|
|
129
|
+
qty,
|
|
130
|
+
setQty,
|
|
131
|
+
refetchCatalog,
|
|
132
|
+
current: catalogCurrent,
|
|
133
|
+
} = catalog
|
|
120
134
|
const waitingBuyer = hooks.useAuthSession()
|
|
121
135
|
const cachedProfile = hooks.useProfile({ enabled: false }).data
|
|
122
136
|
const waitingPrefill = waitingListProfilePrefill(
|
|
@@ -132,42 +146,17 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
132
146
|
const bundleStarting = bundleWait !== null
|
|
133
147
|
const promoMeta = ticketsQuery.data?.promo
|
|
134
148
|
|
|
135
|
-
const
|
|
136
|
-
const [
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
)
|
|
145
|
-
useEffect(() => {
|
|
146
|
-
if (isTimeSlot && !slotDate && dates.length) setSlotDate(dates[0])
|
|
147
|
-
}, [isTimeSlot, slotDate, dates])
|
|
148
|
-
const slotQuery = hooks.useTimeSlotTickets(eventId, isTimeSlot ? slotDate : undefined)
|
|
149
|
-
const refetchCatalog = () =>
|
|
150
|
-
Promise.all([
|
|
151
|
-
ticketsQuery.refetch(),
|
|
152
|
-
...(isTimeSlot && slotDate ? [slotQuery.refetch()] : []),
|
|
153
|
-
])
|
|
154
|
-
|
|
155
|
-
const tickets = useMemo(
|
|
156
|
-
() => (isTimeSlot ? (slotQuery.data ?? []) : (ticketsQuery.data?.tickets ?? [])),
|
|
157
|
-
[isTimeSlot, slotQuery.data, ticketsQuery.data?.tickets]
|
|
149
|
+
const tickets = catalog.tickets
|
|
150
|
+
const [error, updateError] = hooks.useScopedState(catalog.scope, '')
|
|
151
|
+
const setError = (message: string) => {
|
|
152
|
+
if (catalogCurrent()) updateError(message)
|
|
153
|
+
}
|
|
154
|
+
const [code, setCode] = hooks.useScopedState(catalog.scope, '')
|
|
155
|
+
const [codeState, setCodeState] = hooks.useScopedState<'idle' | 'busy'>(
|
|
156
|
+
catalog.scope,
|
|
157
|
+
'idle'
|
|
158
158
|
)
|
|
159
|
-
const [
|
|
160
|
-
const [code, setCode] = useState('')
|
|
161
|
-
const [codeState, setCodeState] = useState<'idle' | 'busy'>('idle')
|
|
162
|
-
const [promoOpen, setPromoOpen] = useState(false)
|
|
163
|
-
// Another event: its tickets, date and code start fresh.
|
|
164
|
-
hooks.useOnEventChange(eventId, () => {
|
|
165
|
-
setQty({})
|
|
166
|
-
setSlotDate('')
|
|
167
|
-
setCode('')
|
|
168
|
-
setPromoOpen(false)
|
|
169
|
-
setError('')
|
|
170
|
-
})
|
|
159
|
+
const [promoOpen, setPromoOpen] = hooks.useScopedState(catalog.scope, false)
|
|
171
160
|
const [visibleDescription, setVisibleDescription] = useState<string | null>(null)
|
|
172
161
|
const attributionEvent = useRef<string | null>(null)
|
|
173
162
|
|
|
@@ -175,7 +164,12 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
175
164
|
// per event. Explicit props win. URL reading (web-only) is intentionally
|
|
176
165
|
// skipped on native — `readAttributionFromUrl` is accepted for prop parity.
|
|
177
166
|
useEffect(() => {
|
|
178
|
-
if (
|
|
167
|
+
if (
|
|
168
|
+
!catalogCurrent() ||
|
|
169
|
+
!catalog.attributionReady ||
|
|
170
|
+
attributionEvent.current === eventId
|
|
171
|
+
)
|
|
172
|
+
return
|
|
179
173
|
attributionEvent.current = eventId
|
|
180
174
|
|
|
181
175
|
const promo = initialPromoCode?.trim() ?? ''
|
|
@@ -192,13 +186,27 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
192
186
|
.applyPromoCode(eventId, promo)
|
|
193
187
|
.catch(() => undefined)
|
|
194
188
|
.finally(() => {
|
|
195
|
-
|
|
189
|
+
if (!catalogCurrent()) return
|
|
190
|
+
return refetchCatalog()
|
|
191
|
+
.catch((e) => {
|
|
192
|
+
if (catalogCurrent()) setError(errorMessage(e))
|
|
193
|
+
})
|
|
194
|
+
.finally(() => {
|
|
195
|
+
if (catalogCurrent()) setCodeState('idle')
|
|
196
|
+
})
|
|
196
197
|
})
|
|
197
198
|
}
|
|
198
199
|
// Checkout actions and query functions are stable; this effect is scoped to
|
|
199
200
|
// one attribution pass per event.
|
|
200
201
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
201
|
-
}, [
|
|
202
|
+
}, [
|
|
203
|
+
eventId,
|
|
204
|
+
initialPromoCode,
|
|
205
|
+
referrer,
|
|
206
|
+
readAttributionFromUrl,
|
|
207
|
+
catalog.instance,
|
|
208
|
+
catalog.attributionReady,
|
|
209
|
+
])
|
|
202
210
|
|
|
203
211
|
// Seed the currency state once so a multi-currency event can offer a switcher
|
|
204
212
|
// on the ticket screen (before the cart is primed). No-op for single-currency.
|
|
@@ -211,9 +219,11 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
211
219
|
checkout
|
|
212
220
|
.syncCurrency(eventId, nextCode)
|
|
213
221
|
.then((res) => {
|
|
214
|
-
if (res.requiresCatalogRefresh) {
|
|
222
|
+
if (catalogCurrent() && res.requiresCatalogRefresh) {
|
|
215
223
|
setQty({})
|
|
216
|
-
refetchCatalog()
|
|
224
|
+
refetchCatalog().catch((e) => {
|
|
225
|
+
if (catalogCurrent()) setError(errorMessage(e))
|
|
226
|
+
})
|
|
217
227
|
}
|
|
218
228
|
})
|
|
219
229
|
.catch(() => undefined)
|
|
@@ -223,8 +233,9 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
223
233
|
const originals = useRef<Record<string, number>>({})
|
|
224
234
|
useEffect(() => {
|
|
225
235
|
for (const t of tickets)
|
|
226
|
-
if (originals.current[t
|
|
227
|
-
|
|
236
|
+
if (originals.current[ticketPriceContext(catalog.scope, t)] == null)
|
|
237
|
+
originals.current[ticketPriceContext(catalog.scope, t)] = t.price
|
|
238
|
+
}, [tickets, catalog.scope])
|
|
228
239
|
|
|
229
240
|
const selected = tickets.filter((t) => (qty[t.id] ?? 0) > 0)
|
|
230
241
|
// The current selection's kind — drives the CTA label and the mutual-exclusion
|
|
@@ -259,7 +270,9 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
259
270
|
: undefined
|
|
260
271
|
|
|
261
272
|
const prime = () => {
|
|
273
|
+
if (!catalogCurrent() || catalog.busy || codeState === 'busy' || catalog.error) return
|
|
262
274
|
setError('')
|
|
275
|
+
const continuationCurrent = catalog.capturePrimeContinuation()
|
|
263
276
|
const selections: TicketSelection[] = selected.map((t) => ({
|
|
264
277
|
id: t.id,
|
|
265
278
|
name: t.name,
|
|
@@ -272,7 +285,9 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
272
285
|
// payment SDK nothing is primed, and the error names that flow.
|
|
273
286
|
checkout
|
|
274
287
|
.setTickets(eventId, selections)
|
|
275
|
-
.then(() =>
|
|
288
|
+
.then(() => {
|
|
289
|
+
if (continuationCurrent()) onContinue?.()
|
|
290
|
+
})
|
|
276
291
|
.catch((e) =>
|
|
277
292
|
setError(
|
|
278
293
|
e instanceof ConfirmerBusyError
|
|
@@ -283,25 +298,40 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
283
298
|
}
|
|
284
299
|
|
|
285
300
|
const applyCode = () => {
|
|
286
|
-
if (
|
|
301
|
+
if (
|
|
302
|
+
!catalogCurrent() ||
|
|
303
|
+
!catalog.attributionReady ||
|
|
304
|
+
catalog.busy ||
|
|
305
|
+
codeState === 'busy' ||
|
|
306
|
+
!code.trim()
|
|
307
|
+
)
|
|
308
|
+
return
|
|
309
|
+
setError('')
|
|
287
310
|
setCodeState('busy')
|
|
288
311
|
checkout
|
|
289
312
|
.applyPromoCode(eventId, code.trim())
|
|
290
313
|
.catch(() => undefined)
|
|
291
314
|
.finally(() => {
|
|
292
|
-
|
|
315
|
+
if (!catalogCurrent()) return
|
|
316
|
+
return refetchCatalog()
|
|
317
|
+
.catch((e) => {
|
|
318
|
+
if (catalogCurrent()) setError(errorMessage(e))
|
|
319
|
+
})
|
|
320
|
+
.finally(() => {
|
|
321
|
+
if (catalogCurrent()) setCodeState('idle')
|
|
322
|
+
})
|
|
293
323
|
})
|
|
294
324
|
}
|
|
295
325
|
|
|
296
326
|
const isAccess = promoMeta?.isAccessCodeEnabled
|
|
297
327
|
const showCodeInput = promoMeta?.isPromotionsEnabled || isAccess
|
|
298
|
-
const listQuery =
|
|
328
|
+
const listQuery = catalog
|
|
299
329
|
const currency = checkout.currency
|
|
300
330
|
|
|
301
331
|
const renderRow = (t: TicketType) => {
|
|
302
332
|
const notStarted = !t.salesStarted
|
|
303
333
|
const unavailable = t.soldOut || t.salesEnded || notStarted
|
|
304
|
-
const original = originals.current[t
|
|
334
|
+
const original = originals.current[ticketPriceContext(catalog.scope, t)]
|
|
305
335
|
const discounted = original != null && original > t.price
|
|
306
336
|
const isFree = Number(t.price) === 0
|
|
307
337
|
const sym = t.priceSymbol || t.currency
|
|
@@ -416,7 +446,7 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
416
446
|
// Time-slot layout groups the date's tickets under their time heading.
|
|
417
447
|
const timeGroups: { time: string; items: TicketType[] }[] = []
|
|
418
448
|
if (isTimeSlot)
|
|
419
|
-
for (const t of
|
|
449
|
+
for (const t of catalog.datedTickets) {
|
|
420
450
|
const time = t.timeSlot?.time ?? ''
|
|
421
451
|
const g = timeGroups.find((x) => x.time === time)
|
|
422
452
|
if (g) g.items.push(t)
|
|
@@ -452,7 +482,6 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
452
482
|
value={slotDate}
|
|
453
483
|
onChange={(d) => {
|
|
454
484
|
setSlotDate(d)
|
|
455
|
-
setQty({})
|
|
456
485
|
}}
|
|
457
486
|
options={dates.map((d) => ({ label: d, value: d }))}
|
|
458
487
|
/>
|
|
@@ -473,7 +502,7 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
473
502
|
</View>
|
|
474
503
|
) : null}
|
|
475
504
|
|
|
476
|
-
{isTimeSlot && !slotQuery.isLoading &&
|
|
505
|
+
{isTimeSlot && !slotQuery.isLoading && !slotQuery.data?.length && slotDate ? (
|
|
477
506
|
<MutedText>No sessions available on this date.</MutedText>
|
|
478
507
|
) : null}
|
|
479
508
|
|
|
@@ -486,6 +515,13 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
486
515
|
))
|
|
487
516
|
: tickets.map(renderRow)}
|
|
488
517
|
|
|
518
|
+
{isTimeSlot && catalog.regularTickets.length > 0 ? (
|
|
519
|
+
<>
|
|
520
|
+
<Text style={s.slotTime}>{createTranslator()('tickets.getTickets')}</Text>
|
|
521
|
+
{catalog.regularTickets.map(renderRow)}
|
|
522
|
+
</>
|
|
523
|
+
) : null}
|
|
524
|
+
|
|
489
525
|
{/* Promo / access code */}
|
|
490
526
|
{showCodeInput ? (
|
|
491
527
|
<View style={s.promo}>
|
|
@@ -511,7 +547,12 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
511
547
|
variant="ghost"
|
|
512
548
|
onPress={applyCode}
|
|
513
549
|
loading={codeState === 'busy'}
|
|
514
|
-
disabled={
|
|
550
|
+
disabled={
|
|
551
|
+
!catalog.attributionReady ||
|
|
552
|
+
catalog.busy ||
|
|
553
|
+
codeState === 'busy' ||
|
|
554
|
+
!code.trim()
|
|
555
|
+
}
|
|
515
556
|
>
|
|
516
557
|
{isAccess ? 'ENTER' : 'APPLY'}
|
|
517
558
|
</Button>
|
|
@@ -541,6 +582,9 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
541
582
|
checkout.orderFixed ||
|
|
542
583
|
checkout.isBusy ||
|
|
543
584
|
bundleStarting ||
|
|
585
|
+
catalog.busy ||
|
|
586
|
+
codeState === 'busy' ||
|
|
587
|
+
!!catalog.error ||
|
|
544
588
|
selected.length === 0,
|
|
545
589
|
title: continueTitle,
|
|
546
590
|
})
|
|
@@ -548,7 +592,14 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
548
592
|
<Button
|
|
549
593
|
onPress={prime}
|
|
550
594
|
loading={checkout.isBusy}
|
|
551
|
-
disabled={
|
|
595
|
+
disabled={
|
|
596
|
+
checkout.isBusy ||
|
|
597
|
+
bundleStarting ||
|
|
598
|
+
catalog.busy ||
|
|
599
|
+
codeState === 'busy' ||
|
|
600
|
+
!!catalog.error ||
|
|
601
|
+
selected.length === 0
|
|
602
|
+
}
|
|
552
603
|
>
|
|
553
604
|
{checkout.isBusy
|
|
554
605
|
? 'Reserving…'
|