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.
Files changed (71) hide show
  1. package/README.md +115 -24
  2. package/dist/index.d.mts +8195 -282
  3. package/dist/index.d.ts +8195 -282
  4. package/dist/index.js +32346 -7001
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +32586 -7128
  7. package/dist/index.mjs.map +1 -1
  8. package/package.json +23 -11
  9. package/scripts/build-seat-map.mjs +31 -0
  10. package/scripts/seat-map-entry.jsx +22 -0
  11. package/scripts/verify-discussion-support.cjs +131 -0
  12. package/src/CardInput.tsx +64 -3
  13. package/src/EmbeddedCheckout.tsx +140 -38
  14. package/src/auth/AuthModal.tsx +186 -9
  15. package/src/auth/EmailCodeForm.tsx +91 -0
  16. package/src/auth/RegisterForm.tsx +91 -5
  17. package/src/auth/TwoFactorForm.tsx +74 -0
  18. package/src/auth/index.ts +4 -0
  19. package/src/configure.ts +19 -14
  20. package/src/confirmer.ts +112 -20
  21. package/src/generated/seatMapWidget.ts +3 -0
  22. package/src/index.tsx +1 -1
  23. package/src/sections/Accommodation.tsx +57 -0
  24. package/src/sections/AccountDeletion.tsx +347 -0
  25. package/src/sections/AccountEmailEnrichment.tsx +122 -0
  26. package/src/sections/AccountInformation.tsx +139 -0
  27. package/src/sections/AccountPaymentPlan.tsx +105 -0
  28. package/src/sections/AccountRewards.tsx +108 -0
  29. package/src/sections/AddOns.tsx +99 -148
  30. package/src/sections/BillingInfo.tsx +80 -78
  31. package/src/sections/Bundles.tsx +467 -0
  32. package/src/sections/EventDiscussionView.tsx +655 -0
  33. package/src/sections/NotificationPreferences.tsx +244 -0
  34. package/src/sections/OrderSummary.tsx +29 -2
  35. package/src/sections/Passes.tsx +177 -0
  36. package/src/sections/Payment.tsx +170 -53
  37. package/src/sections/PhoneAuthentication.tsx +371 -0
  38. package/src/sections/PostReaction.tsx +122 -0
  39. package/src/sections/ProfileSettings.tsx +446 -0
  40. package/src/sections/Rewards.tsx +320 -0
  41. package/src/sections/SeatMap.tsx +225 -0
  42. package/src/sections/SeatsIoMap.tsx +110 -0
  43. package/src/sections/Streaming.tsx +278 -0
  44. package/src/sections/TicketChange.tsx +379 -0
  45. package/src/sections/TicketGift.tsx +206 -0
  46. package/src/sections/TicketRefund.tsx +104 -0
  47. package/src/sections/Tickets.tsx +289 -208
  48. package/src/sections/Trips.tsx +87 -0
  49. package/src/sections/WaiverSigning.tsx +429 -0
  50. package/src/sections/billing/PhoneIdentity.tsx +169 -0
  51. package/src/sections/billing/TicketHoldersCard.tsx +4 -0
  52. package/src/sections/discussion/CommentImages.tsx +366 -0
  53. package/src/sections/discussion/SwipeReply.tsx +88 -0
  54. package/src/sections/index.ts +46 -0
  55. package/src/sections/rewards/RewardsLadder.tsx +167 -0
  56. package/src/sections/rewards/RewardsTree.tsx +117 -0
  57. package/src/storage.ts +8 -9
  58. package/src/ui/CheckoutProvider.tsx +51 -15
  59. package/src/ui/CustomFields.tsx +48 -12
  60. package/src/ui/Field.tsx +21 -3
  61. package/src/ui/Frozen.tsx +22 -0
  62. package/src/ui/InstallmentPicker.tsx +15 -18
  63. package/src/ui/PaymentMethods.tsx +6 -3
  64. package/src/ui/ProviderPages.tsx +214 -0
  65. package/src/ui/QuantitySelect.tsx +28 -3
  66. package/src/ui/Select.tsx +4 -1
  67. package/src/ui/SignaturePad.tsx +218 -0
  68. package/src/ui/index.ts +3 -0
  69. package/src/ui/primitives.tsx +13 -2
  70. package/src/ui/useAccountT.ts +16 -0
  71. package/src/useCheckout.ts +3 -1
@@ -0,0 +1,244 @@
1
+ import React, { useEffect, useMemo, type ReactNode } from 'react'
2
+ import {
3
+ ActivityIndicator,
4
+ Pressable,
5
+ StyleSheet,
6
+ Text,
7
+ View,
8
+ type StyleProp,
9
+ type ViewStyle,
10
+ } from 'react-native'
11
+ import { hooks, type NotificationPreferencesFlow } from 'tf-checkout-shared'
12
+ import { useTheme } from '../ui/CheckoutProvider'
13
+ import { resolveTheme, type CheckoutTheme } from '../ui/theme'
14
+ import { useAccountT } from '../ui/useAccountT'
15
+
16
+ export interface NotificationPreferencesProps {
17
+ locale?: string
18
+ translate?: (english: string, values?: Record<string, string | number>) => string
19
+ showHeader?: boolean
20
+ onBack?: () => void
21
+ onSignIn?: () => void
22
+ onSaved?: () => void
23
+ theme?: CheckoutTheme
24
+ style?: StyleProp<ViewStyle>
25
+ renderSaveButton?: (label: string, onPress: () => void, disabled: boolean) => ReactNode
26
+ }
27
+ export const NotificationPreferences = (props: NotificationPreferencesProps) => {
28
+ const flow = hooks.useNotificationPreferences()
29
+ return <NotificationPreferencesView {...props} preferences={flow} />
30
+ }
31
+ export const NotificationPreferencesView = ({
32
+ preferences: flow,
33
+ locale,
34
+ translate,
35
+ showHeader = true,
36
+ onBack,
37
+ onSignIn,
38
+ onSaved,
39
+ theme: override,
40
+ style,
41
+ renderSaveButton,
42
+ }: NotificationPreferencesProps & { preferences: NotificationPreferencesFlow }) => {
43
+ const defaultT = useAccountT(locale)
44
+ const t = translate ?? defaultT
45
+ const inherited = useTheme()
46
+ const theme = useMemo(
47
+ () => (override ? resolveTheme({ ...inherited, ...override }) : inherited),
48
+ [inherited, override]
49
+ )
50
+ const assertOwner = hooks.useSessionAction()
51
+ const act = (action?: () => unknown) => {
52
+ try {
53
+ assertOwner()
54
+ action?.()
55
+ } catch {
56
+ /* Retired account control. */
57
+ }
58
+ }
59
+ useEffect(() => {
60
+ if (flow.preferences?.available) flow.begin()
61
+ }, [flow.scopeKey, Boolean(flow.preferences), flow.preferences?.available])
62
+ const text = { color: theme.text, fontFamily: theme.fontFamily }
63
+ const button = (label: string, action: () => unknown, disabled = flow.busy) => (
64
+ <Pressable
65
+ accessibilityRole="button"
66
+ accessibilityLabel={t(label)}
67
+ accessibilityState={{ disabled, busy: flow.busy }}
68
+ disabled={disabled}
69
+ onPress={() => act(action)}
70
+ style={[s.button, { borderColor: theme.border }, disabled && s.disabled]}
71
+ >
72
+ <Text style={[text, { color: theme.accent }]}>{t(label)}</Text>
73
+ </Pressable>
74
+ )
75
+ if (flow.confirmingAccount || (flow.signedIn && flow.query.isPending))
76
+ return (
77
+ <ActivityIndicator
78
+ accessibilityLabel={t('Loading notification preferences…')}
79
+ color={theme.accent}
80
+ />
81
+ )
82
+ if (!flow.signedIn)
83
+ return (
84
+ <View style={[s.container, style]}>
85
+ {flow.admission.error && (
86
+ <>
87
+ <Text accessibilityRole="alert" style={{ color: theme.error }}>
88
+ {t(flow.admission.error.message)}
89
+ </Text>
90
+ {button('Try again', flow.retry, false)}
91
+ </>
92
+ )}
93
+ <Text style={text}>{t('Sign in to choose your event updates.')}</Text>
94
+ {onSignIn && button('Sign in', onSignIn, false)}
95
+ </View>
96
+ )
97
+ if (!flow.preferences || !flow.preferences.available)
98
+ return (
99
+ <View style={[s.container, style]}>
100
+ <Text accessibilityRole="alert" style={{ color: theme.error }}>
101
+ {t(
102
+ flow.query.error?.message ??
103
+ flow.preferences?.reason ??
104
+ 'Notification preferences are not available right now.'
105
+ )}
106
+ </Text>
107
+ {button('Try again', flow.retry, false)}
108
+ </View>
109
+ )
110
+ const save = async () => {
111
+ if (await flow.save()) act(onSaved)
112
+ }
113
+ const disabled = flow.busy || !flow.editor
114
+ const cancel = () => {
115
+ if (flow.cancel()) {
116
+ if (onBack) act(onBack)
117
+ else flow.begin()
118
+ }
119
+ }
120
+ return (
121
+ <View style={[s.container, style]}>
122
+ {showHeader && (
123
+ <Text accessibilityRole="header" style={[text, s.title]}>
124
+ {t('Notifications')}
125
+ </Text>
126
+ )}
127
+ <Text style={[text, s.description]}>
128
+ {t(
129
+ 'Choose which optional event recommendations you receive. Ticket and account messages stay the same.'
130
+ )}
131
+ </Text>
132
+ <View
133
+ accessibilityRole="radiogroup"
134
+ accessibilityLabel={t('Event updates')}
135
+ style={{ gap: 12 }}
136
+ >
137
+ {(
138
+ [
139
+ [
140
+ 'everything',
141
+ 'Everything',
142
+ 'Recommendations for any events that match your interests.',
143
+ ],
144
+ [
145
+ 'my_events',
146
+ 'My events only',
147
+ 'Recommendations for events you have tickets for or have saved.',
148
+ ],
149
+ ] as const
150
+ ).map(([value, label, description]) => (
151
+ <Pressable
152
+ key={value}
153
+ accessibilityRole="radio"
154
+ accessibilityLabel={t(label)}
155
+ accessibilityState={{ checked: flow.editor?.audience === value, disabled }}
156
+ disabled={disabled}
157
+ onPress={() => flow.choose(value)}
158
+ style={[s.choice, { borderColor: theme.border }, disabled && s.disabled]}
159
+ >
160
+ <View style={[s.radio, { borderColor: theme.accent }]}>
161
+ {flow.editor?.audience === value && (
162
+ <View style={[s.dot, { backgroundColor: theme.accent }]} />
163
+ )}
164
+ </View>
165
+ <View style={{ flex: 1, gap: 6 }}>
166
+ <Text style={text}>{t(label)}</Text>
167
+ <Text
168
+ style={{
169
+ color: theme.muted,
170
+ fontFamily: theme.fontFamily,
171
+ lineHeight: 21,
172
+ }}
173
+ >
174
+ {t(description)}
175
+ </Text>
176
+ </View>
177
+ </Pressable>
178
+ ))}
179
+ </View>
180
+ {flow.error && (
181
+ <>
182
+ <Text accessibilityRole="alert" style={{ color: theme.error }}>
183
+ {t(flow.error)}
184
+ </Text>
185
+ {button('Try again', flow.retry)}
186
+ </>
187
+ )}
188
+ {flow.saved && (
189
+ <Text accessibilityLiveRegion="polite" style={text}>
190
+ {t('Your notification preferences have been saved.')}
191
+ </Text>
192
+ )}
193
+ {renderSaveButton
194
+ ? renderSaveButton(
195
+ t('Save'),
196
+ () =>
197
+ act(() => {
198
+ void save()
199
+ }),
200
+ disabled
201
+ )
202
+ : button(
203
+ 'Save',
204
+ () => {
205
+ void save()
206
+ },
207
+ disabled
208
+ )}
209
+ {button('Cancel', cancel)}
210
+ </View>
211
+ )
212
+ }
213
+ const s = StyleSheet.create({
214
+ container: { gap: 20, paddingVertical: 16 },
215
+ title: { fontSize: 24, fontWeight: '600' },
216
+ description: { lineHeight: 24 },
217
+ choice: {
218
+ flexDirection: 'row',
219
+ alignItems: 'center',
220
+ gap: 16,
221
+ padding: 16,
222
+ minHeight: 72,
223
+ borderWidth: 1,
224
+ borderRadius: 16,
225
+ },
226
+ radio: {
227
+ width: 24,
228
+ height: 24,
229
+ borderRadius: 12,
230
+ borderWidth: 1,
231
+ alignItems: 'center',
232
+ justifyContent: 'center',
233
+ },
234
+ dot: { width: 14, height: 14, borderRadius: 7 },
235
+ button: {
236
+ minHeight: 48,
237
+ alignItems: 'center',
238
+ justifyContent: 'center',
239
+ borderWidth: 1,
240
+ borderRadius: 24,
241
+ padding: 12,
242
+ },
243
+ disabled: { opacity: 0.5 },
244
+ })
@@ -8,7 +8,14 @@
8
8
  import React, { type FC } from 'react'
9
9
  import { StyleSheet, View } from 'react-native'
10
10
 
11
- import { hooks } from 'tf-checkout-shared'
11
+ import {
12
+ hooks,
13
+ memberCreditRow,
14
+ createTranslator,
15
+ type LocaleCode,
16
+ type Messages,
17
+ type Translate,
18
+ } from 'tf-checkout-shared'
12
19
 
13
20
  import { BodyText, Card, Divider, MutedText, SectionHeader } from '../ui/primitives'
14
21
  import { useCheckoutContext, useTheme } from '../ui/CheckoutProvider'
@@ -22,15 +29,28 @@ const money = (symbol: string, code: string, amount: number): string =>
22
29
  export interface OrderSummaryProps {
23
30
  /** Event whose name to show; omit to skip the event line. */
24
31
  eventId?: string
32
+ locale?: LocaleCode
33
+ messages?: Partial<Messages>
34
+ translate?: Translate
25
35
  }
26
36
 
27
37
  /** Storefront-compatible order summary shown after the checkout controls. */
28
- export const OrderSummary: FC<OrderSummaryProps> = ({ eventId }) => {
38
+ export const OrderSummary: FC<OrderSummaryProps> = ({
39
+ eventId,
40
+ locale,
41
+ messages,
42
+ translate,
43
+ }) => {
29
44
  const checkout = useCheckoutContext()
30
45
  const theme = useTheme()
31
46
  const event = hooks.useEvent(eventId)
32
47
  const breakdown = checkout.breakdown
33
48
  const detailed = checkout.priceBreakdown
49
+ const memberCredit = memberCreditRow(
50
+ detailed,
51
+ breakdown,
52
+ (translate ?? createTranslator({ locale, messages }))('summary.memberCredit')
53
+ )
34
54
 
35
55
  if (!breakdown || breakdown.tickets.length === 0) return null
36
56
 
@@ -87,6 +107,13 @@ export const OrderSummary: FC<OrderSummaryProps> = ({ eventId }) => {
87
107
  </View>
88
108
  ) : null}
89
109
 
110
+ {memberCredit ? (
111
+ <View style={styles.totalRow}>
112
+ <BodyText>{memberCredit.name}</BodyText>
113
+ <BodyText style={styles.tabular}>{memberCredit.amount}</BodyText>
114
+ </View>
115
+ ) : null}
116
+
90
117
  <Divider />
91
118
  <View style={styles.totalRow}>
92
119
  <BodyText style={[styles.total, { fontSize: theme.fontSize + 1 }]}>
@@ -0,0 +1,177 @@
1
+ import React, { type FC } from 'react'
2
+ import { View } from 'react-native'
3
+ import { hooks, type PassesProps } from 'tf-checkout-shared'
4
+ import {
5
+ BodyText,
6
+ Button,
7
+ Card,
8
+ ErrorText,
9
+ Loader,
10
+ SectionHeader,
11
+ } from '../ui/primitives'
12
+ import { Input } from '../ui/Field'
13
+ import { useAccountT } from '../ui/useAccountT'
14
+
15
+ export const Passes: FC<PassesProps> = ({ onTicketsChanged, locale }) => {
16
+ const t = useAccountT(locale)
17
+ const flow = hooks.useCustomerPasses(onTicketsChanged)
18
+ const { state } = flow
19
+ if (state.command)
20
+ return (
21
+ <Card>
22
+ <SectionHeader
23
+ title={t(
24
+ state.command.kind === 'redeem'
25
+ ? 'Use an event credit'
26
+ : 'Cancel this booking'
27
+ )}
28
+ />
29
+ <BodyText>
30
+ {state.command.kind === 'redeem'
31
+ ? t('Use one event credit for {{event}}?', {
32
+ event: state.command.event.name,
33
+ })
34
+ : t('Cancel {{event}} and return its credit to your pass?', {
35
+ event: state.command.booking.event_name || t('this event'),
36
+ })}
37
+ </BodyText>
38
+ {state.error && <ErrorText>{t(state.error)}</ErrorText>}
39
+ <Button
40
+ loading={state.busy}
41
+ disabled={state.busy}
42
+ onPress={() => void flow.submit()}
43
+ >
44
+ {t(state.submitted ? 'Check my booking' : 'Confirm')}
45
+ </Button>
46
+ {!state.submitted && (
47
+ <Button variant="ghost" onPress={flow.back}>
48
+ {t('Back')}
49
+ </Button>
50
+ )}
51
+ </Card>
52
+ )
53
+ if (!state.passId)
54
+ return (
55
+ <View style={{ gap: 12 }}>
56
+ <SectionHeader title={t('My passes')} />
57
+ {flow.passes.isLoading && <Loader />}
58
+ {flow.passes.error && (
59
+ <>
60
+ <ErrorText>{flow.passes.error.message}</ErrorText>
61
+ <Button onPress={() => void flow.passes.refetch()}>{t('Try again')}</Button>
62
+ </>
63
+ )}
64
+ {flow.passes.data?.passes.length === 0 && (
65
+ <BodyText>{t('You do not have any event passes yet.')}</BodyText>
66
+ )}
67
+ {flow.passes.data?.passes.map((pass) => (
68
+ <Card key={pass.id}>
69
+ <SectionHeader title={pass.name || t('Event pass')} />
70
+ <BodyText>
71
+ {t('{{remaining}} of {{total}} event credits remaining', {
72
+ remaining: pass.credits_remaining,
73
+ total: pass.credits_total,
74
+ })}
75
+ </BodyText>
76
+ <BodyText>{t(pass.status)}</BodyText>
77
+ <Button onPress={() => flow.open(pass.id)}>{t('View pass')}</Button>
78
+ </Card>
79
+ ))}
80
+ {state.page > 1 && (
81
+ <Button variant="ghost" onPress={() => flow.page(state.page - 1)}>
82
+ {t('Previous page')}
83
+ </Button>
84
+ )}
85
+ {flow.passes.data?.meta.has_more && (
86
+ <Button onPress={() => flow.page(state.page + 1)}>{t('Next page')}</Button>
87
+ )}
88
+ </View>
89
+ )
90
+ const pass = flow.pass.data
91
+ return (
92
+ <View style={{ gap: 12 }}>
93
+ <Button variant="ghost" onPress={() => flow.open()}>
94
+ {t('Back to passes')}
95
+ </Button>
96
+ {flow.pass.isLoading && <Loader />}
97
+ {[flow.pass.error, flow.events.error, flow.bookings.error]
98
+ .filter(Boolean)
99
+ .map((error, index) => (
100
+ <ErrorText key={index}>{error!.message}</ErrorText>
101
+ ))}
102
+ {pass && (
103
+ <Card>
104
+ <SectionHeader title={pass.name || t('Event pass')} />
105
+ <BodyText>{pass.description || ''}</BodyText>
106
+ <BodyText>
107
+ {t('{{remaining}} of {{total}} event credits remaining', {
108
+ remaining: pass.credits_remaining,
109
+ total: pass.credits_total,
110
+ })}
111
+ </BodyText>
112
+ {pass.valid_until && (
113
+ <BodyText>{t('Valid until {{date}}', { date: pass.valid_until })}</BodyText>
114
+ )}
115
+ </Card>
116
+ )}
117
+ {state.message && <BodyText>{t(state.message)}</BodyText>}
118
+ <SectionHeader title={t('Your bookings')} />
119
+ {flow.bookings.data?.redemptions.length === 0 && (
120
+ <BodyText>{t('You have not booked any events with this pass yet.')}</BodyText>
121
+ )}
122
+ {flow.bookings.data?.redemptions.map((booking) => (
123
+ <Card key={booking.id}>
124
+ <BodyText>{booking.event_name || t('Event')}</BodyText>
125
+ <BodyText>{t(booking.status)}</BodyText>
126
+ {booking.can_cancel && (
127
+ <Button variant="ghost" onPress={() => flow.cancel(booking)}>
128
+ {t('Cancel booking')}
129
+ </Button>
130
+ )}
131
+ </Card>
132
+ ))}
133
+ {state.after && (
134
+ <Button variant="ghost" onPress={() => flow.bookingPage()}>
135
+ {t('First bookings')}
136
+ </Button>
137
+ )}
138
+ {flow.bookings.data?.next_cursor && (
139
+ <Button onPress={() => flow.bookingPage(flow.bookings.data!.next_cursor!)}>
140
+ {t('More bookings')}
141
+ </Button>
142
+ )}
143
+ <SectionHeader title={t('Choose an event')} />
144
+ <Input
145
+ value={state.search}
146
+ onChangeText={flow.search}
147
+ placeholder={t('Search events')}
148
+ />
149
+ {flow.events.isFetching && <Loader />}
150
+ {flow.events.data?.events.length === 0 && (
151
+ <BodyText>{t('No eligible events found.')}</BodyText>
152
+ )}
153
+ {flow.events.data?.events.map((event) => (
154
+ <Card key={event.event_id}>
155
+ <SectionHeader title={event.name} />
156
+ <BodyText>{event.start_date || ''}</BodyText>
157
+ <BodyText>{event.ticket_type_name || ''}</BodyText>
158
+ {event.available && pass?.status === 'active' && pass.credits_remaining > 0 ? (
159
+ <Button onPress={() => flow.choose(event)}>{t('Use one credit')}</Button>
160
+ ) : (
161
+ <BodyText>{t('Not available')}</BodyText>
162
+ )}
163
+ </Card>
164
+ ))}
165
+ {state.eventPage > 1 && (
166
+ <Button variant="ghost" onPress={() => flow.eventPage(state.eventPage - 1)}>
167
+ {t('Previous events')}
168
+ </Button>
169
+ )}
170
+ {flow.events.data?.meta.has_more && (
171
+ <Button onPress={() => flow.eventPage(state.eventPage + 1)}>
172
+ {t('More events')}
173
+ </Button>
174
+ )}
175
+ </View>
176
+ )
177
+ }