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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tf-checkout-react-native",
3
- "version": "2.0.0-beta.18",
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",
@@ -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 ticketsQuery = hooks.useTicketsWithMeta(eventId)
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 [qty, setQty] = useState<Record<string, number>>({})
136
- const [slotDate, setSlotDate] = useState('')
137
-
138
- // Time-slot / calendar events: pick a date, then buy from that session's slots.
139
- const eventInfo = hooks.useEvent(eventId).data
140
- const isTimeSlot = Boolean(timeSlotDates?.length || eventInfo?.isTimeSlotEvent)
141
- const dates = useMemo(
142
- () => timeSlotDates ?? eventInfo?.availableDates ?? [],
143
- [timeSlotDates, eventInfo?.availableDates]
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 [error, setError] = useState('')
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 (attributionEvent.current === eventId) return
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
- refetchCatalog().finally(() => setCodeState('idle'))
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
- }, [eventId, initialPromoCode, referrer, readAttributionFromUrl])
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.id] == null) originals.current[t.id] = t.price
227
- }, [tickets])
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(() => onContinue?.())
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 (!code.trim()) return
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
- refetchCatalog().finally(() => setCodeState('idle'))
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 = isTimeSlot ? slotQuery : ticketsQuery
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.id]
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 tickets) {
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 && tickets.length === 0 && slotDate ? (
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={codeState === 'busy' || !code.trim()}
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={checkout.isBusy || bundleStarting || selected.length === 0}
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…'