tf-checkout-react-native 2.0.0-beta.17 → 2.0.0-beta.19
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 +10 -3
- package/dist/index.d.mts +118 -9
- package/dist/index.d.ts +118 -9
- package/dist/index.js +2877 -2169
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2652 -1937
- package/dist/index.mjs.map +1 -1
- package/package.json +19 -1
- package/src/sections/Accommodation.tsx +3 -2
- package/src/sections/AddOns.tsx +4 -3
- package/src/sections/Bundles.tsx +4 -3
- package/src/sections/Payment.tsx +19 -8
- package/src/sections/Tickets.tsx +117 -141
- package/src/sections/WaiverSigning.tsx +2 -2
- package/src/ui/RichText.tsx +187 -0
- package/src/ui/WaitingListForm.tsx +139 -0
- package/src/ui/index.ts +1 -0
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.19",
|
|
4
4
|
"description": "TheTicketFairy ReactNative Checkout library",
|
|
5
5
|
"homepage": "https://github.com/theticketfairy/tf-shared-libraries#readme",
|
|
6
6
|
"author": "The Ticket Fairy",
|
|
@@ -103,7 +103,9 @@
|
|
|
103
103
|
"react-dom": "19.1.0",
|
|
104
104
|
"react-native": "0.81.4",
|
|
105
105
|
"react-native-mmkv": "^3.3.1",
|
|
106
|
+
"react-native-render-html": "6.3.4",
|
|
106
107
|
"react-native-webview": "13.15.0",
|
|
108
|
+
"react-test-renderer": "19.1.0",
|
|
107
109
|
"release-it": "^19.0.4",
|
|
108
110
|
"tf-checkout-shared": "workspace:*",
|
|
109
111
|
"tf-seat-map-view": "9.4.7-beta.33",
|
|
@@ -113,6 +115,21 @@
|
|
|
113
115
|
},
|
|
114
116
|
"jest": {
|
|
115
117
|
"preset": "react-native",
|
|
118
|
+
"transform": {
|
|
119
|
+
"^.+\\.(js|ts|tsx)$": [
|
|
120
|
+
"babel-jest",
|
|
121
|
+
{
|
|
122
|
+
"babelrc": false,
|
|
123
|
+
"configFile": false,
|
|
124
|
+
"presets": [
|
|
125
|
+
"module:@react-native/babel-preset"
|
|
126
|
+
]
|
|
127
|
+
}
|
|
128
|
+
]
|
|
129
|
+
},
|
|
130
|
+
"transformIgnorePatterns": [
|
|
131
|
+
"node_modules/(?!((jest-)?react-native|@react-native(-community)?|\\.pnpm)/)"
|
|
132
|
+
],
|
|
116
133
|
"modulePathIgnorePatterns": [
|
|
117
134
|
"<rootDir>/example/node_modules",
|
|
118
135
|
"<rootDir>/dist/"
|
|
@@ -160,6 +177,7 @@
|
|
|
160
177
|
"react": ">=19.1",
|
|
161
178
|
"react-native": ">=0.81",
|
|
162
179
|
"react-native-mmkv": "^3.0.0",
|
|
180
|
+
"react-native-render-html": ">=6.3.4 <7",
|
|
163
181
|
"react-native-webview": ">=13.15.0"
|
|
164
182
|
},
|
|
165
183
|
"//dependencies": "tf-checkout-shared is INLINED into this bundle (tsup noExternal), so its own runtime deps are ours too \u2014 axios, entities and htmlparser2 must stay declared here or tsup bundles them (axios drags Node's stream into a browser build) / the consumer never installs them.",
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import { StyleSheet, Text, View } from 'react-native'
|
|
3
|
-
import { hooks,
|
|
3
|
+
import { hooks, accommodationBookingDeadline } from 'tf-checkout-shared'
|
|
4
4
|
import { useCheckoutContext, useTheme } from '../ui/CheckoutProvider'
|
|
5
5
|
import { Field, Input } from '../ui/Field'
|
|
6
6
|
import { QuantitySelect } from '../ui/QuantitySelect'
|
|
7
|
+
import { RichText } from '../ui/RichText'
|
|
7
8
|
import { Button, Card, ErrorText, Loader, MutedText, SectionHeader } from '../ui/primitives'
|
|
8
9
|
|
|
9
10
|
/** Native controls over the shared stay draft, quote and reservation lifecycle. */
|
|
@@ -26,7 +27,7 @@ export const Accommodation = ({ eventId }: { eventId: string }) => {
|
|
|
26
27
|
return (
|
|
27
28
|
<View key={id} style={styles.room}>
|
|
28
29
|
<Text style={[text, styles.title]}>{room.property_name} · {room.name}</Text>
|
|
29
|
-
{!!room.description && <
|
|
30
|
+
{!!room.description && <RichText html={room.description} />}
|
|
30
31
|
{accommodationBookingDeadline(room) && <MutedText>{accommodationBookingDeadline(room)}</MutedText>}
|
|
31
32
|
<MutedText>{room.sold_out ? 'Sold out' : `${room.currency} ${room.min_price.toFixed(2)} per night${room.max_price > room.min_price ? ' and up' : ''}`}</MutedText>
|
|
32
33
|
<Field label="Rooms">
|
package/src/sections/AddOns.tsx
CHANGED
|
@@ -15,7 +15,6 @@ import {
|
|
|
15
15
|
addOnMaxQuantity,
|
|
16
16
|
errorMessage,
|
|
17
17
|
hooks,
|
|
18
|
-
htmlToPlainText,
|
|
19
18
|
type EventAddon,
|
|
20
19
|
} from 'tf-checkout-shared'
|
|
21
20
|
|
|
@@ -26,6 +25,7 @@ import { useCheckoutContext, useTheme } from '../ui/CheckoutProvider'
|
|
|
26
25
|
// `CustomFieldGroupFields` API: { group, values, onChange(name, value) }.
|
|
27
26
|
import { CustomFieldGroupFields } from '../ui/CustomFields'
|
|
28
27
|
import { QuantitySelect } from '../ui/QuantitySelect'
|
|
28
|
+
import { RichText } from '../ui/RichText'
|
|
29
29
|
import type { ResolvedTheme } from '../ui/theme'
|
|
30
30
|
|
|
31
31
|
const money = (currency: string, n: number): string =>
|
|
@@ -135,7 +135,6 @@ export const AddOns: FC<AddOnsProps> = ({
|
|
|
135
135
|
{g.items.map((a) => {
|
|
136
136
|
const n = qty[a.id] ?? 0
|
|
137
137
|
const group = fieldGroups[a.id]
|
|
138
|
-
const desc = a.description ? htmlToPlainText(a.description) : ''
|
|
139
138
|
return (
|
|
140
139
|
<View key={a.id} style={s.item}>
|
|
141
140
|
<View style={s.row}>
|
|
@@ -150,7 +149,9 @@ export const AddOns: FC<AddOnsProps> = ({
|
|
|
150
149
|
<Text style={s.priceNote}> incl. fees</Text>
|
|
151
150
|
) : null}
|
|
152
151
|
</Text>
|
|
153
|
-
{
|
|
152
|
+
{a.description ? (
|
|
153
|
+
<RichText html={a.description} textStyle={s.desc} />
|
|
154
|
+
) : null}
|
|
154
155
|
</View>
|
|
155
156
|
<View style={s.quantity}>
|
|
156
157
|
{soldOut(a) ? (
|
package/src/sections/Bundles.tsx
CHANGED
|
@@ -17,7 +17,6 @@ import { StyleSheet, Text, View } from 'react-native'
|
|
|
17
17
|
import {
|
|
18
18
|
bundleAvailabilityCopy,
|
|
19
19
|
bundleContentsLine,
|
|
20
|
-
htmlToPlainText,
|
|
21
20
|
bundleQuantityRange,
|
|
22
21
|
createTranslator,
|
|
23
22
|
hooks,
|
|
@@ -33,6 +32,7 @@ import { useCheckoutContext, useTheme } from '../ui/CheckoutProvider'
|
|
|
33
32
|
import { Field, Input } from '../ui/Field'
|
|
34
33
|
import { Button, Card, ErrorText, Loader } from '../ui/primitives'
|
|
35
34
|
import { QuantitySelect } from '../ui/QuantitySelect'
|
|
35
|
+
import { RichText } from '../ui/RichText'
|
|
36
36
|
import type { ResolvedTheme } from '../ui/theme'
|
|
37
37
|
|
|
38
38
|
const money = (currency: string, amount: number) => `${currency} ${amount.toFixed(2)}`
|
|
@@ -361,7 +361,6 @@ export const Bundles: FC<BundlesProps> = ({
|
|
|
361
361
|
const range = bundleQuantityRange(bundle)
|
|
362
362
|
const stock = bundleAvailabilityCopy(bundle)
|
|
363
363
|
const contents = bundleContentsLine(bundle)
|
|
364
|
-
const desc = htmlToPlainText(bundle.description ?? '')
|
|
365
364
|
return (
|
|
366
365
|
<View key={bundle.id} style={[s.item, { borderTopColor: theme.border }]}>
|
|
367
366
|
<View style={s.info}>
|
|
@@ -377,7 +376,9 @@ export const Bundles: FC<BundlesProps> = ({
|
|
|
377
376
|
})}
|
|
378
377
|
</Text>
|
|
379
378
|
)}
|
|
380
|
-
{
|
|
379
|
+
{bundle.description ? (
|
|
380
|
+
<RichText html={bundle.description} textStyle={s.muted} />
|
|
381
|
+
) : null}
|
|
381
382
|
{stock && stock.key !== 'bundles.soldOut' ? (
|
|
382
383
|
<Text style={[s.muted, s.stock]}>{t(stock.key, stock.values)}</Text>
|
|
383
384
|
) : null}
|
package/src/sections/Payment.tsx
CHANGED
|
@@ -15,7 +15,7 @@ import { Accommodation } from './Accommodation'
|
|
|
15
15
|
* payment plans. Existing explicit wallet rows keep their own policy gates.
|
|
16
16
|
* - CheckoutProvider owns provider-page windows for Xendit, redirect methods,
|
|
17
17
|
* Razorpay and PayPal; imperative hosts provide that port themselves.
|
|
18
|
-
* - Conditions
|
|
18
|
+
* - Conditions use the shared native rich renderer, preserving links/formatting.
|
|
19
19
|
*/
|
|
20
20
|
import React, { useEffect, useMemo, useState, type FC } from 'react'
|
|
21
21
|
import { Platform, View } from 'react-native'
|
|
@@ -30,7 +30,6 @@ import {
|
|
|
30
30
|
cartTicketCount,
|
|
31
31
|
getConfig,
|
|
32
32
|
errorMessage,
|
|
33
|
-
htmlToPlainText,
|
|
34
33
|
missingCheckoutDetails,
|
|
35
34
|
paymentAcknowledgementsMet,
|
|
36
35
|
paymentPlanTerms,
|
|
@@ -52,8 +51,13 @@ import {
|
|
|
52
51
|
import { CardInput, claimCardField } from '../CardInput'
|
|
53
52
|
import { nativePaymentSheetAvailable } from '../paymentSheet'
|
|
54
53
|
import { rnConfirmer } from '../instance'
|
|
55
|
-
import {
|
|
54
|
+
import {
|
|
55
|
+
useCheckoutContext,
|
|
56
|
+
useCheckoutValidation,
|
|
57
|
+
useTheme,
|
|
58
|
+
} from '../ui/CheckoutProvider'
|
|
56
59
|
import { Checkbox } from '../ui/Field'
|
|
60
|
+
import { RichText } from '../ui/RichText'
|
|
57
61
|
import { PaymentMethods, type PaymentMethodOption } from '../ui/PaymentMethods'
|
|
58
62
|
import {
|
|
59
63
|
BodyText,
|
|
@@ -85,10 +89,6 @@ const REDIRECT_LABELS: Record<string, string> = {
|
|
|
85
89
|
const redirectLabel = (t: string) =>
|
|
86
90
|
REDIRECT_LABELS[t] ?? t.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase())
|
|
87
91
|
|
|
88
|
-
/** Condition copy as one line of plain text: native has no DOM to render
|
|
89
|
-
* markup into, and the copy is a checkbox label. */
|
|
90
|
-
const conditionText = (html: string) => htmlToPlainText(html).replace(/\s*\n\s*/g, ' ')
|
|
91
|
-
|
|
92
92
|
export interface PaymentProps {
|
|
93
93
|
onComplete?: (order: PlaceOrderResult) => void
|
|
94
94
|
/** Optional event context (parity with web; native `OrderSummary` reads the
|
|
@@ -150,6 +150,7 @@ export const Payment: FC<PaymentProps> = ({
|
|
|
150
150
|
}) => {
|
|
151
151
|
const t = translate ?? createTranslator({ locale, messages })
|
|
152
152
|
const checkout = useCheckoutContext()
|
|
153
|
+
const theme = useTheme()
|
|
153
154
|
// This checkout's own card field: another checkout on the page has its own.
|
|
154
155
|
const cardOwner = hooks.useTicketCardOwner()
|
|
155
156
|
// A bundle purchase holds the payment SDK: Pay would do nothing now.
|
|
@@ -506,7 +507,17 @@ export const Payment: FC<PaymentProps> = ({
|
|
|
506
507
|
checked={Boolean(checkout.acceptedConditions?.[c.id])}
|
|
507
508
|
invalid={invalid}
|
|
508
509
|
onChange={(next) => checkout.acceptCondition(c.id, next)}
|
|
509
|
-
label={
|
|
510
|
+
label={
|
|
511
|
+
<View style={{ flex: 1, minWidth: 0, flexDirection: 'row', gap: 4 }}>
|
|
512
|
+
<View style={{ flex: 1, minWidth: 0 }}>
|
|
513
|
+
<RichText
|
|
514
|
+
html={c.content || c.name}
|
|
515
|
+
textStyle={{ color: theme.text }}
|
|
516
|
+
/>
|
|
517
|
+
</View>
|
|
518
|
+
{c.isRequired ? <BodyText>*</BodyText> : null}
|
|
519
|
+
</View>
|
|
520
|
+
}
|
|
510
521
|
/>
|
|
511
522
|
</FrozenFields>
|
|
512
523
|
{invalid ? (
|
package/src/sections/Tickets.tsx
CHANGED
|
@@ -25,11 +25,10 @@ import {
|
|
|
25
25
|
changeTicketQuantity,
|
|
26
26
|
errorMessage,
|
|
27
27
|
hooks,
|
|
28
|
-
htmlToPlainText,
|
|
29
28
|
ticketQuantityRange,
|
|
29
|
+
waitingListProfilePrefill,
|
|
30
30
|
type TicketSelection,
|
|
31
31
|
type TicketType,
|
|
32
|
-
type WaitingListEntry,
|
|
33
32
|
createTranslator,
|
|
34
33
|
} from 'tf-checkout-shared'
|
|
35
34
|
|
|
@@ -46,73 +45,13 @@ import { FrozenFields } from '../ui/Frozen'
|
|
|
46
45
|
import { useCheckoutContext, useTheme } from '../ui/CheckoutProvider'
|
|
47
46
|
import { Field, Input } from '../ui/Field'
|
|
48
47
|
import { QuantitySelect } from '../ui/QuantitySelect'
|
|
48
|
+
import { WaitingListForm } from '../ui/WaitingListForm'
|
|
49
|
+
import { RichText } from '../ui/RichText'
|
|
49
50
|
import { Select } from '../ui/Select'
|
|
50
51
|
import type { ResolvedTheme } from '../ui/theme'
|
|
51
52
|
|
|
52
53
|
const money = (sym: string, n: number): string => `${sym} ${Number(n).toFixed(2)}`
|
|
53
54
|
|
|
54
|
-
/** Inline waiting-list sign-up for a sold-out type. */
|
|
55
|
-
const WaitingListForm: FC<{
|
|
56
|
-
ticket: TicketType
|
|
57
|
-
onJoin: (entry: WaitingListEntry) => Promise<void>
|
|
58
|
-
}> = ({ ticket, onJoin }) => {
|
|
59
|
-
const [form, setForm] = useState({ firstName: '', lastName: '', email: '' })
|
|
60
|
-
const [state, setState] = useState<'idle' | 'busy' | 'done' | 'error'>('idle')
|
|
61
|
-
const [error, setError] = useState('')
|
|
62
|
-
const ok = Boolean(form.firstName.trim() && form.lastName.trim() && form.email.trim())
|
|
63
|
-
|
|
64
|
-
if (state === 'done')
|
|
65
|
-
return (
|
|
66
|
-
<MutedText>You’re on the waiting list — we’ll email you if a spot opens.</MutedText>
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
return (
|
|
70
|
-
<View style={staticStyles.wlForm}>
|
|
71
|
-
<Field>
|
|
72
|
-
<Input
|
|
73
|
-
placeholder="First name"
|
|
74
|
-
value={form.firstName}
|
|
75
|
-
onChangeText={(t) => setForm((f) => ({ ...f, firstName: t }))}
|
|
76
|
-
/>
|
|
77
|
-
</Field>
|
|
78
|
-
<Field>
|
|
79
|
-
<Input
|
|
80
|
-
placeholder="Last name"
|
|
81
|
-
value={form.lastName}
|
|
82
|
-
onChangeText={(t) => setForm((f) => ({ ...f, lastName: t }))}
|
|
83
|
-
/>
|
|
84
|
-
</Field>
|
|
85
|
-
<Field>
|
|
86
|
-
<Input
|
|
87
|
-
placeholder="Email"
|
|
88
|
-
value={form.email}
|
|
89
|
-
keyboardType="email-address"
|
|
90
|
-
autoCapitalize="none"
|
|
91
|
-
onChangeText={(t) => setForm((f) => ({ ...f, email: t }))}
|
|
92
|
-
/>
|
|
93
|
-
</Field>
|
|
94
|
-
<Button
|
|
95
|
-
variant="ghost"
|
|
96
|
-
loading={state === 'busy'}
|
|
97
|
-
disabled={!ok || state === 'busy'}
|
|
98
|
-
onPress={() => {
|
|
99
|
-
setState('busy')
|
|
100
|
-
setError('')
|
|
101
|
-
onJoin({ ...form, ticketTypeId: ticket.id })
|
|
102
|
-
.then(() => setState('done'))
|
|
103
|
-
.catch((e) => {
|
|
104
|
-
setState('error')
|
|
105
|
-
setError(errorMessage(e))
|
|
106
|
-
})
|
|
107
|
-
}}
|
|
108
|
-
>
|
|
109
|
-
Join the waiting list
|
|
110
|
-
</Button>
|
|
111
|
-
{error ? <ErrorText>{error}</ErrorText> : null}
|
|
112
|
-
</View>
|
|
113
|
-
)
|
|
114
|
-
}
|
|
115
|
-
|
|
116
55
|
/** Host-owned visuals over the shared catalog and reservation flow. */
|
|
117
56
|
export interface TicketRowRenderProps {
|
|
118
57
|
ticket: TicketType
|
|
@@ -177,7 +116,27 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
177
116
|
const checkout = useCheckoutContext()
|
|
178
117
|
const theme = useTheme()
|
|
179
118
|
const s = useTicketStyles(theme)
|
|
180
|
-
const
|
|
119
|
+
const catalog = hooks.useTicketCatalog(eventId, timeSlotDates)
|
|
120
|
+
const {
|
|
121
|
+
ticketsQuery,
|
|
122
|
+
eventInfo,
|
|
123
|
+
slotQuery,
|
|
124
|
+
isTimeSlot,
|
|
125
|
+
dates,
|
|
126
|
+
slotDate,
|
|
127
|
+
setSlotDate,
|
|
128
|
+
qty,
|
|
129
|
+
setQty,
|
|
130
|
+
refetchCatalog,
|
|
131
|
+
current: catalogCurrent,
|
|
132
|
+
} = catalog
|
|
133
|
+
const waitingBuyer = hooks.useAuthSession()
|
|
134
|
+
const cachedProfile = hooks.useProfile({ enabled: false }).data
|
|
135
|
+
const waitingPrefill = waitingListProfilePrefill(
|
|
136
|
+
cachedProfile,
|
|
137
|
+
waitingBuyer.buyerId,
|
|
138
|
+
waitingBuyer.sessionScope
|
|
139
|
+
)
|
|
181
140
|
// A bundle order is starting: wait, so the two flows never start at once.
|
|
182
141
|
// The checkout start lock is held by a bundle start or pay (shown as a
|
|
183
142
|
// wait), or by this checkout's own priming (shown as the busy button).
|
|
@@ -185,40 +144,18 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
185
144
|
const bundleWait = hooks.useTicketGatewayWait(checkout.store)
|
|
186
145
|
const bundleStarting = bundleWait !== null
|
|
187
146
|
const promoMeta = ticketsQuery.data?.promo
|
|
188
|
-
const showWaitingList = ticketsQuery.data?.showWaitingList ?? false
|
|
189
|
-
|
|
190
|
-
const [qty, setQty] = useState<Record<string, number>>({})
|
|
191
|
-
const [slotDate, setSlotDate] = useState('')
|
|
192
|
-
|
|
193
|
-
// Time-slot / calendar events: pick a date, then buy from that session's slots.
|
|
194
|
-
const eventInfo = hooks.useEvent(eventId).data
|
|
195
|
-
const isTimeSlot = Boolean(timeSlotDates?.length || eventInfo?.isTimeSlotEvent)
|
|
196
|
-
const dates = useMemo(
|
|
197
|
-
() => timeSlotDates ?? eventInfo?.availableDates ?? [],
|
|
198
|
-
[timeSlotDates, eventInfo?.availableDates]
|
|
199
|
-
)
|
|
200
|
-
useEffect(() => {
|
|
201
|
-
if (isTimeSlot && !slotDate && dates.length) setSlotDate(dates[0])
|
|
202
|
-
}, [isTimeSlot, slotDate, dates])
|
|
203
|
-
const slotQuery = hooks.useTimeSlotTickets(eventId, isTimeSlot ? slotDate : undefined)
|
|
204
147
|
|
|
205
|
-
const tickets =
|
|
206
|
-
|
|
207
|
-
|
|
148
|
+
const tickets = catalog.tickets
|
|
149
|
+
const [error, updateError] = hooks.useScopedState(catalog.scope, '')
|
|
150
|
+
const setError = (message: string) => {
|
|
151
|
+
if (catalogCurrent()) updateError(message)
|
|
152
|
+
}
|
|
153
|
+
const [code, setCode] = hooks.useScopedState(catalog.scope, '')
|
|
154
|
+
const [codeState, setCodeState] = hooks.useScopedState<'idle' | 'busy'>(
|
|
155
|
+
catalog.scope,
|
|
156
|
+
'idle'
|
|
208
157
|
)
|
|
209
|
-
const [
|
|
210
|
-
const [code, setCode] = useState('')
|
|
211
|
-
const [codeState, setCodeState] = useState<'idle' | 'busy'>('idle')
|
|
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
|
-
})
|
|
221
|
-
const [wlOpen, setWlOpen] = useState<Record<string, boolean>>({})
|
|
158
|
+
const [promoOpen, setPromoOpen] = hooks.useScopedState(catalog.scope, false)
|
|
222
159
|
const [visibleDescription, setVisibleDescription] = useState<string | null>(null)
|
|
223
160
|
const attributionEvent = useRef<string | null>(null)
|
|
224
161
|
|
|
@@ -226,7 +163,12 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
226
163
|
// per event. Explicit props win. URL reading (web-only) is intentionally
|
|
227
164
|
// skipped on native — `readAttributionFromUrl` is accepted for prop parity.
|
|
228
165
|
useEffect(() => {
|
|
229
|
-
if (
|
|
166
|
+
if (
|
|
167
|
+
!catalogCurrent() ||
|
|
168
|
+
!catalog.attributionReady ||
|
|
169
|
+
attributionEvent.current === eventId
|
|
170
|
+
)
|
|
171
|
+
return
|
|
230
172
|
attributionEvent.current = eventId
|
|
231
173
|
|
|
232
174
|
const promo = initialPromoCode?.trim() ?? ''
|
|
@@ -243,13 +185,27 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
243
185
|
.applyPromoCode(eventId, promo)
|
|
244
186
|
.catch(() => undefined)
|
|
245
187
|
.finally(() => {
|
|
246
|
-
|
|
188
|
+
if (!catalogCurrent()) return
|
|
189
|
+
return refetchCatalog()
|
|
190
|
+
.catch((e) => {
|
|
191
|
+
if (catalogCurrent()) setError(errorMessage(e))
|
|
192
|
+
})
|
|
193
|
+
.finally(() => {
|
|
194
|
+
if (catalogCurrent()) setCodeState('idle')
|
|
195
|
+
})
|
|
247
196
|
})
|
|
248
197
|
}
|
|
249
198
|
// Checkout actions and query functions are stable; this effect is scoped to
|
|
250
199
|
// one attribution pass per event.
|
|
251
200
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
252
|
-
}, [
|
|
201
|
+
}, [
|
|
202
|
+
eventId,
|
|
203
|
+
initialPromoCode,
|
|
204
|
+
referrer,
|
|
205
|
+
readAttributionFromUrl,
|
|
206
|
+
catalog.instance,
|
|
207
|
+
catalog.attributionReady,
|
|
208
|
+
])
|
|
253
209
|
|
|
254
210
|
// Seed the currency state once so a multi-currency event can offer a switcher
|
|
255
211
|
// on the ticket screen (before the cart is primed). No-op for single-currency.
|
|
@@ -262,9 +218,11 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
262
218
|
checkout
|
|
263
219
|
.syncCurrency(eventId, nextCode)
|
|
264
220
|
.then((res) => {
|
|
265
|
-
if (res.requiresCatalogRefresh) {
|
|
221
|
+
if (catalogCurrent() && res.requiresCatalogRefresh) {
|
|
266
222
|
setQty({})
|
|
267
|
-
|
|
223
|
+
refetchCatalog().catch((e) => {
|
|
224
|
+
if (catalogCurrent()) setError(errorMessage(e))
|
|
225
|
+
})
|
|
268
226
|
}
|
|
269
227
|
})
|
|
270
228
|
.catch(() => undefined)
|
|
@@ -310,7 +268,9 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
310
268
|
: undefined
|
|
311
269
|
|
|
312
270
|
const prime = () => {
|
|
271
|
+
if (!catalogCurrent() || catalog.busy || codeState === 'busy' || catalog.error) return
|
|
313
272
|
setError('')
|
|
273
|
+
const continuationCurrent = catalog.capturePrimeContinuation()
|
|
314
274
|
const selections: TicketSelection[] = selected.map((t) => ({
|
|
315
275
|
id: t.id,
|
|
316
276
|
name: t.name,
|
|
@@ -323,7 +283,9 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
323
283
|
// payment SDK nothing is primed, and the error names that flow.
|
|
324
284
|
checkout
|
|
325
285
|
.setTickets(eventId, selections)
|
|
326
|
-
.then(() =>
|
|
286
|
+
.then(() => {
|
|
287
|
+
if (continuationCurrent()) onContinue?.()
|
|
288
|
+
})
|
|
327
289
|
.catch((e) =>
|
|
328
290
|
setError(
|
|
329
291
|
e instanceof ConfirmerBusyError
|
|
@@ -334,19 +296,34 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
334
296
|
}
|
|
335
297
|
|
|
336
298
|
const applyCode = () => {
|
|
337
|
-
if (
|
|
299
|
+
if (
|
|
300
|
+
!catalogCurrent() ||
|
|
301
|
+
!catalog.attributionReady ||
|
|
302
|
+
catalog.busy ||
|
|
303
|
+
codeState === 'busy' ||
|
|
304
|
+
!code.trim()
|
|
305
|
+
)
|
|
306
|
+
return
|
|
307
|
+
setError('')
|
|
338
308
|
setCodeState('busy')
|
|
339
309
|
checkout
|
|
340
310
|
.applyPromoCode(eventId, code.trim())
|
|
341
311
|
.catch(() => undefined)
|
|
342
312
|
.finally(() => {
|
|
343
|
-
|
|
313
|
+
if (!catalogCurrent()) return
|
|
314
|
+
return refetchCatalog()
|
|
315
|
+
.catch((e) => {
|
|
316
|
+
if (catalogCurrent()) setError(errorMessage(e))
|
|
317
|
+
})
|
|
318
|
+
.finally(() => {
|
|
319
|
+
if (catalogCurrent()) setCodeState('idle')
|
|
320
|
+
})
|
|
344
321
|
})
|
|
345
322
|
}
|
|
346
323
|
|
|
347
324
|
const isAccess = promoMeta?.isAccessCodeEnabled
|
|
348
325
|
const showCodeInput = promoMeta?.isPromotionsEnabled || isAccess
|
|
349
|
-
const listQuery =
|
|
326
|
+
const listQuery = catalog
|
|
350
327
|
const currency = checkout.currency
|
|
351
328
|
|
|
352
329
|
const renderRow = (t: TicketType) => {
|
|
@@ -354,8 +331,6 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
354
331
|
const unavailable = t.soldOut || t.salesEnded || notStarted
|
|
355
332
|
const original = originals.current[t.id]
|
|
356
333
|
const discounted = original != null && original > t.price
|
|
357
|
-
const canWaitlist =
|
|
358
|
-
t.soldOut && (t.waitingListEnabled || showWaitingList) && !t.salesEnded
|
|
359
334
|
const isFree = Number(t.price) === 0
|
|
360
335
|
const sym = t.priceSymbol || t.currency
|
|
361
336
|
const range = ticketQuantityRange(t)
|
|
@@ -457,26 +432,10 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
457
432
|
)}
|
|
458
433
|
</View>
|
|
459
434
|
{t.descriptionRich && visibleDescription === t.id ? (
|
|
460
|
-
<
|
|
435
|
+
<RichText html={t.descriptionRich} textStyle={s.description} />
|
|
461
436
|
) : null}
|
|
462
437
|
</>
|
|
463
438
|
)}
|
|
464
|
-
{canWaitlist ? (
|
|
465
|
-
wlOpen[t.id] ? (
|
|
466
|
-
<WaitingListForm
|
|
467
|
-
ticket={t}
|
|
468
|
-
onJoin={(entry) => checkout.joinWaitingList(eventId, entry)}
|
|
469
|
-
/>
|
|
470
|
-
) : (
|
|
471
|
-
<Pressable
|
|
472
|
-
accessibilityRole="button"
|
|
473
|
-
style={s.textAction}
|
|
474
|
-
onPress={() => setWlOpen((w) => ({ ...w, [t.id]: true }))}
|
|
475
|
-
>
|
|
476
|
-
<Text style={s.wlToggle}>Join the waiting list</Text>
|
|
477
|
-
</Pressable>
|
|
478
|
-
)
|
|
479
|
-
) : null}
|
|
480
439
|
</View>
|
|
481
440
|
</Fragment>
|
|
482
441
|
)
|
|
@@ -485,7 +444,7 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
485
444
|
// Time-slot layout groups the date's tickets under their time heading.
|
|
486
445
|
const timeGroups: { time: string; items: TicketType[] }[] = []
|
|
487
446
|
if (isTimeSlot)
|
|
488
|
-
for (const t of
|
|
447
|
+
for (const t of catalog.datedTickets) {
|
|
489
448
|
const time = t.timeSlot?.time ?? ''
|
|
490
449
|
const g = timeGroups.find((x) => x.time === time)
|
|
491
450
|
if (g) g.items.push(t)
|
|
@@ -521,7 +480,6 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
521
480
|
value={slotDate}
|
|
522
481
|
onChange={(d) => {
|
|
523
482
|
setSlotDate(d)
|
|
524
|
-
setQty({})
|
|
525
483
|
}}
|
|
526
484
|
options={dates.map((d) => ({ label: d, value: d }))}
|
|
527
485
|
/>
|
|
@@ -542,7 +500,7 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
542
500
|
</View>
|
|
543
501
|
) : null}
|
|
544
502
|
|
|
545
|
-
{isTimeSlot && !slotQuery.isLoading &&
|
|
503
|
+
{isTimeSlot && !slotQuery.isLoading && !slotQuery.data?.length && slotDate ? (
|
|
546
504
|
<MutedText>No sessions available on this date.</MutedText>
|
|
547
505
|
) : null}
|
|
548
506
|
|
|
@@ -555,6 +513,13 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
555
513
|
))
|
|
556
514
|
: tickets.map(renderRow)}
|
|
557
515
|
|
|
516
|
+
{isTimeSlot && catalog.regularTickets.length > 0 ? (
|
|
517
|
+
<>
|
|
518
|
+
<Text style={s.slotTime}>{createTranslator()('tickets.getTickets')}</Text>
|
|
519
|
+
{catalog.regularTickets.map(renderRow)}
|
|
520
|
+
</>
|
|
521
|
+
) : null}
|
|
522
|
+
|
|
558
523
|
{/* Promo / access code */}
|
|
559
524
|
{showCodeInput ? (
|
|
560
525
|
<View style={s.promo}>
|
|
@@ -580,7 +545,12 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
580
545
|
variant="ghost"
|
|
581
546
|
onPress={applyCode}
|
|
582
547
|
loading={codeState === 'busy'}
|
|
583
|
-
disabled={
|
|
548
|
+
disabled={
|
|
549
|
+
!catalog.attributionReady ||
|
|
550
|
+
catalog.busy ||
|
|
551
|
+
codeState === 'busy' ||
|
|
552
|
+
!code.trim()
|
|
553
|
+
}
|
|
584
554
|
>
|
|
585
555
|
{isAccess ? 'ENTER' : 'APPLY'}
|
|
586
556
|
</Button>
|
|
@@ -610,6 +580,9 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
610
580
|
checkout.orderFixed ||
|
|
611
581
|
checkout.isBusy ||
|
|
612
582
|
bundleStarting ||
|
|
583
|
+
catalog.busy ||
|
|
584
|
+
codeState === 'busy' ||
|
|
585
|
+
!!catalog.error ||
|
|
613
586
|
selected.length === 0,
|
|
614
587
|
title: continueTitle,
|
|
615
588
|
})
|
|
@@ -617,7 +590,14 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
617
590
|
<Button
|
|
618
591
|
onPress={prime}
|
|
619
592
|
loading={checkout.isBusy}
|
|
620
|
-
disabled={
|
|
593
|
+
disabled={
|
|
594
|
+
checkout.isBusy ||
|
|
595
|
+
bundleStarting ||
|
|
596
|
+
catalog.busy ||
|
|
597
|
+
codeState === 'busy' ||
|
|
598
|
+
!!catalog.error ||
|
|
599
|
+
selected.length === 0
|
|
600
|
+
}
|
|
621
601
|
>
|
|
622
602
|
{checkout.isBusy
|
|
623
603
|
? 'Reserving…'
|
|
@@ -639,6 +619,13 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
639
619
|
</Button>
|
|
640
620
|
) : null}
|
|
641
621
|
|
|
622
|
+
<WaitingListForm
|
|
623
|
+
eventId={eventId}
|
|
624
|
+
definition={ticketsQuery.data?.waitingList}
|
|
625
|
+
prefill={waitingPrefill}
|
|
626
|
+
onJoin={(entry) => checkout.joinWaitingList(eventId, entry)}
|
|
627
|
+
/>
|
|
628
|
+
|
|
642
629
|
{error || checkout.error ? (
|
|
643
630
|
<ErrorText>{error || checkout.error}</ErrorText>
|
|
644
631
|
) : null}
|
|
@@ -647,11 +634,6 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
647
634
|
)
|
|
648
635
|
}
|
|
649
636
|
|
|
650
|
-
/** Theme-independent layout bits (used by the WaitingListForm subcomponent). */
|
|
651
|
-
const staticStyles = StyleSheet.create({
|
|
652
|
-
wlForm: { gap: 8 },
|
|
653
|
-
})
|
|
654
|
-
|
|
655
637
|
const useTicketStyles = (theme: ResolvedTheme) =>
|
|
656
638
|
useMemo(
|
|
657
639
|
() =>
|
|
@@ -775,12 +757,6 @@ const useTicketStyles = (theme: ResolvedTheme) =>
|
|
|
775
757
|
lineHeight: theme.fontSize * 1.5,
|
|
776
758
|
fontFamily: theme.fontFamily,
|
|
777
759
|
},
|
|
778
|
-
wlToggle: {
|
|
779
|
-
color: theme.accent,
|
|
780
|
-
fontSize: theme.fontSize - 1,
|
|
781
|
-
fontWeight: '600',
|
|
782
|
-
fontFamily: theme.fontFamily,
|
|
783
|
-
},
|
|
784
760
|
promo: { gap: 8 },
|
|
785
761
|
promoToggle: {
|
|
786
762
|
color: theme.accent,
|
|
@@ -20,7 +20,6 @@ import {
|
|
|
20
20
|
emptyWaiverDraft,
|
|
21
21
|
formatDateDigits,
|
|
22
22
|
hooks,
|
|
23
|
-
htmlToPlainText,
|
|
24
23
|
holderBackfilledFrom,
|
|
25
24
|
signatureMatchesHolder,
|
|
26
25
|
waiverTicketFormKey,
|
|
@@ -38,6 +37,7 @@ import {
|
|
|
38
37
|
|
|
39
38
|
import { useCheckoutContext, useTheme } from '../ui/CheckoutProvider'
|
|
40
39
|
import { SignaturePad } from '../ui/SignaturePad'
|
|
40
|
+
import { RichText } from '../ui/RichText'
|
|
41
41
|
import { Checkbox, Field, Input } from '../ui/Field'
|
|
42
42
|
import { Button, Card, ErrorText, SectionHeader } from '../ui/primitives'
|
|
43
43
|
import type { ResolvedTheme } from '../ui/theme'
|
|
@@ -116,7 +116,7 @@ export const WaiverSigning: FC<WaiverSigningProps> = ({
|
|
|
116
116
|
)}
|
|
117
117
|
</Text>
|
|
118
118
|
<ScrollView style={[s.textBox, { borderColor: theme.border }]} nestedScrollEnabled>
|
|
119
|
-
<
|
|
119
|
+
<RichText html={waiver.bodyHtml} textStyle={s.body} selectable />
|
|
120
120
|
</ScrollView>
|
|
121
121
|
|
|
122
122
|
{tickets.map((ticket) => {
|