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.
Files changed (71) hide show
  1. package/README.md +115 -24
  2. package/dist/index.d.mts +8194 -282
  3. package/dist/index.d.ts +8194 -282
  4. package/dist/index.js +32308 -7001
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +32547 -7127
  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,214 @@
1
+ import React, { useEffect, useState } from 'react'
2
+ import {
3
+ ActivityIndicator,
4
+ Linking,
5
+ Modal,
6
+ SafeAreaView,
7
+ StyleSheet,
8
+ Text,
9
+ View,
10
+ } from 'react-native'
11
+ import { WebView } from 'react-native-webview'
12
+ import {
13
+ createProviderPages,
14
+ providerPageMatchesReturn,
15
+ providerPageOutcomeFromMessage,
16
+ type PendingProviderPage,
17
+ } from 'tf-checkout-shared'
18
+ import { Button, ErrorText } from './primitives'
19
+ import { useTheme } from './CheckoutProvider'
20
+ import { resolveTheme } from './theme'
21
+
22
+ const hasUnsafeNativeUriCharacters = (uri: string): boolean => {
23
+ for (const character of uri) {
24
+ const code = character.charCodeAt(0)
25
+ if (code <= 0x20 || code === 0x7f || character === '\\') return true
26
+ }
27
+ return false
28
+ }
29
+
30
+ /** Reject unsupported initial URI sources before WebView's native load path.
31
+ * Hostless sources can reach WKWebView.loadFileURL before navigation policy runs. */
32
+ const secureInitialUri = (uri: string): boolean => {
33
+ // URL parsers normalize scheme-only/slash variants that NSURL can treat as
34
+ // hostless file sources. Require the original string's HTTPS authority.
35
+ if (hasUnsafeNativeUriCharacters(uri)) return false
36
+ if (!/^https:\/\/[^/?#\s\\]+(?:[/?#][^\s\\]*)?$/i.test(uri)) return false
37
+ try {
38
+ const url = new URL(uri)
39
+ return (
40
+ url.protocol === 'https:' && Boolean(url.hostname) && !url.username && !url.password
41
+ )
42
+ } catch {
43
+ return false
44
+ }
45
+ }
46
+
47
+ /** Preserve provider app links without handing their hostless URL to WKWebView. */
48
+ const externalInitialUri = (uri: string): boolean => {
49
+ if (hasUnsafeNativeUriCharacters(uri)) return false
50
+ const match = /^([a-z][a-z\d+.-]*):.+$/i.exec(uri)
51
+ return Boolean(
52
+ match &&
53
+ !['http', 'https', 'about', 'file', 'javascript', 'data', 'blob'].includes(
54
+ match[1].toLowerCase()
55
+ )
56
+ )
57
+ }
58
+
59
+ /** Native window plumbing over the same return/message contract as web. */
60
+ export const useProviderPages = () => {
61
+ const [pending, setPending] = useState<PendingProviderPage | null>(null)
62
+ const [pages] = useState(() => createProviderPages(setPending))
63
+ useEffect(() => () => pages.cancel(), [pages])
64
+ useEffect(() => {
65
+ if (!pending) return
66
+ const subscription = Linking.addEventListener('url', ({ url }) => {
67
+ if (providerPageMatchesReturn(url, pending.request.returnUrlPrefix))
68
+ pages.settle(pending.id, { event: 'success', returnUrl: url })
69
+ })
70
+ return () => subscription.remove()
71
+ }, [pending, pages])
72
+ return {
73
+ providerPage: pages.open,
74
+ page: pending ? (
75
+ <ProviderPage key={pending.id} pending={pending} settle={pages.settle} />
76
+ ) : null,
77
+ }
78
+ }
79
+
80
+ const ProviderPage = ({
81
+ pending,
82
+ settle,
83
+ }: {
84
+ pending: PendingProviderPage
85
+ settle: ReturnType<typeof createProviderPages>['settle']
86
+ }) => {
87
+ const theme = useTheme()
88
+ const backdrop = resolveTheme({ colorScheme: theme.colorScheme }).background
89
+ const externalUri =
90
+ pending.request.uri && externalInitialUri(pending.request.uri)
91
+ ? pending.request.uri
92
+ : null
93
+ const sourceError =
94
+ pending.request.uri && !secureInitialUri(pending.request.uri) && !externalUri
95
+ ? 'This payment page could not be opened securely.'
96
+ : ''
97
+ const [error, setError] = useState('')
98
+ const displayedError = sourceError || error
99
+ const [reload, setReload] = useState(0)
100
+ useEffect(() => {
101
+ if (!externalUri) return
102
+ let active = true
103
+ void Linking.openURL(externalUri).catch(() => {
104
+ if (active)
105
+ setError(
106
+ 'Unable to open this payment app. Close this page to return to checkout and check the payment status.'
107
+ )
108
+ })
109
+ return () => {
110
+ active = false
111
+ }
112
+ }, [externalUri, reload])
113
+ const close = () => settle(pending.id, { event: 'dismiss' })
114
+ const source = pending.request.uri
115
+ ? { uri: pending.request.uri }
116
+ : { html: pending.request.html ?? '', baseUrl: pending.request.baseUrl }
117
+ return (
118
+ <Modal
119
+ visible
120
+ animationType="slide"
121
+ presentationStyle="fullScreen"
122
+ onRequestClose={close}
123
+ >
124
+ <View style={[styles.root, { backgroundColor: backdrop }]}>
125
+ <SafeAreaView style={[styles.root, { backgroundColor: theme.background }]}>
126
+ <View style={styles.toolbar}>
127
+ <Button variant="ghost" onPress={close}>
128
+ Close payment page
129
+ </Button>
130
+ </View>
131
+ {displayedError ? (
132
+ <View style={styles.failure}>
133
+ <ErrorText>{displayedError}</ErrorText>
134
+ {!sourceError ? (
135
+ <Button
136
+ onPress={() => {
137
+ setError('')
138
+ setReload((value) => value + 1)
139
+ }}
140
+ >
141
+ Try again
142
+ </Button>
143
+ ) : null}
144
+ </View>
145
+ ) : externalUri ? (
146
+ <View style={styles.external}>
147
+ <ActivityIndicator color={theme.accent} />
148
+ <Text style={{ color: theme.text, textAlign: 'center' }}>
149
+ Complete payment in your payment app, then return here.
150
+ </Text>
151
+ </View>
152
+ ) : (
153
+ <WebView
154
+ key={reload}
155
+ source={source}
156
+ style={styles.webview}
157
+ javaScriptEnabled
158
+ domStorageEnabled
159
+ incognito
160
+ sharedCookiesEnabled={false}
161
+ thirdPartyCookiesEnabled={false}
162
+ setSupportMultipleWindows={false}
163
+ originWhitelist={['*']}
164
+ startInLoadingState
165
+ renderLoading={() => (
166
+ <ActivityIndicator style={styles.loading} color={theme.accent} />
167
+ )}
168
+ onShouldStartLoadWithRequest={({ url }) => {
169
+ if (providerPageMatchesReturn(url, pending.request.returnUrlPrefix)) {
170
+ settle(pending.id, { event: 'success', returnUrl: url })
171
+ return false
172
+ }
173
+ if (/^(https:|about:)/i.test(url)) return true
174
+ if (/^(http:|javascript:|data:|file:)/i.test(url)) {
175
+ setError('This payment page could not be opened securely.')
176
+ return false
177
+ }
178
+ Linking.openURL(url).catch(() =>
179
+ setError(
180
+ 'Unable to open this payment app. Close this page to return to checkout and check the payment status.'
181
+ )
182
+ )
183
+ return false
184
+ }}
185
+ onMessage={({ nativeEvent }) => {
186
+ const outcome = providerPageOutcomeFromMessage(nativeEvent.data)
187
+ if (outcome) settle(pending.id, outcome)
188
+ }}
189
+ onError={() =>
190
+ setError(
191
+ 'The payment page could not load. Check your connection and try again.'
192
+ )
193
+ }
194
+ />
195
+ )}
196
+ </SafeAreaView>
197
+ </View>
198
+ </Modal>
199
+ )
200
+ }
201
+ const styles = StyleSheet.create({
202
+ root: { flex: 1 },
203
+ toolbar: { padding: 12 },
204
+ webview: { flex: 1 },
205
+ external: {
206
+ flex: 1,
207
+ padding: 24,
208
+ gap: 16,
209
+ justifyContent: 'center',
210
+ alignItems: 'center',
211
+ },
212
+ failure: { padding: 24, gap: 16 },
213
+ loading: { ...StyleSheet.absoluteFillObject },
214
+ })
@@ -10,6 +10,7 @@ import { Pressable, StyleSheet, Text, View } from 'react-native'
10
10
 
11
11
  import { useTheme } from './CheckoutProvider'
12
12
  import type { ResolvedTheme } from './theme'
13
+ import { useFrozen } from './Frozen'
13
14
 
14
15
  export interface QuantitySelectProps {
15
16
  value: number
@@ -20,6 +21,12 @@ export interface QuantitySelectProps {
20
21
  /** Step between quantities (the old checkout's `multiplier`). */
21
22
  step?: number
22
23
  disabled?: boolean
24
+ /**
25
+ * Accessible name, for example "Quantity of Weekend bundle". With a label
26
+ * the stepper is one adjustable control: a screen reader names it, reads
27
+ * the value and swipes up or down to change it.
28
+ */
29
+ label?: string
23
30
  }
24
31
 
25
32
  export const QuantitySelect: FC<QuantitySelectProps> = ({
@@ -28,15 +35,19 @@ export const QuantitySelect: FC<QuantitySelectProps> = ({
28
35
  min = 0,
29
36
  max = 10,
30
37
  step = 1,
31
- disabled = false,
38
+ disabled: disabledProp = false,
39
+ label,
32
40
  }) => {
41
+ const frozen = useFrozen()
42
+ const disabled = disabledProp || frozen
33
43
  const theme = useTheme()
34
44
  const s = useQuantityStyles(theme)
35
45
  // 0 (buy none) is always offered; the first non-zero step honors the type's
36
46
  // minimum, then steps up to the cap — mirroring the web dropdown's option grid
37
47
  // (0, lo, lo + step, …) so a min-2 type goes 0 → 2 → 3, never 0 → 1.
38
48
  const lo = Math.max(1, min)
39
- const cap = Math.min(max, 50)
49
+ const limit = Math.min(max, 50)
50
+ const cap = limit < lo ? 0 : lo + Math.floor((limit - lo) / step) * step
40
51
  const atZero = value <= 0
41
52
  const atCap = value >= cap
42
53
 
@@ -50,7 +61,21 @@ export const QuantitySelect: FC<QuantitySelectProps> = ({
50
61
  }
51
62
 
52
63
  return (
53
- <View style={s.stepper}>
64
+ <View
65
+ style={s.stepper}
66
+ accessible={Boolean(label)}
67
+ accessibilityRole={label ? 'adjustable' : undefined}
68
+ accessibilityLabel={label}
69
+ accessibilityValue={label ? { now: value, min: 0, max: cap } : undefined}
70
+ accessibilityState={label ? { disabled } : undefined}
71
+ accessibilityActions={
72
+ label ? [{ name: 'increment' }, { name: 'decrement' }] : undefined
73
+ }
74
+ onAccessibilityAction={(e) => {
75
+ if (e.nativeEvent.actionName === 'increment' && !atCap) inc()
76
+ if (e.nativeEvent.actionName === 'decrement' && !atZero) dec()
77
+ }}
78
+ >
54
79
  <Pressable
55
80
  accessibilityRole="button"
56
81
  accessibilityLabel="Remove one"
package/src/ui/Select.tsx CHANGED
@@ -9,6 +9,7 @@ import { FlatList, Modal, Pressable, StyleSheet, Text, View } from 'react-native
9
9
 
10
10
  import { useTheme } from './CheckoutProvider'
11
11
  import type { ResolvedTheme } from './theme'
12
+ import { useFrozen } from './Frozen'
12
13
 
13
14
  export interface SelectOption<T = string> {
14
15
  label: string
@@ -21,7 +22,7 @@ export function Select<T = string>({
21
22
  options,
22
23
  placeholder = 'Select…',
23
24
  invalid = false,
24
- disabled = false,
25
+ disabled: disabledProp = false,
25
26
  }: {
26
27
  value: T | null | undefined
27
28
  onChange: (value: T) => void
@@ -30,6 +31,8 @@ export function Select<T = string>({
30
31
  invalid?: boolean
31
32
  disabled?: boolean
32
33
  }) {
34
+ const frozen = useFrozen()
35
+ const disabled = disabledProp || frozen
33
36
  const theme = useTheme()
34
37
  const s = useSelectStyles(theme)
35
38
  const [open, setOpen] = useState(false)
@@ -0,0 +1,218 @@
1
+ /**
2
+ * `<SignaturePad/>` — the native drawing area for a drawn signature. Built
3
+ * from React Native's own `PanResponder` and `View`s, so it adds no native
4
+ * module to the consumer app (`react-native-svg` is not one of the SDK's
5
+ * peers). Each segment of a stroke renders as a thin, rotated `View`; when a
6
+ * stroke ends the pad reports a PNG data URL made by the shared core and the
7
+ * ink drawn so far (`signatureStrokeReport`). '' and 0 once cleared. A stroke
8
+ * the system interrupts (the app going to the background, an OS gesture)
9
+ * ends the same way as a released one, so what is saved is what is shown.
10
+ *
11
+ * Drawing stays smooth on long signatures: points live in a ref, a point
12
+ * closer than `MIN_STEP` to the last one is not kept, the pad re-renders at
13
+ * most once a frame, and a finished stroke never re-renders (`StrokeView`).
14
+ *
15
+ * The pad keeps the gesture inside a `ScrollView` (it refuses to hand the
16
+ * responder over), so drawing does not scroll the page.
17
+ */
18
+ import React, { memo, useEffect, useMemo, useRef, useState, type FC } from 'react'
19
+ import { PanResponder, Pressable, StyleSheet, Text, View } from 'react-native'
20
+ import {
21
+ SIGNATURE_INK,
22
+ SIGNATURE_PAPER,
23
+ signatureStrokeReport,
24
+ type SignatureStroke,
25
+ } from 'tf-checkout-shared'
26
+
27
+ import { useTheme } from './CheckoutProvider'
28
+
29
+ export interface SignaturePadProps {
30
+ onChange: (pngDataUrl: string, inkLength: number) => void
31
+ /** Accessible name of the drawing area. */
32
+ label: string
33
+ clearLabel: string
34
+ disabled?: boolean
35
+ }
36
+
37
+ const LINE_WIDTH = 2.5
38
+ const PAD_HEIGHT = 160
39
+ /** Pad points between kept points: finer moves add views, not detail. */
40
+ const MIN_STEP = 1.5
41
+
42
+ /** One stroke's segments. `points` changes only while the stroke is drawn. */
43
+ const StrokeView = memo<{ stroke: SignatureStroke; points: number }>(({ stroke }) =>
44
+ stroke.length === 1 ? (
45
+ <View
46
+ pointerEvents="none"
47
+ style={[
48
+ styles.dot,
49
+ { left: stroke[0].x - LINE_WIDTH / 2, top: stroke[0].y - LINE_WIDTH / 2 },
50
+ ]}
51
+ />
52
+ ) : (
53
+ <>
54
+ {stroke.slice(1).map((p, i) => {
55
+ const a = stroke[i]
56
+ const length = Math.hypot(p.x - a.x, p.y - a.y)
57
+ const angle = Math.atan2(p.y - a.y, p.x - a.x)
58
+ return (
59
+ <View
60
+ key={i}
61
+ pointerEvents="none"
62
+ style={[
63
+ styles.segment,
64
+ {
65
+ width: length + LINE_WIDTH,
66
+ left: (a.x + p.x) / 2 - (length + LINE_WIDTH) / 2,
67
+ top: (a.y + p.y) / 2 - LINE_WIDTH / 2,
68
+ transform: [{ rotate: `${angle}rad` }],
69
+ },
70
+ ]}
71
+ />
72
+ )
73
+ })}
74
+ </>
75
+ )
76
+ )
77
+ StrokeView.displayName = 'StrokeView'
78
+
79
+ export const SignaturePad: FC<SignaturePadProps> = ({
80
+ onChange,
81
+ label,
82
+ clearLabel,
83
+ disabled,
84
+ }) => {
85
+ const theme = useTheme()
86
+ const strokesRef = useRef<SignatureStroke[]>([])
87
+ // Bumped to re-render; the strokes themselves stay in the ref.
88
+ const [, setFrame] = useState(0)
89
+ const frameRequest = useRef<number | null>(null)
90
+ const size = useRef({ width: 0, height: PAD_HEIGHT })
91
+ const report = useRef(onChange)
92
+ report.current = onChange
93
+ const disabledRef = useRef(disabled)
94
+ disabledRef.current = disabled
95
+
96
+ const render = () => {
97
+ if (frameRequest.current !== null) cancelAnimationFrame(frameRequest.current)
98
+ frameRequest.current = null
99
+ setFrame((n) => n + 1)
100
+ }
101
+ const renderNextFrame = () => {
102
+ if (frameRequest.current !== null) return
103
+ frameRequest.current = requestAnimationFrame(render)
104
+ }
105
+ useEffect(
106
+ () => () => {
107
+ if (frameRequest.current !== null) cancelAnimationFrame(frameRequest.current)
108
+ },
109
+ []
110
+ )
111
+
112
+ // Paints every kept point first, so the report and the pad show the same.
113
+ const endStroke = () => {
114
+ render()
115
+ const ended = signatureStrokeReport(strokesRef.current, {
116
+ padWidth: size.current.width,
117
+ padHeight: size.current.height,
118
+ lineWidth: LINE_WIDTH,
119
+ })
120
+ if (ended) report.current(ended.pngDataUrl, ended.inkLength)
121
+ }
122
+
123
+ const responder = useMemo(
124
+ () =>
125
+ PanResponder.create({
126
+ onStartShouldSetPanResponder: () => !disabledRef.current,
127
+ onMoveShouldSetPanResponder: () => !disabledRef.current,
128
+ onPanResponderTerminationRequest: () => false,
129
+ onPanResponderGrant: (e) => {
130
+ const { locationX: x, locationY: y } = e.nativeEvent
131
+ strokesRef.current = [...strokesRef.current, [{ x, y }]]
132
+ render()
133
+ },
134
+ onPanResponderMove: (e) => {
135
+ const { locationX: x, locationY: y } = e.nativeEvent
136
+ const last = strokesRef.current[strokesRef.current.length - 1]
137
+ if (!last) return
138
+ const previous = last[last.length - 1]
139
+ if (Math.hypot(x - previous.x, y - previous.y) < MIN_STEP) return
140
+ last.push({ x, y })
141
+ renderNextFrame()
142
+ },
143
+ onPanResponderRelease: endStroke,
144
+ onPanResponderTerminate: endStroke,
145
+ }),
146
+ // eslint-disable-next-line react-hooks/exhaustive-deps
147
+ []
148
+ )
149
+
150
+ const clear = () => {
151
+ strokesRef.current = []
152
+ render()
153
+ report.current('', 0)
154
+ }
155
+
156
+ return (
157
+ <View style={styles.wrap}>
158
+ <View
159
+ accessible
160
+ accessibilityRole="image"
161
+ accessibilityLabel={label}
162
+ onLayout={(e) => {
163
+ size.current = {
164
+ width: e.nativeEvent.layout.width,
165
+ height: e.nativeEvent.layout.height,
166
+ }
167
+ }}
168
+ style={[
169
+ styles.pad,
170
+ {
171
+ borderColor: theme.border,
172
+ borderRadius: theme.radiusControl,
173
+ // The stored image is black on white, so the pad shows the same.
174
+ backgroundColor: SIGNATURE_PAPER,
175
+ },
176
+ ]}
177
+ {...responder.panHandlers}
178
+ >
179
+ {strokesRef.current.map((stroke, si) => (
180
+ <StrokeView key={si} stroke={stroke} points={stroke.length} />
181
+ ))}
182
+ </View>
183
+ <Pressable
184
+ accessibilityRole="button"
185
+ onPress={clear}
186
+ disabled={disabled}
187
+ style={styles.clear}
188
+ >
189
+ <Text style={{ color: theme.accent, fontFamily: theme.fontFamily }}>
190
+ {clearLabel}
191
+ </Text>
192
+ </Pressable>
193
+ </View>
194
+ )
195
+ }
196
+
197
+ const styles = StyleSheet.create({
198
+ wrap: { gap: 6 },
199
+ pad: {
200
+ height: PAD_HEIGHT,
201
+ borderWidth: 1,
202
+ overflow: 'hidden',
203
+ },
204
+ segment: {
205
+ position: 'absolute',
206
+ height: LINE_WIDTH,
207
+ borderRadius: LINE_WIDTH / 2,
208
+ backgroundColor: SIGNATURE_INK,
209
+ },
210
+ dot: {
211
+ position: 'absolute',
212
+ width: LINE_WIDTH,
213
+ height: LINE_WIDTH,
214
+ borderRadius: LINE_WIDTH / 2,
215
+ backgroundColor: SIGNATURE_INK,
216
+ },
217
+ clear: { alignSelf: 'flex-start', paddingVertical: 4 },
218
+ })
package/src/ui/index.ts CHANGED
@@ -2,12 +2,15 @@
2
2
  export * from './theme'
3
3
  export {
4
4
  CheckoutProvider,
5
+ CheckoutThemeProvider,
5
6
  useCheckoutContext,
6
7
  useCheckoutValidation,
7
8
  useTheme,
8
9
  type CheckoutProviderProps,
10
+ type CheckoutThemeProviderProps,
9
11
  } from './CheckoutProvider'
10
12
  export * from './primitives'
13
+ export * from './Frozen'
11
14
  export * from './Field'
12
15
  export * from './Select'
13
16
  export * from './QuantitySelect'
@@ -17,6 +17,7 @@ import {
17
17
 
18
18
  import { useTheme } from './CheckoutProvider'
19
19
  import type { ResolvedTheme } from './theme'
20
+ import { useFrozen } from './Frozen'
20
21
 
21
22
  /** Small spinner. */
22
23
  export const Loader: FC<{ size?: 'small' | 'large'; color?: string }> = ({
@@ -79,7 +80,16 @@ export const MutedText: FC<{ children: ReactNode; style?: StyleProp<TextStyle> }
79
80
  export const ErrorText: FC<{ children: ReactNode }> = ({ children }) => {
80
81
  const theme = useTheme()
81
82
  const s = useStyles(theme)
82
- return <Text style={s.errorText}>{children}</Text>
83
+ // Announced when it appears, like role="alert" on web.
84
+ return (
85
+ <Text
86
+ style={s.errorText}
87
+ accessibilityRole="alert"
88
+ accessibilityLiveRegion="assertive"
89
+ >
90
+ {children}
91
+ </Text>
92
+ )
83
93
  }
84
94
 
85
95
  export interface ButtonProps {
@@ -99,7 +109,8 @@ export const Button: FC<ButtonProps> = ({
99
109
  }) => {
100
110
  const theme = useTheme()
101
111
  const s = useStyles(theme)
102
- const isDisabled = disabled || loading
112
+ const frozen = useFrozen()
113
+ const isDisabled = disabled || loading || frozen
103
114
  const ghost = variant === 'ghost'
104
115
  return (
105
116
  <Pressable
@@ -0,0 +1,16 @@
1
+ import { useMemo } from 'react'
2
+ import {
3
+ createTranslator,
4
+ resolveLocale,
5
+ translateAccountMessage,
6
+ type TranslateVars,
7
+ } from 'tf-checkout-shared'
8
+
9
+ export const useAccountT = (locale?: string) => {
10
+ const translate = useMemo(
11
+ () => createTranslator({ locale: resolveLocale(locale) }),
12
+ [locale]
13
+ )
14
+ return (english: string, vars?: TranslateVars) =>
15
+ translateAccountMessage(translate, english, vars)
16
+ }
@@ -18,6 +18,7 @@ import {
18
18
  type UseCheckout,
19
19
  } from 'tf-checkout-shared'
20
20
 
21
+ import { claimDefaultCardField } from './CardInput'
21
22
  import { rnConfirmer, rnPorts } from './instance'
22
23
 
23
24
  export interface UseCheckoutOptions {
@@ -30,6 +31,7 @@ export interface UseCheckoutOptions {
30
31
 
31
32
  export const useCheckout = (opts: UseCheckoutOptions = {}): UseCheckout =>
32
33
  useSharedCheckout({
33
- ports: { confirmer: rnConfirmer, ...rnPorts, ...opts.ports },
34
+ // A Pay without its own claim uses the default ticket field (`checkout`).
35
+ ports: { confirmer: rnConfirmer, ...rnPorts, claimDefaultCardField, ...opts.ports },
34
36
  options: opts.options,
35
37
  })