tf-checkout-react-native 2.0.0-beta.16 → 2.0.0-beta.18
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 +7 -0
- package/dist/index.d.mts +78 -9
- package/dist/index.d.ts +78 -9
- package/dist/index.js +2648 -2134
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2344 -1823
- package/dist/index.mjs.map +1 -1
- package/package.json +19 -1
- package/src/paymentSheet.ts +15 -6
- 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 +26 -99
- 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.18",
|
|
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.",
|
package/src/paymentSheet.ts
CHANGED
|
@@ -59,12 +59,21 @@ export const nativeSheetIntentIdentity = (
|
|
|
59
59
|
)
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
/**
|
|
63
|
-
*
|
|
64
|
-
const
|
|
62
|
+
/** Keep the complete original list: the server can enable immediate USD Cash
|
|
63
|
+
* App Pay beside card. Other rails still need their own canonical contract. */
|
|
64
|
+
const nativeSheetMethods = (intent: unknown): boolean => {
|
|
65
65
|
if (!intent || typeof intent !== 'object') return false
|
|
66
66
|
const methods = Object.getOwnPropertyDescriptor(intent, 'paymentMethodTypes')?.value
|
|
67
|
-
|
|
67
|
+
if (!Array.isArray(methods)) return false
|
|
68
|
+
if (methods.length === 1) return methods[0] === 'card'
|
|
69
|
+
const currency = Object.getOwnPropertyDescriptor(intent, 'currency')?.value
|
|
70
|
+
return (
|
|
71
|
+
methods.length === 2 &&
|
|
72
|
+
methods.includes('card') &&
|
|
73
|
+
methods.includes('cashapp') &&
|
|
74
|
+
typeof currency === 'string' &&
|
|
75
|
+
currency.toUpperCase() === 'USD'
|
|
76
|
+
)
|
|
68
77
|
}
|
|
69
78
|
|
|
70
79
|
/** Internal building block. The caller must hold
|
|
@@ -111,7 +120,7 @@ export const createPaymentSheetConfirmation = (
|
|
|
111
120
|
!nativeSheetIntentIdentity(result.paymentIntent, intentId, options.reviewed)
|
|
112
121
|
)
|
|
113
122
|
throw paymentProviderError(NATIVE_SHEET_REVIEW_REQUIRED, 'notStarted')
|
|
114
|
-
if (!
|
|
123
|
+
if (!nativeSheetMethods(result.paymentIntent))
|
|
115
124
|
throw paymentProviderError(NATIVE_SHEET_CARD_FIELD_MESSAGE, 'notStarted', {
|
|
116
125
|
code: NATIVE_SHEET_CARD_FIELD_REQUIRED,
|
|
117
126
|
})
|
|
@@ -155,7 +164,7 @@ export const createPaymentSheetConfirmation = (
|
|
|
155
164
|
if (
|
|
156
165
|
result.error ||
|
|
157
166
|
!nativeSheetIntentIdentity(result.paymentIntent, intentId, options.reviewed) ||
|
|
158
|
-
!
|
|
167
|
+
!nativeSheetMethods(result.paymentIntent)
|
|
159
168
|
)
|
|
160
169
|
throw paymentProviderError(
|
|
161
170
|
'The original payment could not be verified.',
|
|
@@ -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
|
|
@@ -178,6 +117,13 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
178
117
|
const theme = useTheme()
|
|
179
118
|
const s = useTicketStyles(theme)
|
|
180
119
|
const ticketsQuery = hooks.useTicketsWithMeta(eventId)
|
|
120
|
+
const waitingBuyer = hooks.useAuthSession()
|
|
121
|
+
const cachedProfile = hooks.useProfile({ enabled: false }).data
|
|
122
|
+
const waitingPrefill = waitingListProfilePrefill(
|
|
123
|
+
cachedProfile,
|
|
124
|
+
waitingBuyer.buyerId,
|
|
125
|
+
waitingBuyer.sessionScope
|
|
126
|
+
)
|
|
181
127
|
// A bundle order is starting: wait, so the two flows never start at once.
|
|
182
128
|
// The checkout start lock is held by a bundle start or pay (shown as a
|
|
183
129
|
// wait), or by this checkout's own priming (shown as the busy button).
|
|
@@ -185,7 +131,6 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
185
131
|
const bundleWait = hooks.useTicketGatewayWait(checkout.store)
|
|
186
132
|
const bundleStarting = bundleWait !== null
|
|
187
133
|
const promoMeta = ticketsQuery.data?.promo
|
|
188
|
-
const showWaitingList = ticketsQuery.data?.showWaitingList ?? false
|
|
189
134
|
|
|
190
135
|
const [qty, setQty] = useState<Record<string, number>>({})
|
|
191
136
|
const [slotDate, setSlotDate] = useState('')
|
|
@@ -201,6 +146,11 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
201
146
|
if (isTimeSlot && !slotDate && dates.length) setSlotDate(dates[0])
|
|
202
147
|
}, [isTimeSlot, slotDate, dates])
|
|
203
148
|
const slotQuery = hooks.useTimeSlotTickets(eventId, isTimeSlot ? slotDate : undefined)
|
|
149
|
+
const refetchCatalog = () =>
|
|
150
|
+
Promise.all([
|
|
151
|
+
ticketsQuery.refetch(),
|
|
152
|
+
...(isTimeSlot && slotDate ? [slotQuery.refetch()] : []),
|
|
153
|
+
])
|
|
204
154
|
|
|
205
155
|
const tickets = useMemo(
|
|
206
156
|
() => (isTimeSlot ? (slotQuery.data ?? []) : (ticketsQuery.data?.tickets ?? [])),
|
|
@@ -218,7 +168,6 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
218
168
|
setPromoOpen(false)
|
|
219
169
|
setError('')
|
|
220
170
|
})
|
|
221
|
-
const [wlOpen, setWlOpen] = useState<Record<string, boolean>>({})
|
|
222
171
|
const [visibleDescription, setVisibleDescription] = useState<string | null>(null)
|
|
223
172
|
const attributionEvent = useRef<string | null>(null)
|
|
224
173
|
|
|
@@ -243,7 +192,7 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
243
192
|
.applyPromoCode(eventId, promo)
|
|
244
193
|
.catch(() => undefined)
|
|
245
194
|
.finally(() => {
|
|
246
|
-
|
|
195
|
+
refetchCatalog().finally(() => setCodeState('idle'))
|
|
247
196
|
})
|
|
248
197
|
}
|
|
249
198
|
// Checkout actions and query functions are stable; this effect is scoped to
|
|
@@ -264,7 +213,7 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
264
213
|
.then((res) => {
|
|
265
214
|
if (res.requiresCatalogRefresh) {
|
|
266
215
|
setQty({})
|
|
267
|
-
|
|
216
|
+
refetchCatalog()
|
|
268
217
|
}
|
|
269
218
|
})
|
|
270
219
|
.catch(() => undefined)
|
|
@@ -340,7 +289,7 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
340
289
|
.applyPromoCode(eventId, code.trim())
|
|
341
290
|
.catch(() => undefined)
|
|
342
291
|
.finally(() => {
|
|
343
|
-
|
|
292
|
+
refetchCatalog().finally(() => setCodeState('idle'))
|
|
344
293
|
})
|
|
345
294
|
}
|
|
346
295
|
|
|
@@ -354,8 +303,6 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
354
303
|
const unavailable = t.soldOut || t.salesEnded || notStarted
|
|
355
304
|
const original = originals.current[t.id]
|
|
356
305
|
const discounted = original != null && original > t.price
|
|
357
|
-
const canWaitlist =
|
|
358
|
-
t.soldOut && (t.waitingListEnabled || showWaitingList) && !t.salesEnded
|
|
359
306
|
const isFree = Number(t.price) === 0
|
|
360
307
|
const sym = t.priceSymbol || t.currency
|
|
361
308
|
const range = ticketQuantityRange(t)
|
|
@@ -457,26 +404,10 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
457
404
|
)}
|
|
458
405
|
</View>
|
|
459
406
|
{t.descriptionRich && visibleDescription === t.id ? (
|
|
460
|
-
<
|
|
407
|
+
<RichText html={t.descriptionRich} textStyle={s.description} />
|
|
461
408
|
) : null}
|
|
462
409
|
</>
|
|
463
410
|
)}
|
|
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
411
|
</View>
|
|
481
412
|
</Fragment>
|
|
482
413
|
)
|
|
@@ -639,6 +570,13 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
639
570
|
</Button>
|
|
640
571
|
) : null}
|
|
641
572
|
|
|
573
|
+
<WaitingListForm
|
|
574
|
+
eventId={eventId}
|
|
575
|
+
definition={ticketsQuery.data?.waitingList}
|
|
576
|
+
prefill={waitingPrefill}
|
|
577
|
+
onJoin={(entry) => checkout.joinWaitingList(eventId, entry)}
|
|
578
|
+
/>
|
|
579
|
+
|
|
642
580
|
{error || checkout.error ? (
|
|
643
581
|
<ErrorText>{error || checkout.error}</ErrorText>
|
|
644
582
|
) : null}
|
|
@@ -647,11 +585,6 @@ export const Tickets: FC<TicketsProps> = ({
|
|
|
647
585
|
)
|
|
648
586
|
}
|
|
649
587
|
|
|
650
|
-
/** Theme-independent layout bits (used by the WaitingListForm subcomponent). */
|
|
651
|
-
const staticStyles = StyleSheet.create({
|
|
652
|
-
wlForm: { gap: 8 },
|
|
653
|
-
})
|
|
654
|
-
|
|
655
588
|
const useTicketStyles = (theme: ResolvedTheme) =>
|
|
656
589
|
useMemo(
|
|
657
590
|
() =>
|
|
@@ -775,12 +708,6 @@ const useTicketStyles = (theme: ResolvedTheme) =>
|
|
|
775
708
|
lineHeight: theme.fontSize * 1.5,
|
|
776
709
|
fontFamily: theme.fontFamily,
|
|
777
710
|
},
|
|
778
|
-
wlToggle: {
|
|
779
|
-
color: theme.accent,
|
|
780
|
-
fontSize: theme.fontSize - 1,
|
|
781
|
-
fontWeight: '600',
|
|
782
|
-
fontFamily: theme.fontFamily,
|
|
783
|
-
},
|
|
784
711
|
promo: { gap: 8 },
|
|
785
712
|
promoToggle: {
|
|
786
713
|
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) => {
|