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
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<Bundles/>` — the RN analogue of the web section: the event's bundles,
|
|
3
|
+
* bought on their own order. Choose bundles, enter your details (and, when
|
|
4
|
+
* offered, a name on each ticket), pay by card, see the confirmation.
|
|
5
|
+
* Everything the flow decides is in the shared core (`hooks.useBundleFlow`);
|
|
6
|
+
* this file only renders it.
|
|
7
|
+
*
|
|
8
|
+
* At most one native card field exists at a time: the bundle card field is
|
|
9
|
+
* mounted only while this section is visible.
|
|
10
|
+
*
|
|
11
|
+
* Renders nothing when the event sells no bundles. Translated through the
|
|
12
|
+
* shared translator; `locale`, `messages` or `translate` override it.
|
|
13
|
+
*/
|
|
14
|
+
import React, { useMemo, type FC } from 'react'
|
|
15
|
+
import { StyleSheet, Text, View } from 'react-native'
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
bundleAvailabilityCopy,
|
|
19
|
+
bundleContentsLine,
|
|
20
|
+
htmlToPlainText,
|
|
21
|
+
bundleQuantityRange,
|
|
22
|
+
createTranslator,
|
|
23
|
+
hooks,
|
|
24
|
+
type LocaleCode,
|
|
25
|
+
type Messages,
|
|
26
|
+
type PlaceOrderResult,
|
|
27
|
+
type Translate,
|
|
28
|
+
} from 'tf-checkout-shared'
|
|
29
|
+
|
|
30
|
+
import { CardInput, claimCardField } from '../CardInput'
|
|
31
|
+
import { rnConfirmer } from '../instance'
|
|
32
|
+
import { useCheckoutContext, useTheme } from '../ui/CheckoutProvider'
|
|
33
|
+
import { Field, Input } from '../ui/Field'
|
|
34
|
+
import { Button, Card, ErrorText, Loader } from '../ui/primitives'
|
|
35
|
+
import { QuantitySelect } from '../ui/QuantitySelect'
|
|
36
|
+
import type { ResolvedTheme } from '../ui/theme'
|
|
37
|
+
|
|
38
|
+
const money = (currency: string, amount: number) => `${currency} ${amount.toFixed(2)}`
|
|
39
|
+
|
|
40
|
+
export interface BundlesProps {
|
|
41
|
+
eventId: string
|
|
42
|
+
/** Fired once when a bundle purchase is complete, with its order hash. */
|
|
43
|
+
onPurchased?: (orderHash: string) => void
|
|
44
|
+
/**
|
|
45
|
+
* Fired once when a bundle purchase is complete, in the same shape as a
|
|
46
|
+
* ticket order, so the host routes both the same way.
|
|
47
|
+
*/
|
|
48
|
+
onComplete?: (order: PlaceOrderResult) => void
|
|
49
|
+
/** `redirect`: skip the inline receipt while the host navigates. */
|
|
50
|
+
confirmationMode?: 'inline' | 'redirect'
|
|
51
|
+
/** False while the section is hidden: no card field is mounted then. */
|
|
52
|
+
visible?: boolean
|
|
53
|
+
/** Section heading. Defaults to the translated "Bundles". */
|
|
54
|
+
heading?: string
|
|
55
|
+
locale?: LocaleCode
|
|
56
|
+
messages?: Partial<Messages>
|
|
57
|
+
translate?: Translate
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const Bundles: FC<BundlesProps> = ({
|
|
61
|
+
eventId,
|
|
62
|
+
onPurchased,
|
|
63
|
+
onComplete,
|
|
64
|
+
confirmationMode = 'inline',
|
|
65
|
+
visible = true,
|
|
66
|
+
heading,
|
|
67
|
+
locale,
|
|
68
|
+
messages,
|
|
69
|
+
translate,
|
|
70
|
+
}) => {
|
|
71
|
+
const checkout = useCheckoutContext()
|
|
72
|
+
const theme = useTheme()
|
|
73
|
+
const s = useBundleStyles(theme)
|
|
74
|
+
const t = translate ?? createTranslator({ locale, messages })
|
|
75
|
+
const flow = hooks.useBundleFlow(rnConfirmer, eventId, {
|
|
76
|
+
claimCard: claimCardField,
|
|
77
|
+
defaults: {
|
|
78
|
+
firstName: checkout.billing?.firstName ?? '',
|
|
79
|
+
lastName: checkout.billing?.lastName ?? '',
|
|
80
|
+
email: checkout.billing?.email ?? '',
|
|
81
|
+
},
|
|
82
|
+
store: checkout.store,
|
|
83
|
+
visible,
|
|
84
|
+
confirmationMode,
|
|
85
|
+
onComplete,
|
|
86
|
+
onPurchased,
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
if (flow.view === 'loading') {
|
|
90
|
+
return (
|
|
91
|
+
<Card>
|
|
92
|
+
<View style={s.loader}>
|
|
93
|
+
<Loader />
|
|
94
|
+
</View>
|
|
95
|
+
</Card>
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
if (flow.view === 'catalogError') {
|
|
99
|
+
return (
|
|
100
|
+
<Card>
|
|
101
|
+
<ErrorText>{t('bundles.error.catalog')}</ErrorText>
|
|
102
|
+
<Button
|
|
103
|
+
variant="ghost"
|
|
104
|
+
loading={flow.catalogRetrying}
|
|
105
|
+
disabled={flow.catalogRetrying}
|
|
106
|
+
onPress={flow.retryCatalog}
|
|
107
|
+
>
|
|
108
|
+
{t('bundles.retryCard')}
|
|
109
|
+
</Button>
|
|
110
|
+
</Card>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
// No bundles to sell and no purchase to show.
|
|
114
|
+
if (flow.view === 'hidden') return null
|
|
115
|
+
|
|
116
|
+
const title = <Text style={s.heading}>{heading ?? t('bundles.heading')}</Text>
|
|
117
|
+
|
|
118
|
+
if (flow.view === 'redirecting') {
|
|
119
|
+
return (
|
|
120
|
+
<Card>
|
|
121
|
+
{title}
|
|
122
|
+
<Text style={s.muted} accessibilityLiveRegion="polite">
|
|
123
|
+
{t('bundles.redirecting')}
|
|
124
|
+
</Text>
|
|
125
|
+
<Loader />
|
|
126
|
+
</Card>
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
if (flow.view === 'receipt' && flow.result) {
|
|
130
|
+
const { result } = flow
|
|
131
|
+
return (
|
|
132
|
+
<Card>
|
|
133
|
+
{title}
|
|
134
|
+
<Text style={s.strong}>{t('bundles.done.title')}</Text>
|
|
135
|
+
<Text style={s.muted}>
|
|
136
|
+
{result.pending
|
|
137
|
+
? t('bundles.done.pending')
|
|
138
|
+
: result.email
|
|
139
|
+
? t('bundles.done.email', { email: result.email })
|
|
140
|
+
: t('bundles.done.emailUnknown')}
|
|
141
|
+
</Text>
|
|
142
|
+
<Text style={s.muted}>
|
|
143
|
+
{t('bundles.done.order', { orderHash: result.orderHash })}
|
|
144
|
+
</Text>
|
|
145
|
+
{flow.gatewayWait ? (
|
|
146
|
+
<Text style={s.muted} accessibilityLiveRegion="polite">
|
|
147
|
+
{t(flow.gatewayWait)}
|
|
148
|
+
</Text>
|
|
149
|
+
) : null}
|
|
150
|
+
<Button
|
|
151
|
+
variant="ghost"
|
|
152
|
+
disabled={flow.gatewayBlocked}
|
|
153
|
+
onPress={() => void flow.again()}
|
|
154
|
+
>
|
|
155
|
+
{t('bundles.done.again')}
|
|
156
|
+
</Button>
|
|
157
|
+
</Card>
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const anySelected = Object.values(flow.qty).some((n) => n > 0)
|
|
162
|
+
|
|
163
|
+
if (flow.view === 'details') {
|
|
164
|
+
const amount = money(flow.payAmount.currency || flow.currency, flow.payAmount.amount)
|
|
165
|
+
return (
|
|
166
|
+
<Card>
|
|
167
|
+
{title}
|
|
168
|
+
{flow.notice ? (
|
|
169
|
+
<Text style={s.muted} accessibilityLiveRegion="polite">
|
|
170
|
+
{t(flow.notice.key, flow.notice.values)}
|
|
171
|
+
</Text>
|
|
172
|
+
) : null}
|
|
173
|
+
<Text style={s.strong}>{t('bundles.buyerHeading')}</Text>
|
|
174
|
+
<Field label={t('bundles.firstName')}>
|
|
175
|
+
<Input
|
|
176
|
+
value={flow.buyer.firstName}
|
|
177
|
+
autoCapitalize="words"
|
|
178
|
+
editable={!flow.busy}
|
|
179
|
+
onChangeText={(firstName) => flow.patchBuyer({ firstName })}
|
|
180
|
+
/>
|
|
181
|
+
</Field>
|
|
182
|
+
<Field label={t('bundles.lastName')}>
|
|
183
|
+
<Input
|
|
184
|
+
value={flow.buyer.lastName}
|
|
185
|
+
autoCapitalize="words"
|
|
186
|
+
editable={!flow.busy}
|
|
187
|
+
onChangeText={(lastName) => flow.patchBuyer({ lastName })}
|
|
188
|
+
/>
|
|
189
|
+
</Field>
|
|
190
|
+
<Field label={t('bundles.email')}>
|
|
191
|
+
<Input
|
|
192
|
+
value={flow.buyer.email}
|
|
193
|
+
keyboardType="email-address"
|
|
194
|
+
autoCapitalize="none"
|
|
195
|
+
editable={!flow.busy}
|
|
196
|
+
onChangeText={(email) => flow.patchBuyer({ email })}
|
|
197
|
+
/>
|
|
198
|
+
</Field>
|
|
199
|
+
|
|
200
|
+
{flow.slots.length > 0 ? (
|
|
201
|
+
<>
|
|
202
|
+
<Text style={s.strong}>{t('bundles.holdersHeading')}</Text>
|
|
203
|
+
{!flow.namesRequired ? (
|
|
204
|
+
<Text style={s.muted}>{t('bundles.holdersOptional')}</Text>
|
|
205
|
+
) : null}
|
|
206
|
+
{flow.slots.map((slot) => {
|
|
207
|
+
const value = flow.holderValue(slot)
|
|
208
|
+
const label = t('bundles.holderLabel', {
|
|
209
|
+
ticket: slot.ticketName,
|
|
210
|
+
number: String(slot.ticketIndex + 1),
|
|
211
|
+
})
|
|
212
|
+
return (
|
|
213
|
+
<View key={`${slot.bundleId}-${slot.ticketIndex}`}>
|
|
214
|
+
<Field label={`${label} · ${t('bundles.holderFirstName')}`}>
|
|
215
|
+
<Input
|
|
216
|
+
value={value?.firstName ?? ''}
|
|
217
|
+
autoCapitalize="words"
|
|
218
|
+
editable={!flow.busy}
|
|
219
|
+
onChangeText={(firstName) => flow.setHolder(slot, { firstName })}
|
|
220
|
+
/>
|
|
221
|
+
</Field>
|
|
222
|
+
<Field label={`${label} · ${t('bundles.holderLastName')}`}>
|
|
223
|
+
<Input
|
|
224
|
+
value={value?.lastName ?? ''}
|
|
225
|
+
autoCapitalize="words"
|
|
226
|
+
editable={!flow.busy}
|
|
227
|
+
onChangeText={(lastName) => flow.setHolder(slot, { lastName })}
|
|
228
|
+
/>
|
|
229
|
+
</Field>
|
|
230
|
+
</View>
|
|
231
|
+
)
|
|
232
|
+
})}
|
|
233
|
+
</>
|
|
234
|
+
) : null}
|
|
235
|
+
{flow.problems.map((key) => (
|
|
236
|
+
<ErrorText key={key}>{t(key)}</ErrorText>
|
|
237
|
+
))}
|
|
238
|
+
|
|
239
|
+
{flow.awaitingCard ? (
|
|
240
|
+
<View style={s.card}>
|
|
241
|
+
<Text style={s.strong}>{t('bundles.cardHeading')}</Text>
|
|
242
|
+
{flow.cardElsewhere ? (
|
|
243
|
+
<>
|
|
244
|
+
<Text style={s.muted}>{t('bundles.payingElsewhere')}</Text>
|
|
245
|
+
<Button variant="ghost" onPress={flow.payHere}>
|
|
246
|
+
{t('bundles.payHere')}
|
|
247
|
+
</Button>
|
|
248
|
+
</>
|
|
249
|
+
) : flow.cardFailed ? (
|
|
250
|
+
<Button variant="ghost" onPress={flow.retryCard}>
|
|
251
|
+
{t('bundles.retryCard')}
|
|
252
|
+
</Button>
|
|
253
|
+
) : flow.cardReady && visible ? (
|
|
254
|
+
<CardInput owner={flow.cardOwner} visible={visible} />
|
|
255
|
+
) : (
|
|
256
|
+
<Loader />
|
|
257
|
+
)}
|
|
258
|
+
</View>
|
|
259
|
+
) : null}
|
|
260
|
+
|
|
261
|
+
{flow.stillSaving ? (
|
|
262
|
+
<Text style={s.muted} accessibilityLiveRegion="polite">
|
|
263
|
+
{t('common.stillSaving')}
|
|
264
|
+
</Text>
|
|
265
|
+
) : null}
|
|
266
|
+
{flow.checkingAttempt ? (
|
|
267
|
+
<Text style={s.muted} accessibilityLiveRegion="polite">
|
|
268
|
+
{t('bundles.checkingAttempt')}
|
|
269
|
+
</Text>
|
|
270
|
+
) : null}
|
|
271
|
+
{flow.waitingForTickets ? (
|
|
272
|
+
<Text style={s.muted} accessibilityLiveRegion="polite">
|
|
273
|
+
{t('bundles.waitTickets')}
|
|
274
|
+
</Text>
|
|
275
|
+
) : null}
|
|
276
|
+
{flow.waitingForBundle ? (
|
|
277
|
+
<Text style={s.muted} accessibilityLiveRegion="polite">
|
|
278
|
+
{t(flow.gatewayWait ?? 'bundles.waitOtherBundle')}
|
|
279
|
+
</Text>
|
|
280
|
+
) : null}
|
|
281
|
+
<Button
|
|
282
|
+
onPress={() => void flow.submit()}
|
|
283
|
+
loading={flow.busy}
|
|
284
|
+
disabled={flow.submitDisabled}
|
|
285
|
+
>
|
|
286
|
+
{flow.paying
|
|
287
|
+
? t('bundles.paying')
|
|
288
|
+
: flow.awaitingCard
|
|
289
|
+
? t('bundles.pay', { amount })
|
|
290
|
+
: t('bundles.continue')}
|
|
291
|
+
</Button>
|
|
292
|
+
<Button
|
|
293
|
+
variant="ghost"
|
|
294
|
+
disabled={flow.busy || flow.gatewayBlocked}
|
|
295
|
+
onPress={() => void flow.back()}
|
|
296
|
+
>
|
|
297
|
+
{t('bundles.back')}
|
|
298
|
+
</Button>
|
|
299
|
+
{flow.error ? <ErrorText>{t(flow.error)}</ErrorText> : null}
|
|
300
|
+
{flow.orderInProgress ? (
|
|
301
|
+
<Button variant="ghost" onPress={flow.continueOrder}>
|
|
302
|
+
{t('bundles.continueOrder')}
|
|
303
|
+
</Button>
|
|
304
|
+
) : null}
|
|
305
|
+
{flow.canStartOver ? (
|
|
306
|
+
<Button
|
|
307
|
+
variant="ghost"
|
|
308
|
+
disabled={flow.gatewayBlocked}
|
|
309
|
+
onPress={() => void flow.startOver()}
|
|
310
|
+
>
|
|
311
|
+
{t('bundles.startOver')}
|
|
312
|
+
</Button>
|
|
313
|
+
) : null}
|
|
314
|
+
</Card>
|
|
315
|
+
)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const ticketsInCart = checkout.selections.length > 0
|
|
319
|
+
return (
|
|
320
|
+
<Card>
|
|
321
|
+
{title}
|
|
322
|
+
<Text style={s.muted}>{t('bundles.subtitle')}</Text>
|
|
323
|
+
{flow.notice ? (
|
|
324
|
+
<Text style={s.muted} accessibilityLiveRegion="polite">
|
|
325
|
+
{t(flow.notice.key, flow.notice.values)}
|
|
326
|
+
</Text>
|
|
327
|
+
) : null}
|
|
328
|
+
{flow.view === 'heldOrder' ? (
|
|
329
|
+
<>
|
|
330
|
+
<Text style={s.muted} accessibilityLiveRegion="polite">
|
|
331
|
+
{t('bundles.heldOrder')}
|
|
332
|
+
</Text>
|
|
333
|
+
{flow.error ? <ErrorText>{t(flow.error)}</ErrorText> : null}
|
|
334
|
+
{flow.checkingAttempt ? (
|
|
335
|
+
<Text style={s.muted} accessibilityLiveRegion="polite">
|
|
336
|
+
{t('bundles.checkingAttempt')}
|
|
337
|
+
</Text>
|
|
338
|
+
) : null}
|
|
339
|
+
{flow.gatewayWait ? (
|
|
340
|
+
<Text style={s.muted} accessibilityLiveRegion="polite">
|
|
341
|
+
{t(flow.gatewayWait)}
|
|
342
|
+
</Text>
|
|
343
|
+
) : null}
|
|
344
|
+
<Button
|
|
345
|
+
disabled={flow.busy || flow.checkingAttempt}
|
|
346
|
+
onPress={flow.continueOrder}
|
|
347
|
+
>
|
|
348
|
+
{t('bundles.continueOrder')}
|
|
349
|
+
</Button>
|
|
350
|
+
<Button
|
|
351
|
+
variant="ghost"
|
|
352
|
+
disabled={flow.busy || flow.checkingAttempt || flow.gatewayBlocked}
|
|
353
|
+
onPress={() => void flow.startOver()}
|
|
354
|
+
>
|
|
355
|
+
{t('bundles.startOver')}
|
|
356
|
+
</Button>
|
|
357
|
+
</>
|
|
358
|
+
) : null}
|
|
359
|
+
|
|
360
|
+
{flow.bundles.map((bundle) => {
|
|
361
|
+
const range = bundleQuantityRange(bundle)
|
|
362
|
+
const stock = bundleAvailabilityCopy(bundle)
|
|
363
|
+
const contents = bundleContentsLine(bundle)
|
|
364
|
+
const desc = htmlToPlainText(bundle.description ?? '')
|
|
365
|
+
return (
|
|
366
|
+
<View key={bundle.id} style={[s.item, { borderTopColor: theme.border }]}>
|
|
367
|
+
<View style={s.info}>
|
|
368
|
+
<Text style={s.name}>{bundle.name}</Text>
|
|
369
|
+
<Text style={s.price}>{money(bundle.currency, bundle.price)}</Text>
|
|
370
|
+
{contents ? (
|
|
371
|
+
<Text style={s.muted}>{t('bundles.includes', { contents })}</Text>
|
|
372
|
+
) : null}
|
|
373
|
+
{bundle.bookingFee > 0 && (
|
|
374
|
+
<Text style={s.muted}>
|
|
375
|
+
{t('bundles.bookingFee', {
|
|
376
|
+
amount: money(bundle.currency, bundle.bookingFee),
|
|
377
|
+
})}
|
|
378
|
+
</Text>
|
|
379
|
+
)}
|
|
380
|
+
{desc ? <Text style={s.muted}>{desc}</Text> : null}
|
|
381
|
+
{stock && stock.key !== 'bundles.soldOut' ? (
|
|
382
|
+
<Text style={[s.muted, s.stock]}>{t(stock.key, stock.values)}</Text>
|
|
383
|
+
) : null}
|
|
384
|
+
</View>
|
|
385
|
+
{range.max === 0 ? (
|
|
386
|
+
<Text style={s.soldOut}>{t('bundles.soldOut')}</Text>
|
|
387
|
+
) : (
|
|
388
|
+
<QuantitySelect
|
|
389
|
+
label={t('bundles.quantity', { name: bundle.name })}
|
|
390
|
+
value={flow.qty[bundle.id] ?? 0}
|
|
391
|
+
min={range.min}
|
|
392
|
+
max={range.max}
|
|
393
|
+
onChange={(v) => flow.setQuantity(bundle.id, v)}
|
|
394
|
+
/>
|
|
395
|
+
)}
|
|
396
|
+
</View>
|
|
397
|
+
)
|
|
398
|
+
})}
|
|
399
|
+
|
|
400
|
+
{anySelected ? (
|
|
401
|
+
<Text style={s.strong}>
|
|
402
|
+
{t('bundles.total', { amount: money(flow.currency, flow.total) })}
|
|
403
|
+
</Text>
|
|
404
|
+
) : null}
|
|
405
|
+
{anySelected && ticketsInCart ? (
|
|
406
|
+
<Text style={s.muted}>{t('bundles.boughtSeparately')}</Text>
|
|
407
|
+
) : null}
|
|
408
|
+
<Button onPress={flow.continueToDetails} disabled={!anySelected}>
|
|
409
|
+
{t('bundles.continue')}
|
|
410
|
+
</Button>
|
|
411
|
+
</Card>
|
|
412
|
+
)
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const useBundleStyles = (theme: ResolvedTheme) =>
|
|
416
|
+
useMemo(
|
|
417
|
+
() =>
|
|
418
|
+
StyleSheet.create({
|
|
419
|
+
loader: { paddingVertical: 20, alignItems: 'center' },
|
|
420
|
+
heading: {
|
|
421
|
+
color: theme.text,
|
|
422
|
+
fontSize: theme.fontSize + 2,
|
|
423
|
+
fontWeight: '700',
|
|
424
|
+
fontFamily: theme.fontFamily,
|
|
425
|
+
},
|
|
426
|
+
strong: {
|
|
427
|
+
color: theme.text,
|
|
428
|
+
fontSize: theme.fontSize,
|
|
429
|
+
fontWeight: '700',
|
|
430
|
+
fontFamily: theme.fontFamily,
|
|
431
|
+
},
|
|
432
|
+
muted: {
|
|
433
|
+
color: theme.muted,
|
|
434
|
+
fontSize: theme.fontSize - 1,
|
|
435
|
+
fontFamily: theme.fontFamily,
|
|
436
|
+
},
|
|
437
|
+
stock: { fontWeight: '600' },
|
|
438
|
+
card: { gap: 8 },
|
|
439
|
+
item: {
|
|
440
|
+
flexDirection: 'row',
|
|
441
|
+
alignItems: 'flex-start',
|
|
442
|
+
gap: 12,
|
|
443
|
+
paddingTop: 12,
|
|
444
|
+
borderTopWidth: StyleSheet.hairlineWidth,
|
|
445
|
+
},
|
|
446
|
+
info: { flex: 1, gap: 2 },
|
|
447
|
+
name: {
|
|
448
|
+
color: theme.text,
|
|
449
|
+
fontSize: theme.fontSize,
|
|
450
|
+
fontWeight: '600',
|
|
451
|
+
fontFamily: theme.fontFamily,
|
|
452
|
+
},
|
|
453
|
+
price: {
|
|
454
|
+
color: theme.text,
|
|
455
|
+
fontSize: theme.fontSize - 1,
|
|
456
|
+
fontVariant: ['tabular-nums'],
|
|
457
|
+
fontFamily: theme.fontFamily,
|
|
458
|
+
},
|
|
459
|
+
soldOut: {
|
|
460
|
+
color: theme.muted,
|
|
461
|
+
fontSize: theme.fontSize - 1,
|
|
462
|
+
fontWeight: '600',
|
|
463
|
+
fontFamily: theme.fontFamily,
|
|
464
|
+
},
|
|
465
|
+
}),
|
|
466
|
+
[theme]
|
|
467
|
+
)
|