tf-checkout-react-native 2.0.0-beta.12 → 2.0.0-beta.14

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tf-checkout-react-native",
3
- "version": "2.0.0-beta.12",
3
+ "version": "2.0.0-beta.14",
4
4
  "description": "TheTicketFairy ReactNative Checkout library",
5
5
  "homepage": "https://github.com/theticketfairy/tf-shared-libraries#readme",
6
6
  "author": "The Ticket Fairy",
@@ -34,10 +34,13 @@
34
34
  "!**/__tests__",
35
35
  "!**/__fixtures__",
36
36
  "!**/__mocks__",
37
- "!**/.*"
37
+ "!**/.*",
38
+ "!scripts/verify-wallet-context.cjs",
39
+ "!scripts/verify-provider-uri.cjs",
40
+ "!scripts/verify-native-textarea.cjs"
38
41
  ],
39
42
  "scripts": {
40
- "test": "node scripts/verify-discussion-support.cjs && jest --passWithNoTests",
43
+ "test": "node scripts/verify-discussion-support.cjs && node scripts/verify-wallet-context.cjs && node scripts/verify-provider-uri.cjs && node scripts/verify-native-textarea.cjs && jest --passWithNoTests",
41
44
  "typecheck": "tsc -p tsconfig.build.json",
42
45
  "lint": "eslint \"**/*.{js,ts,tsx}\"",
43
46
  "lint:fix": "eslint \"**/*.{js,ts,tsx}\" --fix",
@@ -157,7 +160,7 @@
157
160
  "react-native-mmkv": "^3.0.0",
158
161
  "react-native-webview": ">=13.15.0"
159
162
  },
160
- "//dependencies": "tf-checkout-shared is INLINED into this bundle (tsup noExternal), so its own runtime deps are ours too — 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.",
163
+ "//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.",
161
164
  "engines": {
162
165
  "node": ">=20.19.4"
163
166
  }
package/src/confirmer.ts CHANGED
@@ -30,6 +30,7 @@ import {
30
30
  nativeStripeErrorOutcome,
31
31
  confirmedStripeIntentStatus,
32
32
  paymentProviderError,
33
+ stripeWalletPaymentContext,
33
34
  type CheckoutBilling,
34
35
  type CheckoutPorts,
35
36
  type NativeStripeErrorLike,
@@ -119,14 +120,24 @@ export const createRnStripeConfirmer = (): PaymentConfirmer => {
119
120
  },
120
121
 
121
122
  confirmApplePay: async (clientSecret, total, currencyCode, merchant) => {
123
+ const context = stripeWalletPaymentContext(
124
+ total,
125
+ currencyCode,
126
+ merchant.merchantCountryCode
127
+ )
128
+ if (!context)
129
+ throw paymentProviderError(
130
+ 'The order wallet details are unavailable.',
131
+ 'notStarted'
132
+ )
122
133
  const { error, paymentIntent } = await confirmPlatformPayPayment(clientSecret, {
123
134
  applePay: {
124
- merchantCountryCode: merchant.merchantCountryCode ?? 'US',
125
- currencyCode,
135
+ merchantCountryCode: context.merchantCountry,
136
+ currencyCode: context.currency,
126
137
  cartItems: [
127
138
  {
128
139
  label: merchant.merchantName ?? 'Order',
129
- amount: total,
140
+ amount: context.amount,
130
141
  paymentType: PlatformPay.PaymentType.Immediate,
131
142
  },
132
143
  ],
@@ -137,11 +148,21 @@ export const createRnStripeConfirmer = (): PaymentConfirmer => {
137
148
  },
138
149
 
139
150
  confirmGooglePay: async (clientSecret, total, currencyCode, merchant) => {
151
+ const context = stripeWalletPaymentContext(
152
+ total,
153
+ currencyCode,
154
+ merchant.merchantCountryCode
155
+ )
156
+ if (!context)
157
+ throw paymentProviderError(
158
+ 'The order wallet details are unavailable.',
159
+ 'notStarted'
160
+ )
140
161
  const { error, paymentIntent } = await confirmPlatformPayPayment(clientSecret, {
141
162
  googlePay: {
142
163
  testEnv: merchant.testEnv === true,
143
- merchantCountryCode: merchant.merchantCountryCode ?? 'US',
144
- currencyCode,
164
+ merchantCountryCode: context.merchantCountry,
165
+ currencyCode: context.currency,
145
166
  merchantName: merchant.merchantName ?? 'Order',
146
167
  },
147
168
  })
@@ -152,16 +152,18 @@ export const AddOns: FC<AddOnsProps> = ({
152
152
  </Text>
153
153
  {desc ? <Text style={s.desc}>{desc}</Text> : null}
154
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
- )}
155
+ <View style={s.quantity}>
156
+ {soldOut(a) ? (
157
+ <Text style={s.soldOut}>Sold out</Text>
158
+ ) : (
159
+ <QuantitySelect
160
+ value={n}
161
+ min={0}
162
+ max={max(a)}
163
+ onChange={(v) => setQty((q) => ({ ...q, [a.id]: v }))}
164
+ />
165
+ )}
166
+ </View>
165
167
  </View>
166
168
  {n > 0 && group ? (
167
169
  <View style={s.fields}>
@@ -223,7 +225,14 @@ const useAddonStyles = (theme: ResolvedTheme) =>
223
225
  fontWeight: '700',
224
226
  fontFamily: theme.fontFamily,
225
227
  },
226
- skipBtn: { paddingHorizontal: 4, paddingVertical: 2 },
228
+ skipBtn: {
229
+ maxWidth: '100%',
230
+ minWidth: 44,
231
+ minHeight: 44,
232
+ paddingHorizontal: 8,
233
+ paddingVertical: 10,
234
+ justifyContent: 'center',
235
+ },
227
236
  skip: {
228
237
  color: theme.accent,
229
238
  fontSize: theme.fontSize - 1,
@@ -247,8 +256,14 @@ const useAddonStyles = (theme: ResolvedTheme) =>
247
256
  fontFamily: theme.fontFamily,
248
257
  },
249
258
  item: { gap: 10 },
250
- row: { flexDirection: 'row', alignItems: 'flex-start', gap: 12 },
251
- info: { flex: 1, gap: 2 },
259
+ row: {
260
+ flexDirection: 'row',
261
+ flexWrap: 'wrap',
262
+ alignItems: 'flex-start',
263
+ gap: 16,
264
+ },
265
+ info: { flexGrow: 1, flexShrink: 1, flexBasis: 220, gap: 8 },
266
+ quantity: { marginStart: 'auto', maxWidth: '100%' },
252
267
  name: {
253
268
  color: theme.text,
254
269
  fontSize: theme.fontSize,
@@ -489,7 +489,11 @@ export const BillingInfo: FC<BillingInfoProps> = ({
489
489
  {onLogin && !checkout.isLoggedIn ? (
490
490
  <View style={s.loginRow}>
491
491
  <Text style={s.note}>Login &amp; skip ahead:</Text>
492
- <Pressable onPress={() => onLogin()}>
492
+ <Pressable
493
+ accessibilityRole="button"
494
+ style={s.loginAction}
495
+ onPress={() => onLogin()}
496
+ >
493
497
  <Text style={s.link}>Login</Text>
494
498
  </Pressable>
495
499
  </View>
@@ -740,7 +744,20 @@ const useStyles = (theme: ResolvedTheme) =>
740
744
  useMemo(
741
745
  () =>
742
746
  StyleSheet.create({
743
- loginRow: { flexDirection: 'row', alignItems: 'center', gap: 8 },
747
+ loginRow: {
748
+ flexDirection: 'row',
749
+ flexWrap: 'wrap',
750
+ alignItems: 'center',
751
+ gap: 12,
752
+ },
753
+ loginAction: {
754
+ maxWidth: '100%',
755
+ minWidth: 44,
756
+ minHeight: 44,
757
+ paddingHorizontal: 8,
758
+ paddingVertical: 10,
759
+ justifyContent: 'center',
760
+ },
744
761
  note: {
745
762
  color: theme.muted,
746
763
  fontSize: theme.fontSize - 1,
@@ -765,8 +782,8 @@ const useStyles = (theme: ResolvedTheme) =>
765
782
  fontSize: theme.fontSize - 1,
766
783
  fontFamily: theme.fontFamily,
767
784
  },
768
- row: { flexDirection: 'row', gap: 10 },
769
- col: { flex: 1 },
785
+ row: { flexDirection: 'row', flexWrap: 'wrap', gap: 12 },
786
+ col: { flexGrow: 1, flexShrink: 1, flexBasis: 180 },
770
787
  }),
771
788
  [theme]
772
789
  )
@@ -397,6 +397,7 @@ export const Tickets: FC<TicketsProps> = ({
397
397
  {t.descriptionRich ? (
398
398
  <Pressable
399
399
  accessibilityRole="button"
400
+ style={s.textAction}
400
401
  onPress={() =>
401
402
  setVisibleDescription((cur) => (cur === t.id ? null : t.id))
402
403
  }
@@ -469,6 +470,7 @@ export const Tickets: FC<TicketsProps> = ({
469
470
  ) : (
470
471
  <Pressable
471
472
  accessibilityRole="button"
473
+ style={s.textAction}
472
474
  onPress={() => setWlOpen((w) => ({ ...w, [t.id]: true }))}
473
475
  >
474
476
  <Text style={s.wlToggle}>Join the waiting list</Text>
@@ -557,7 +559,11 @@ export const Tickets: FC<TicketsProps> = ({
557
559
  {showCodeInput ? (
558
560
  <View style={s.promo}>
559
561
  {!isAccess && !promoOpen ? (
560
- <Pressable accessibilityRole="button" onPress={() => setPromoOpen(true)}>
562
+ <Pressable
563
+ accessibilityRole="button"
564
+ style={s.textAction}
565
+ onPress={() => setPromoOpen(true)}
566
+ >
561
567
  <Text style={s.promoToggle}>Got a promo code? Tap here</Text>
562
568
  </Pressable>
563
569
  ) : (
@@ -672,10 +678,25 @@ const useTicketStyles = (theme: ResolvedTheme) =>
672
678
  marginTop: 4,
673
679
  fontFamily: theme.fontFamily,
674
680
  },
675
- rowWrap: { gap: 8 },
676
- row: { flexDirection: 'row', alignItems: 'flex-start', gap: 12 },
677
- info: { flex: 1, gap: 4 },
678
- nameRow: { flexDirection: 'row', alignItems: 'center', flexWrap: 'wrap', gap: 8 },
681
+ rowWrap: { gap: 12 },
682
+ // Keep a readable text column; a narrow host moves controls to the next line.
683
+ row: {
684
+ flexDirection: 'row',
685
+ flexWrap: 'wrap',
686
+ alignItems: 'flex-start',
687
+ gap: 16,
688
+ },
689
+ info: { flexGrow: 1, flexShrink: 1, flexBasis: 220, gap: 8 },
690
+ nameRow: { gap: 4 },
691
+ textAction: {
692
+ alignSelf: 'flex-start',
693
+ maxWidth: '100%',
694
+ minWidth: 44,
695
+ minHeight: 44,
696
+ paddingHorizontal: 8,
697
+ paddingVertical: 10,
698
+ justifyContent: 'center',
699
+ },
679
700
  name: {
680
701
  color: theme.text,
681
702
  fontSize: theme.fontSize,
@@ -692,7 +713,7 @@ const useTicketStyles = (theme: ResolvedTheme) =>
692
713
  flexDirection: 'row',
693
714
  alignItems: 'center',
694
715
  flexWrap: 'wrap',
695
- gap: 6,
716
+ gap: 8,
696
717
  },
697
718
  oldPrice: {
698
719
  color: theme.muted,
@@ -726,12 +747,22 @@ const useTicketStyles = (theme: ResolvedTheme) =>
726
747
  fontFamily: theme.fontFamily,
727
748
  },
728
749
  soldout: {
750
+ marginStart: 'auto',
751
+ maxWidth: '100%',
729
752
  color: theme.muted,
730
753
  fontSize: theme.fontSize - 1,
731
754
  fontWeight: '600',
732
755
  fontFamily: theme.fontFamily,
733
756
  },
734
- qtyWrap: { flexDirection: 'row', alignItems: 'center', gap: 8 },
757
+ qtyWrap: {
758
+ flexDirection: 'row',
759
+ flexWrap: 'wrap',
760
+ maxWidth: '100%',
761
+ alignItems: 'center',
762
+ justifyContent: 'flex-end',
763
+ gap: 12,
764
+ marginStart: 'auto',
765
+ },
735
766
  guests: {
736
767
  color: theme.muted,
737
768
  fontSize: theme.fontSize - 2,
@@ -741,6 +772,7 @@ const useTicketStyles = (theme: ResolvedTheme) =>
741
772
  description: {
742
773
  color: theme.muted,
743
774
  fontSize: theme.fontSize - 1,
775
+ lineHeight: theme.fontSize * 1.5,
744
776
  fontFamily: theme.fontFamily,
745
777
  },
746
778
  wlToggle: {
@@ -8,8 +8,7 @@
8
8
  * - `<select>` and `radio` (single choice) both render as the native `Select`
9
9
  * sheet — there is no native radio primitive.
10
10
  * - `select_multi` renders a column of native `Checkbox` rows.
11
- * - there is no multiline `Textarea` primitive, so `textarea` falls back to a
12
- * single-line `Input` (value parity preserved).
11
+ * - `textarea` uses the native multiline Input, preserving line breaks.
13
12
  */
14
13
  import React, { type FC } from 'react'
15
14
  import { Image, View } from 'react-native'
@@ -94,11 +93,15 @@ export const CustomFieldInput: FC<CustomFieldInputProps> = ({
94
93
 
95
94
  switch (field.type) {
96
95
  case 'textarea':
97
- // Native adaptation: no multiline Textarea primitive — single-line Input.
98
96
  return (
99
97
  <Field label={field.label} required={field.required} error={error}>
100
98
  {desc}
101
- <Input value={asString(value)} onChangeText={onChange} invalid={invalid} />
99
+ <Input
100
+ multiline
101
+ value={asString(value)}
102
+ onChangeText={onChange}
103
+ invalid={invalid}
104
+ />
102
105
  </Field>
103
106
  )
104
107
  case 'select':
@@ -35,9 +35,21 @@ const secureInitialUri = (uri: string): boolean => {
35
35
  if (hasUnsafeNativeUriCharacters(uri)) return false
36
36
  if (!/^https:\/\/[^/?#\s\\]+(?:[/?#][^\s\\]*)?$/i.test(uri)) return false
37
37
  try {
38
- const url = new URL(uri)
38
+ // RN's URL runtime exposes these fields, but its global type omits them.
39
+ // Check the runtime surface rather than assuming a host polyfill's shape.
40
+ const url: unknown = new URL(uri)
39
41
  return (
40
- url.protocol === 'https:' && Boolean(url.hostname) && !url.username && !url.password
42
+ url !== null &&
43
+ typeof url === 'object' &&
44
+ 'protocol' in url &&
45
+ url.protocol === 'https:' &&
46
+ 'hostname' in url &&
47
+ typeof url.hostname === 'string' &&
48
+ Boolean(url.hostname) &&
49
+ 'username' in url &&
50
+ url.username === '' &&
51
+ 'password' in url &&
52
+ url.password === ''
41
53
  )
42
54
  } catch {
43
55
  return false
@@ -105,9 +105,10 @@ const useQuantityStyles = (theme: ResolvedTheme) =>
105
105
  StyleSheet.create({
106
106
  stepper: { flexDirection: 'row', alignItems: 'center', gap: 14 },
107
107
  btn: {
108
- width: 36,
109
- height: 36,
110
- borderRadius: 18,
108
+ minWidth: 44,
109
+ minHeight: 44,
110
+ padding: 10,
111
+ borderRadius: 22,
111
112
  borderWidth: StyleSheet.hairlineWidth,
112
113
  borderColor: theme.border,
113
114
  alignItems: 'center',
@@ -191,6 +191,8 @@ export const useStyles = (theme: ResolvedTheme) =>
191
191
  buttonDisabled: { opacity: 0.5 },
192
192
  buttonPressed: { opacity: 0.85 },
193
193
  buttonLabel: {
194
+ maxWidth: '100%',
195
+ textAlign: 'center',
194
196
  color: theme.accentContrast,
195
197
  fontSize: theme.fontSize,
196
198
  fontWeight: '600',