tf-checkout-react-native 2.0.0-beta.0 → 2.0.0-beta.11
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 +8195 -282
- package/dist/index.d.ts +8195 -282
- package/dist/index.js +32346 -7001
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +32586 -7128
- 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,105 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { View } from 'react-native'
|
|
3
|
+
import { accountPlanDate, hooks, type AccountPaymentPlan } from 'tf-checkout-shared'
|
|
4
|
+
import {
|
|
5
|
+
BodyText,
|
|
6
|
+
Button,
|
|
7
|
+
Card,
|
|
8
|
+
ErrorText,
|
|
9
|
+
MutedText,
|
|
10
|
+
SectionHeader,
|
|
11
|
+
} from '../ui/primitives'
|
|
12
|
+
import { useAccountT } from '../ui/useAccountT'
|
|
13
|
+
export interface AccountPaymentPlanSectionProps {
|
|
14
|
+
plan: AccountPaymentPlan
|
|
15
|
+
orderHash: string
|
|
16
|
+
locale?: string
|
|
17
|
+
onOpen: (url: string) => void | Promise<unknown>
|
|
18
|
+
onReturn?: () => void | Promise<unknown>
|
|
19
|
+
}
|
|
20
|
+
export const AccountPaymentPlanSection = ({
|
|
21
|
+
plan,
|
|
22
|
+
orderHash,
|
|
23
|
+
locale,
|
|
24
|
+
onOpen,
|
|
25
|
+
onReturn,
|
|
26
|
+
}: AccountPaymentPlanSectionProps) => {
|
|
27
|
+
const t = useAccountT(locale)
|
|
28
|
+
const flow = hooks.useAccountPlanManagement(plan, orderHash, onOpen, onReturn)
|
|
29
|
+
return (
|
|
30
|
+
<Card>
|
|
31
|
+
<SectionHeader title={plan.terminology || t('Payment plan')} />
|
|
32
|
+
<BodyText>
|
|
33
|
+
{t(
|
|
34
|
+
plan.requiresCompletion
|
|
35
|
+
? 'Payment plan needs attention'
|
|
36
|
+
: plan.status === 'pending'
|
|
37
|
+
? 'Payments in progress'
|
|
38
|
+
: plan.status === 'finished'
|
|
39
|
+
? 'All payments complete'
|
|
40
|
+
: plan.status === 'cancelled'
|
|
41
|
+
? 'Payment plan cancelled'
|
|
42
|
+
: 'Payment plan setup incomplete'
|
|
43
|
+
)}
|
|
44
|
+
</BodyText>
|
|
45
|
+
<BodyText>
|
|
46
|
+
{t('Plan total')}: {plan.currency} {plan.totalAmount}
|
|
47
|
+
</BodyText>
|
|
48
|
+
{plan.remainingAmount !== null ? (
|
|
49
|
+
<BodyText>
|
|
50
|
+
{t('Remaining balance')}: {plan.currency} {plan.remainingAmount}
|
|
51
|
+
</BodyText>
|
|
52
|
+
) : null}
|
|
53
|
+
{plan.installments.map((row, index) => (
|
|
54
|
+
<View key={index} style={{ gap: 4, marginVertical: 8 }}>
|
|
55
|
+
<BodyText>
|
|
56
|
+
{row.currency} {row.amount} ·{' '}
|
|
57
|
+
{t(
|
|
58
|
+
row.status === 'paid'
|
|
59
|
+
? 'Paid'
|
|
60
|
+
: row.status === 'unpaid'
|
|
61
|
+
? 'Scheduled'
|
|
62
|
+
: row.status === 'void' || row.status === 'uncollectible'
|
|
63
|
+
? 'Payment unsuccessful'
|
|
64
|
+
: row.status
|
|
65
|
+
)}
|
|
66
|
+
</BodyText>
|
|
67
|
+
<MutedText>
|
|
68
|
+
{accountPlanDate(
|
|
69
|
+
row.status === 'paid'
|
|
70
|
+
? (row.chargeDate ?? row.updatedAt)
|
|
71
|
+
: row.scheduledDate,
|
|
72
|
+
locale,
|
|
73
|
+
row.status === 'paid'
|
|
74
|
+
) || t('Date not scheduled')}
|
|
75
|
+
</MutedText>
|
|
76
|
+
</View>
|
|
77
|
+
))}
|
|
78
|
+
{plan.status === 'pending' ? (
|
|
79
|
+
<MutedText>
|
|
80
|
+
{t('Your QR code becomes available when your payment plan is complete.')}
|
|
81
|
+
</MutedText>
|
|
82
|
+
) : null}
|
|
83
|
+
<MutedText>
|
|
84
|
+
{t('Manage this plan on the Ticket Fairy website. Sign in there if asked.')}
|
|
85
|
+
</MutedText>
|
|
86
|
+
{flow.error ? <ErrorText>{t(flow.error)}</ErrorText> : null}
|
|
87
|
+
<Button
|
|
88
|
+
disabled={flow.busy}
|
|
89
|
+
onPress={() => {
|
|
90
|
+
void flow.open(
|
|
91
|
+
plan.completeUrl ? 'complete' : plan.updateCardUrl ? 'update' : 'manage'
|
|
92
|
+
)
|
|
93
|
+
}}
|
|
94
|
+
>
|
|
95
|
+
{t(
|
|
96
|
+
plan.completeUrl
|
|
97
|
+
? 'Complete payment plan'
|
|
98
|
+
: plan.updateCardUrl
|
|
99
|
+
? 'Update card details'
|
|
100
|
+
: 'View payment plan on website'
|
|
101
|
+
)}
|
|
102
|
+
</Button>
|
|
103
|
+
</Card>
|
|
104
|
+
)
|
|
105
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import React, { type FC } from 'react'
|
|
2
|
+
import { Alert, Image, Share, View } from 'react-native'
|
|
3
|
+
import {
|
|
4
|
+
createTranslator,
|
|
5
|
+
hooks,
|
|
6
|
+
resolveLocale,
|
|
7
|
+
type AccountRewardsSectionProps,
|
|
8
|
+
} from 'tf-checkout-shared'
|
|
9
|
+
import {
|
|
10
|
+
BodyText,
|
|
11
|
+
Button,
|
|
12
|
+
Card,
|
|
13
|
+
ErrorText,
|
|
14
|
+
Loader,
|
|
15
|
+
SectionHeader,
|
|
16
|
+
} from '../ui/primitives'
|
|
17
|
+
|
|
18
|
+
/** The account aggregate has no order identity and offers no reward mutations. */
|
|
19
|
+
export const AccountRewardsSection: FC<AccountRewardsSectionProps> = ({
|
|
20
|
+
locale,
|
|
21
|
+
onBrowseEvents,
|
|
22
|
+
onViewTickets,
|
|
23
|
+
}) => {
|
|
24
|
+
const t = createTranslator({ locale: resolveLocale(locale) })
|
|
25
|
+
const { session, query, view } = hooks.useAccountRewardsSummary(t)
|
|
26
|
+
const assertOwner = hooks.useSessionAction()
|
|
27
|
+
const share = async (imageUrl: string) => {
|
|
28
|
+
try {
|
|
29
|
+
assertOwner()
|
|
30
|
+
await Share.share({ url: imageUrl, message: imageUrl })
|
|
31
|
+
} catch {
|
|
32
|
+
try {
|
|
33
|
+
assertOwner()
|
|
34
|
+
Alert.alert(t('rewards.account.shareError'))
|
|
35
|
+
} catch {
|
|
36
|
+
// A retired buyer must not display an alert in the next buyer's account.
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return (
|
|
41
|
+
<View style={{ gap: 16 }}>
|
|
42
|
+
<SectionHeader
|
|
43
|
+
title={t('rewards.account.title')}
|
|
44
|
+
subtitle={t('rewards.account.intro')}
|
|
45
|
+
/>
|
|
46
|
+
{!session.isAuthenticated ? (
|
|
47
|
+
<BodyText>{t('rewards.account.signIn')}</BodyText>
|
|
48
|
+
) : (
|
|
49
|
+
<>
|
|
50
|
+
{query.isLoading && <Loader />}
|
|
51
|
+
{query.error && <ErrorText>{t('rewards.account.error')}</ErrorText>}
|
|
52
|
+
<Button
|
|
53
|
+
variant="ghost"
|
|
54
|
+
loading={query.isFetching}
|
|
55
|
+
onPress={() => void query.refetch()}
|
|
56
|
+
>
|
|
57
|
+
{t('rewards.account.refresh')}
|
|
58
|
+
</Button>
|
|
59
|
+
{view?.empty && <BodyText>{t('rewards.account.empty')}</BodyText>}
|
|
60
|
+
{view &&
|
|
61
|
+
(['ambassador', 'packs', 'prizes'] as const).map((kind) =>
|
|
62
|
+
view[kind].length ? (
|
|
63
|
+
<View key={kind} style={{ gap: 12 }}>
|
|
64
|
+
<SectionHeader title={t(`rewards.account.${kind}`)} />
|
|
65
|
+
{view[kind].map((row) => (
|
|
66
|
+
<Card key={row.key}>
|
|
67
|
+
<SectionHeader title={row.title} />
|
|
68
|
+
{row.lines.map((line, index) => (
|
|
69
|
+
<BodyText key={index}>{line}</BodyText>
|
|
70
|
+
))}
|
|
71
|
+
</Card>
|
|
72
|
+
))}
|
|
73
|
+
</View>
|
|
74
|
+
) : null
|
|
75
|
+
)}
|
|
76
|
+
{!!view?.shareCards.length && (
|
|
77
|
+
<View style={{ gap: 12 }}>
|
|
78
|
+
<SectionHeader title={t('rewards.account.shareCards')} />
|
|
79
|
+
{view.shareCards.map((card) => (
|
|
80
|
+
<Card key={card.key}>
|
|
81
|
+
<Image
|
|
82
|
+
source={{ uri: card.imageUrl }}
|
|
83
|
+
accessibilityLabel={t('rewards.account.shareCard')}
|
|
84
|
+
resizeMode="contain"
|
|
85
|
+
style={{ width: '100%', aspectRatio: 1.91 }}
|
|
86
|
+
/>
|
|
87
|
+
<Button variant="ghost" onPress={() => void share(card.imageUrl)}>
|
|
88
|
+
{t('rewards.account.share')}
|
|
89
|
+
</Button>
|
|
90
|
+
</Card>
|
|
91
|
+
))}
|
|
92
|
+
</View>
|
|
93
|
+
)}
|
|
94
|
+
{onViewTickets && (
|
|
95
|
+
<Button variant="ghost" onPress={onViewTickets}>
|
|
96
|
+
{t('rewards.account.viewTickets')}
|
|
97
|
+
</Button>
|
|
98
|
+
)}
|
|
99
|
+
{onBrowseEvents && (
|
|
100
|
+
<Button variant="ghost" onPress={onBrowseEvents}>
|
|
101
|
+
{t('rewards.account.browse')}
|
|
102
|
+
</Button>
|
|
103
|
+
)}
|
|
104
|
+
</>
|
|
105
|
+
)}
|
|
106
|
+
</View>
|
|
107
|
+
)
|
|
108
|
+
}
|
package/src/sections/AddOns.tsx
CHANGED
|
@@ -8,18 +8,18 @@
|
|
|
8
8
|
* Renders nothing when the event has no applicable add-ons (or they're all sold
|
|
9
9
|
* out), so it's safe to always place between `<Tickets/>` and `<BillingInfo/>`.
|
|
10
10
|
*/
|
|
11
|
-
import React, { Fragment,
|
|
11
|
+
import React, { Fragment, useMemo, type FC } from 'react'
|
|
12
12
|
import { Pressable, StyleSheet, Text, View } from 'react-native'
|
|
13
13
|
|
|
14
14
|
import {
|
|
15
15
|
addOnMaxQuantity,
|
|
16
16
|
errorMessage,
|
|
17
17
|
hooks,
|
|
18
|
-
|
|
19
|
-
type CustomFieldValues,
|
|
18
|
+
htmlToPlainText,
|
|
20
19
|
type EventAddon,
|
|
21
20
|
} from 'tf-checkout-shared'
|
|
22
21
|
|
|
22
|
+
import { FrozenFields } from '../ui/Frozen'
|
|
23
23
|
import { Button, Card, ErrorText, Loader, MutedText } from '../ui/primitives'
|
|
24
24
|
import { useCheckoutContext, useTheme } from '../ui/CheckoutProvider'
|
|
25
25
|
// Created by a sibling agent; imported here per the port plan. Mirrors the web
|
|
@@ -28,24 +28,6 @@ import { CustomFieldGroupFields } from '../ui/CustomFields'
|
|
|
28
28
|
import { QuantitySelect } from '../ui/QuantitySelect'
|
|
29
29
|
import type { ResolvedTheme } from '../ui/theme'
|
|
30
30
|
|
|
31
|
-
// Web renders the add-on description via `dangerouslySetInnerHTML`. React Native
|
|
32
|
-
// has no HTML renderer, so we strip tags + decode a few common entities to show
|
|
33
|
-
// the description as plain text (the adaptation for native).
|
|
34
|
-
const stripHtml = (html: string): string =>
|
|
35
|
-
html
|
|
36
|
-
.replace(/<\s*br\s*\/?\s*>/gi, '\n')
|
|
37
|
-
.replace(/<\/\s*(p|div|li|h[1-6])\s*>/gi, '\n')
|
|
38
|
-
.replace(/<[^>]+>/g, '')
|
|
39
|
-
.replace(/ /gi, ' ')
|
|
40
|
-
.replace(/&/gi, '&')
|
|
41
|
-
.replace(/</gi, '<')
|
|
42
|
-
.replace(/>/gi, '>')
|
|
43
|
-
.replace(/'|'/gi, "'")
|
|
44
|
-
.replace(/"/gi, '"')
|
|
45
|
-
.replace(/\n{3,}/g, '\n\n')
|
|
46
|
-
.replace(/[ \t]+\n/g, '\n')
|
|
47
|
-
.trim()
|
|
48
|
-
|
|
49
31
|
const money = (currency: string, n: number): string =>
|
|
50
32
|
`${currency} ${Number(n).toFixed(2)}`
|
|
51
33
|
|
|
@@ -74,57 +56,27 @@ export const AddOns: FC<AddOnsProps> = ({
|
|
|
74
56
|
const checkout = useCheckoutContext()
|
|
75
57
|
const theme = useTheme()
|
|
76
58
|
const s = useAddonStyles(theme)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const [capture, setCapture] = useState<Record<string, CustomFieldValues>>(
|
|
93
|
-
checkout.addOnDataCapture ?? {}
|
|
94
|
-
)
|
|
95
|
-
const [error, setError] = useState('')
|
|
96
|
-
|
|
97
|
-
// Custom-field groups keyed by add-on id (scope 'add-on', group id = add-on id).
|
|
98
|
-
const fieldGroups = useMemo(() => {
|
|
99
|
-
const map: Record<string, (typeof checkout.customFields)[number]> = {}
|
|
100
|
-
for (const g of checkout.customFields) if (g.scope === 'add-on') map[g.id] = g
|
|
101
|
-
return map
|
|
102
|
-
}, [checkout.customFields])
|
|
103
|
-
|
|
104
|
-
// Combined mode: debounce-apply the selection to the cart (which reprices) as
|
|
105
|
-
// it changes, instead of gating behind a "confirm" button.
|
|
106
|
-
useEffect(() => {
|
|
107
|
-
if (!combined) return
|
|
108
|
-
const selection = Object.fromEntries(Object.entries(qty).filter(([, n]) => n > 0))
|
|
109
|
-
if (JSON.stringify(selection) === JSON.stringify(checkout.addOns ?? {})) return
|
|
110
|
-
const captureForSelected = Object.fromEntries(
|
|
111
|
-
Object.entries(capture).filter(([id]) => (selection[id] ?? 0) > 0)
|
|
112
|
-
)
|
|
113
|
-
const t = setTimeout(() => {
|
|
114
|
-
checkout.setAddonDataCapture(captureForSelected)
|
|
115
|
-
checkout.setAddons(selection).catch((e) => setError(errorMessage(e)))
|
|
116
|
-
}, 500)
|
|
117
|
-
return () => clearTimeout(t)
|
|
118
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
119
|
-
}, [combined, qty, capture])
|
|
120
|
-
|
|
121
|
-
const hasRequired = (checkout.requiredAddOns ?? []).some((a) => a.required)
|
|
59
|
+
const {
|
|
60
|
+
query: addonsQuery,
|
|
61
|
+
addons,
|
|
62
|
+
qty,
|
|
63
|
+
setQty,
|
|
64
|
+
capture,
|
|
65
|
+
setCapture,
|
|
66
|
+
error,
|
|
67
|
+
fieldGroups,
|
|
68
|
+
combined,
|
|
69
|
+
ticketQuantity,
|
|
70
|
+
hasRequired,
|
|
71
|
+
save,
|
|
72
|
+
skip,
|
|
73
|
+
} = hooks.useAddOnSelection(eventId, checkout, { onContinue, onSkip })
|
|
122
74
|
const head = (
|
|
123
75
|
<View style={s.head}>
|
|
124
76
|
{eyebrow ? <Text style={s.eyebrow}>{eyebrow}</Text> : null}
|
|
125
77
|
<Text style={s.heading}>{heading}</Text>
|
|
126
78
|
{onSkip && !hasRequired ? (
|
|
127
|
-
<Pressable accessibilityRole="button" onPress={
|
|
79
|
+
<Pressable accessibilityRole="button" onPress={skip} style={s.skipBtn}>
|
|
128
80
|
<Text style={s.skip}>Skip</Text>
|
|
129
81
|
</Pressable>
|
|
130
82
|
) : null}
|
|
@@ -141,6 +93,14 @@ export const AddOns: FC<AddOnsProps> = ({
|
|
|
141
93
|
</Card>
|
|
142
94
|
)
|
|
143
95
|
}
|
|
96
|
+
if (addonsQuery.isError)
|
|
97
|
+
return (
|
|
98
|
+
<Card>
|
|
99
|
+
{head}
|
|
100
|
+
<ErrorText>{errorMessage(addonsQuery.error)}</ErrorText>
|
|
101
|
+
<Button onPress={() => void addonsQuery.refetch()}>Try again</Button>
|
|
102
|
+
</Card>
|
|
103
|
+
)
|
|
144
104
|
if (addons.length === 0) return null
|
|
145
105
|
// A catalog containing only unavailable rows is not a useful checkout step.
|
|
146
106
|
// Required sold-out add-ons are already exempted by the engine preflight.
|
|
@@ -160,95 +120,86 @@ export const AddOns: FC<AddOnsProps> = ({
|
|
|
160
120
|
}
|
|
161
121
|
|
|
162
122
|
const missing = checkout.missingRequiredAddonNames
|
|
163
|
-
const save = () => {
|
|
164
|
-
setError('')
|
|
165
|
-
const selection = Object.fromEntries(Object.entries(qty).filter(([, n]) => n > 0))
|
|
166
|
-
// Only keep captured fields for add-ons that are actually selected.
|
|
167
|
-
const captureForSelected = Object.fromEntries(
|
|
168
|
-
Object.entries(capture).filter(([id]) => (selection[id] ?? 0) > 0)
|
|
169
|
-
)
|
|
170
|
-
checkout.setAddonDataCapture(captureForSelected)
|
|
171
|
-
checkout
|
|
172
|
-
.setAddons(selection)
|
|
173
|
-
.then(() => onContinue?.())
|
|
174
|
-
.catch((e) => setError(errorMessage(e)))
|
|
175
|
-
}
|
|
176
123
|
|
|
177
124
|
const selectedCount = Object.values(qty).filter((n) => n > 0).length
|
|
178
125
|
|
|
179
126
|
return (
|
|
180
|
-
<
|
|
181
|
-
|
|
182
|
-
|
|
127
|
+
<FrozenFields frozen={checkout.orderFixed}>
|
|
128
|
+
<Card>
|
|
129
|
+
{head}
|
|
130
|
+
<Text style={s.subtitle}>PLEASE SELECT FROM THE OPTIONAL ADD-ONS BELOW</Text>
|
|
183
131
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
132
|
+
{groups.map((g, gi) => (
|
|
133
|
+
<Fragment key={g.heading ?? `g${gi}`}>
|
|
134
|
+
{g.heading ? <Text style={s.group}>{g.heading}</Text> : null}
|
|
135
|
+
{g.items.map((a) => {
|
|
136
|
+
const n = qty[a.id] ?? 0
|
|
137
|
+
const group = fieldGroups[a.id]
|
|
138
|
+
const desc = a.description ? htmlToPlainText(a.description) : ''
|
|
139
|
+
return (
|
|
140
|
+
<View key={a.id} style={s.item}>
|
|
141
|
+
<View style={s.row}>
|
|
142
|
+
<View style={s.info}>
|
|
143
|
+
<Text style={s.name}>
|
|
144
|
+
{a.name}
|
|
145
|
+
{a.required ? <Text style={s.req}> · required</Text> : null}
|
|
146
|
+
</Text>
|
|
147
|
+
<Text style={s.price}>
|
|
148
|
+
{money(a.currency, a.price)}
|
|
149
|
+
{a.feeIncluded ? (
|
|
150
|
+
<Text style={s.priceNote}> incl. fees</Text>
|
|
151
|
+
) : null}
|
|
152
|
+
</Text>
|
|
153
|
+
{desc ? <Text style={s.desc}>{desc}</Text> : null}
|
|
154
|
+
</View>
|
|
155
|
+
{soldOut(a) ? (
|
|
156
|
+
<Text style={s.soldOut}>Sold out</Text>
|
|
157
|
+
) : (
|
|
158
|
+
<QuantitySelect
|
|
159
|
+
value={n}
|
|
160
|
+
min={0}
|
|
161
|
+
max={max(a)}
|
|
162
|
+
onChange={(v) => setQty((q) => ({ ...q, [a.id]: v }))}
|
|
163
|
+
/>
|
|
164
|
+
)}
|
|
206
165
|
</View>
|
|
207
|
-
{
|
|
208
|
-
<
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
166
|
+
{n > 0 && group ? (
|
|
167
|
+
<View style={s.fields}>
|
|
168
|
+
<CustomFieldGroupFields
|
|
169
|
+
group={group}
|
|
170
|
+
values={capture[a.id] ?? {}}
|
|
171
|
+
onChange={(name: string, val: string | string[]) =>
|
|
172
|
+
setCapture((c) => ({
|
|
173
|
+
...c,
|
|
174
|
+
[a.id]: { ...(c[a.id] ?? {}), [name]: val },
|
|
175
|
+
}))
|
|
176
|
+
}
|
|
177
|
+
/>
|
|
178
|
+
</View>
|
|
179
|
+
) : null}
|
|
217
180
|
</View>
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
values={capture[a.id] ?? {}}
|
|
223
|
-
onChange={(name: string, val: string | string[]) =>
|
|
224
|
-
setCapture((c) => ({
|
|
225
|
-
...c,
|
|
226
|
-
[a.id]: { ...(c[a.id] ?? {}), [name]: val },
|
|
227
|
-
}))
|
|
228
|
-
}
|
|
229
|
-
/>
|
|
230
|
-
</View>
|
|
231
|
-
) : null}
|
|
232
|
-
</View>
|
|
233
|
-
)
|
|
234
|
-
})}
|
|
235
|
-
</Fragment>
|
|
236
|
-
))}
|
|
181
|
+
)
|
|
182
|
+
})}
|
|
183
|
+
</Fragment>
|
|
184
|
+
))}
|
|
237
185
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
186
|
+
{!combined ? (
|
|
187
|
+
<Button
|
|
188
|
+
onPress={save}
|
|
189
|
+
loading={checkout.isBusy}
|
|
190
|
+
disabled={checkout.isBusy || selectedCount === 0}
|
|
191
|
+
>
|
|
192
|
+
{checkout.isBusy ? 'Updating…' : buttonLabel}
|
|
193
|
+
</Button>
|
|
194
|
+
) : null}
|
|
195
|
+
{missing.length > 0 ? (
|
|
196
|
+
<MutedText>Required add-on: {missing.join(', ')}.</MutedText>
|
|
197
|
+
) : null}
|
|
198
|
+
{error || checkout.error ? (
|
|
199
|
+
<ErrorText>{error || checkout.error}</ErrorText>
|
|
200
|
+
) : null}
|
|
201
|
+
</Card>
|
|
202
|
+
</FrozenFields>
|
|
252
203
|
)
|
|
253
204
|
}
|
|
254
205
|
|