tf-checkout-react-native 2.0.0-beta.0 → 2.0.0-beta.5
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 +92 -8
- package/dist/index.d.mts +7374 -288
- package/dist/index.d.ts +7374 -288
- package/dist/index.js +23316 -5980
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +23461 -6058
- package/dist/index.mjs.map +1 -1
- package/package.json +22 -10
- package/scripts/build-seat-map.mjs +31 -0
- package/scripts/seat-map-entry.jsx +22 -0
- package/src/CardInput.tsx +64 -3
- package/src/EmbeddedCheckout.tsx +129 -36
- package/src/auth/AuthModal.tsx +90 -6
- 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 +8 -1
- 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/AccountRewards.tsx +108 -0
- package/src/sections/AddOns.tsx +99 -148
- package/src/sections/BillingInfo.tsx +63 -64
- package/src/sections/Bundles.tsx +467 -0
- package/src/sections/EventDiscussionView.tsx +633 -0
- package/src/sections/Passes.tsx +177 -0
- package/src/sections/Payment.tsx +169 -52
- package/src/sections/PostReaction.tsx +122 -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/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/discussion/CommentImages.tsx +342 -0
- package/src/sections/discussion/SwipeReply.tsx +88 -0
- package/src/sections/index.ts +20 -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 +25 -13
- 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 +134 -0
- package/src/ui/QuantitySelect.tsx +26 -2
- package/src/ui/Select.tsx +4 -1
- package/src/ui/SignaturePad.tsx +218 -0
- package/src/ui/index.ts +1 -0
- package/src/ui/primitives.tsx +13 -2
- package/src/ui/useAccountT.ts +16 -0
- package/src/useCheckout.ts +3 -1
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import React, { useState, type FC } from 'react'
|
|
2
|
+
import { View } from 'react-native'
|
|
3
|
+
import { Button, ErrorText, MutedText } from '../ui/primitives'
|
|
4
|
+
import { Field, Input } from '../ui/Field'
|
|
5
|
+
export interface EmailCodeFormProps {
|
|
6
|
+
initialEmail: string
|
|
7
|
+
sent: boolean
|
|
8
|
+
target: string
|
|
9
|
+
retryAfter: number
|
|
10
|
+
loading: boolean
|
|
11
|
+
error: string
|
|
12
|
+
successMessage: string
|
|
13
|
+
onRequest: (email: string) => Promise<boolean>
|
|
14
|
+
onVerify: (code: string) => Promise<unknown>
|
|
15
|
+
onBack: () => void
|
|
16
|
+
}
|
|
17
|
+
export const EmailCodeForm: FC<EmailCodeFormProps> = ({
|
|
18
|
+
initialEmail,
|
|
19
|
+
sent,
|
|
20
|
+
target,
|
|
21
|
+
retryAfter,
|
|
22
|
+
loading,
|
|
23
|
+
error,
|
|
24
|
+
successMessage,
|
|
25
|
+
onRequest,
|
|
26
|
+
onVerify,
|
|
27
|
+
onBack,
|
|
28
|
+
}) => {
|
|
29
|
+
const [email, setEmail] = useState(initialEmail)
|
|
30
|
+
const [code, setCode] = useState('')
|
|
31
|
+
return (
|
|
32
|
+
<View style={{ gap: 14 }}>
|
|
33
|
+
<MutedText>
|
|
34
|
+
{sent
|
|
35
|
+
? `Enter the sign-in code sent to ${target}.`
|
|
36
|
+
: 'Receive a one-time sign-in code by email.'}
|
|
37
|
+
</MutedText>
|
|
38
|
+
{error ? <ErrorText>{error}</ErrorText> : null}
|
|
39
|
+
{successMessage ? <MutedText>{successMessage}</MutedText> : null}
|
|
40
|
+
<Field label="Email" required>
|
|
41
|
+
<Input
|
|
42
|
+
value={email}
|
|
43
|
+
onChangeText={setEmail}
|
|
44
|
+
keyboardType="email-address"
|
|
45
|
+
autoCapitalize="none"
|
|
46
|
+
editable={!sent && !loading}
|
|
47
|
+
accessibilityLabel="Email"
|
|
48
|
+
/>
|
|
49
|
+
</Field>
|
|
50
|
+
{sent ? (
|
|
51
|
+
<>
|
|
52
|
+
<Field label="Sign-in code" required>
|
|
53
|
+
<Input
|
|
54
|
+
value={code}
|
|
55
|
+
onChangeText={(value) => setCode(value.replace(/\D/g, '').slice(0, 6))}
|
|
56
|
+
accessibilityLabel="Sign-in code"
|
|
57
|
+
keyboardType="number-pad"
|
|
58
|
+
textContentType="oneTimeCode"
|
|
59
|
+
autoComplete="one-time-code"
|
|
60
|
+
maxLength={6}
|
|
61
|
+
autoFocus
|
|
62
|
+
editable={!loading}
|
|
63
|
+
/>
|
|
64
|
+
</Field>
|
|
65
|
+
<Button
|
|
66
|
+
loading={loading}
|
|
67
|
+
disabled={!/^\d{6}$/.test(code)}
|
|
68
|
+
onPress={() => void onVerify(code)}
|
|
69
|
+
>
|
|
70
|
+
Verify code
|
|
71
|
+
</Button>
|
|
72
|
+
</>
|
|
73
|
+
) : null}
|
|
74
|
+
<Button
|
|
75
|
+
loading={!sent && loading}
|
|
76
|
+
variant={sent ? 'ghost' : 'primary'}
|
|
77
|
+
disabled={loading || retryAfter > 0 || !email.trim()}
|
|
78
|
+
onPress={() => void onRequest(email)}
|
|
79
|
+
>
|
|
80
|
+
{retryAfter > 0
|
|
81
|
+
? `Send another code in ${retryAfter}s`
|
|
82
|
+
: sent
|
|
83
|
+
? 'Resend code'
|
|
84
|
+
: 'Send sign-in code'}
|
|
85
|
+
</Button>
|
|
86
|
+
<Button variant="ghost" disabled={loading} onPress={onBack}>
|
|
87
|
+
Use password instead
|
|
88
|
+
</Button>
|
|
89
|
+
</View>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
@@ -7,29 +7,38 @@
|
|
|
7
7
|
* `phone-pad` input (the US default still seeds it with "+1").
|
|
8
8
|
*/
|
|
9
9
|
import React, { useEffect, useMemo, useState, type FC } from 'react'
|
|
10
|
-
import { StyleSheet, View } from 'react-native'
|
|
10
|
+
import { Linking, Pressable, StyleSheet, View } from 'react-native'
|
|
11
11
|
|
|
12
12
|
import {
|
|
13
13
|
hooks,
|
|
14
|
+
createTranslator,
|
|
15
|
+
type LocaleCode,
|
|
14
16
|
type AuthFieldErrors,
|
|
15
17
|
type AuthFieldName,
|
|
16
18
|
type AuthRegistrationInput,
|
|
17
19
|
} from 'tf-checkout-shared'
|
|
18
20
|
|
|
19
|
-
import { Button, ErrorText } from '../ui/primitives'
|
|
21
|
+
import { Button, ErrorText, MutedText } from '../ui/primitives'
|
|
20
22
|
import { Field, Input } from '../ui/Field'
|
|
21
23
|
import { Select, type SelectOption } from '../ui/Select'
|
|
22
24
|
import { PasswordInput } from './PasswordInput'
|
|
23
25
|
|
|
24
26
|
export interface RegisterFormProps {
|
|
25
27
|
initialEmail?: string
|
|
28
|
+
locale?: LocaleCode
|
|
26
29
|
errors?: AuthFieldErrors
|
|
27
30
|
error?: string
|
|
28
31
|
loading?: boolean
|
|
29
32
|
requireAddress?: boolean
|
|
33
|
+
requireTerms?: boolean
|
|
34
|
+
completionRefresh?: {
|
|
35
|
+
retryAfter: number
|
|
36
|
+
onRequest: () => Promise<boolean>
|
|
37
|
+
onVerify: (code: string) => Promise<unknown>
|
|
38
|
+
}
|
|
30
39
|
onSubmit: (
|
|
31
40
|
input: AuthRegistrationInput,
|
|
32
|
-
options: { requireState: boolean }
|
|
41
|
+
options: { requireState: boolean; termsAccepted?: boolean }
|
|
33
42
|
) => void | Promise<unknown>
|
|
34
43
|
onFieldChange?: (field: AuthFieldName) => void
|
|
35
44
|
}
|
|
@@ -50,16 +59,23 @@ const emptyRegistration = (email: string): AuthRegistrationInput => ({
|
|
|
50
59
|
|
|
51
60
|
export const RegisterForm: FC<RegisterFormProps> = ({
|
|
52
61
|
initialEmail = '',
|
|
62
|
+
locale,
|
|
53
63
|
errors = {},
|
|
54
64
|
error,
|
|
55
65
|
loading = false,
|
|
56
66
|
requireAddress = true,
|
|
67
|
+
requireTerms = false,
|
|
68
|
+
completionRefresh,
|
|
57
69
|
onSubmit,
|
|
58
70
|
onFieldChange,
|
|
59
71
|
}) => {
|
|
72
|
+
const t = useMemo(() => createTranslator({ locale }), [locale])
|
|
60
73
|
const [values, setValues] = useState<AuthRegistrationInput>(() =>
|
|
61
74
|
emptyRegistration(initialEmail)
|
|
62
75
|
)
|
|
76
|
+
const [termsAccepted, setTermsAccepted] = useState(false)
|
|
77
|
+
const [refreshingProof, setRefreshingProof] = useState(false)
|
|
78
|
+
const [refreshCode, setRefreshCode] = useState('')
|
|
63
79
|
const countriesQuery = hooks.useCountries()
|
|
64
80
|
const countries = useMemo(() => countriesQuery.data ?? [], [countriesQuery.data])
|
|
65
81
|
const statesQuery = hooks.useStates(values.countryId, {
|
|
@@ -103,7 +119,8 @@ export const RegisterForm: FC<RegisterFormProps> = ({
|
|
|
103
119
|
[states]
|
|
104
120
|
)
|
|
105
121
|
|
|
106
|
-
const submit = () =>
|
|
122
|
+
const submit = () =>
|
|
123
|
+
void onSubmit(values, { requireState: states.length > 0, termsAccepted })
|
|
107
124
|
|
|
108
125
|
return (
|
|
109
126
|
<View style={styles.form}>
|
|
@@ -137,6 +154,7 @@ export const RegisterForm: FC<RegisterFormProps> = ({
|
|
|
137
154
|
<Field label="Email" required error={errors.email}>
|
|
138
155
|
<Input
|
|
139
156
|
value={values.email}
|
|
157
|
+
editable={!requireTerms}
|
|
140
158
|
onChangeText={(text) => setField('email', text)}
|
|
141
159
|
placeholder="Email"
|
|
142
160
|
keyboardType="email-address"
|
|
@@ -233,7 +251,75 @@ export const RegisterForm: FC<RegisterFormProps> = ({
|
|
|
233
251
|
/>
|
|
234
252
|
</Field>
|
|
235
253
|
|
|
236
|
-
|
|
254
|
+
{completionRefresh ? (
|
|
255
|
+
<View style={{ gap: 12 }}>
|
|
256
|
+
<Button
|
|
257
|
+
disabled={loading || completionRefresh.retryAfter > 0}
|
|
258
|
+
onPress={() => {
|
|
259
|
+
setRefreshingProof(true)
|
|
260
|
+
void completionRefresh.onRequest()
|
|
261
|
+
}}
|
|
262
|
+
>
|
|
263
|
+
{t('auth.completionVerifyAgain')}
|
|
264
|
+
{completionRefresh.retryAfter > 0
|
|
265
|
+
? ` (${completionRefresh.retryAfter}s)`
|
|
266
|
+
: ''}
|
|
267
|
+
</Button>
|
|
268
|
+
{refreshingProof ? (
|
|
269
|
+
<>
|
|
270
|
+
<Field label={t('auth.completionNewCode')}>
|
|
271
|
+
<Input
|
|
272
|
+
value={refreshCode}
|
|
273
|
+
keyboardType="number-pad"
|
|
274
|
+
textContentType="oneTimeCode"
|
|
275
|
+
maxLength={6}
|
|
276
|
+
onChangeText={(value) =>
|
|
277
|
+
setRefreshCode(value.replace(/\D/g, '').slice(0, 6))
|
|
278
|
+
}
|
|
279
|
+
/>
|
|
280
|
+
</Field>
|
|
281
|
+
<Button
|
|
282
|
+
disabled={loading || !/^\d{6}$/.test(refreshCode)}
|
|
283
|
+
onPress={() => void completionRefresh.onVerify(refreshCode)}
|
|
284
|
+
>
|
|
285
|
+
{t('auth.completionVerifyCode')}
|
|
286
|
+
</Button>
|
|
287
|
+
</>
|
|
288
|
+
) : null}
|
|
289
|
+
</View>
|
|
290
|
+
) : null}
|
|
291
|
+
{requireTerms ? (
|
|
292
|
+
<View style={{ gap: 8 }}>
|
|
293
|
+
<Pressable
|
|
294
|
+
accessibilityRole="checkbox"
|
|
295
|
+
accessibilityState={{ checked: termsAccepted }}
|
|
296
|
+
onPress={() => setTermsAccepted((current) => !current)}
|
|
297
|
+
>
|
|
298
|
+
<MutedText>
|
|
299
|
+
{termsAccepted ? '☑' : '☐'} {t('auth.completionAgree')}
|
|
300
|
+
</MutedText>
|
|
301
|
+
</Pressable>
|
|
302
|
+
<Pressable
|
|
303
|
+
accessibilityRole="link"
|
|
304
|
+
onPress={() =>
|
|
305
|
+
void Linking.openURL('https://www.ticketfairy.com/terms-of-service')
|
|
306
|
+
}
|
|
307
|
+
>
|
|
308
|
+
<MutedText>{t('auth.completionTerms')}</MutedText>
|
|
309
|
+
</Pressable>
|
|
310
|
+
<Pressable
|
|
311
|
+
accessibilityRole="link"
|
|
312
|
+
onPress={() => void Linking.openURL('https://www.ticketfairy.com/privacy')}
|
|
313
|
+
>
|
|
314
|
+
<MutedText>{t('auth.completionPrivacy')}</MutedText>
|
|
315
|
+
</Pressable>
|
|
316
|
+
</View>
|
|
317
|
+
) : null}
|
|
318
|
+
<Button
|
|
319
|
+
onPress={submit}
|
|
320
|
+
loading={loading}
|
|
321
|
+
disabled={requireTerms && !termsAccepted}
|
|
322
|
+
>
|
|
237
323
|
Create Account
|
|
238
324
|
</Button>
|
|
239
325
|
</View>
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import React, { useState, type FC } from 'react'
|
|
2
|
+
import { View } from 'react-native'
|
|
3
|
+
import { Button, ErrorText, MutedText } from '../ui/primitives'
|
|
4
|
+
import { Field, Input } from '../ui/Field'
|
|
5
|
+
|
|
6
|
+
export interface TwoFactorFormProps {
|
|
7
|
+
loading: boolean
|
|
8
|
+
error: string
|
|
9
|
+
successMessage: string
|
|
10
|
+
smsAvailable: boolean
|
|
11
|
+
smsRetryAfter: number
|
|
12
|
+
onSubmit: (code: string) => Promise<unknown>
|
|
13
|
+
onSendSms: () => Promise<boolean>
|
|
14
|
+
onCancel: () => Promise<void>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Presentation only; the shared headless flow owns the pending identity. */
|
|
18
|
+
export const TwoFactorForm: FC<TwoFactorFormProps> = ({
|
|
19
|
+
loading,
|
|
20
|
+
error,
|
|
21
|
+
successMessage,
|
|
22
|
+
smsAvailable,
|
|
23
|
+
smsRetryAfter,
|
|
24
|
+
onSubmit,
|
|
25
|
+
onSendSms,
|
|
26
|
+
onCancel,
|
|
27
|
+
}) => {
|
|
28
|
+
const [code, setCode] = useState('')
|
|
29
|
+
return (
|
|
30
|
+
<View style={{ gap: 14 }}>
|
|
31
|
+
<MutedText>
|
|
32
|
+
Enter the six-digit code from your authenticator app or registered phone to
|
|
33
|
+
complete sign-in.
|
|
34
|
+
</MutedText>
|
|
35
|
+
{error ? <ErrorText>{error}</ErrorText> : null}
|
|
36
|
+
{successMessage ? <MutedText>{successMessage}</MutedText> : null}
|
|
37
|
+
<Field label="Verification code" required>
|
|
38
|
+
<Input
|
|
39
|
+
accessibilityLabel="Verification code"
|
|
40
|
+
value={code}
|
|
41
|
+
onChangeText={(value) => setCode(value.replace(/\D/g, '').slice(0, 6))}
|
|
42
|
+
keyboardType="number-pad"
|
|
43
|
+
textContentType="oneTimeCode"
|
|
44
|
+
autoComplete="one-time-code"
|
|
45
|
+
maxLength={6}
|
|
46
|
+
autoFocus
|
|
47
|
+
editable={!loading}
|
|
48
|
+
onSubmitEditing={() => void onSubmit(code)}
|
|
49
|
+
/>
|
|
50
|
+
</Field>
|
|
51
|
+
<Button
|
|
52
|
+
loading={loading}
|
|
53
|
+
disabled={!/^\d{6}$/.test(code)}
|
|
54
|
+
onPress={() => void onSubmit(code)}
|
|
55
|
+
>
|
|
56
|
+
Verify code
|
|
57
|
+
</Button>
|
|
58
|
+
{smsAvailable ? (
|
|
59
|
+
<Button
|
|
60
|
+
variant="ghost"
|
|
61
|
+
disabled={loading || smsRetryAfter > 0}
|
|
62
|
+
onPress={() => void onSendSms()}
|
|
63
|
+
>
|
|
64
|
+
{smsRetryAfter > 0
|
|
65
|
+
? `Send another code in ${smsRetryAfter}s`
|
|
66
|
+
: 'Send a code by SMS'}
|
|
67
|
+
</Button>
|
|
68
|
+
) : null}
|
|
69
|
+
<Button variant="ghost" disabled={loading} onPress={() => void onCancel()}>
|
|
70
|
+
Use password again
|
|
71
|
+
</Button>
|
|
72
|
+
</View>
|
|
73
|
+
)
|
|
74
|
+
}
|
package/src/auth/index.ts
CHANGED
|
@@ -5,3 +5,7 @@ export { RegisterForm } from './RegisterForm'
|
|
|
5
5
|
export { ForgotPasswordForm } from './ForgotPasswordForm'
|
|
6
6
|
export { ResetPasswordForm } from './ResetPasswordForm'
|
|
7
7
|
export { PasswordInput } from './PasswordInput'
|
|
8
|
+
|
|
9
|
+
export { TwoFactorForm, type TwoFactorFormProps } from './TwoFactorForm'
|
|
10
|
+
|
|
11
|
+
export { EmailCodeForm, type EmailCodeFormProps } from './EmailCodeForm'
|
package/src/configure.ts
CHANGED
|
@@ -23,4 +23,11 @@ import { createMmkvStorage } from './storage'
|
|
|
23
23
|
export const mmkvStorageAdapter = createMmkvStorage()
|
|
24
24
|
|
|
25
25
|
export const configureCheckout = (config: TtfConfig): void =>
|
|
26
|
-
configure({
|
|
26
|
+
configure({
|
|
27
|
+
storage: mmkvStorageAdapter,
|
|
28
|
+
cookieAuth: true,
|
|
29
|
+
...config,
|
|
30
|
+
// NSURLSession/RCTNetworking would otherwise accept Set-Cookie outside
|
|
31
|
+
// the SDK epoch guard. Only the SDK's header-backed jar owns native auth.
|
|
32
|
+
withCredentials: false,
|
|
33
|
+
})
|
package/src/confirmer.ts
CHANGED
|
@@ -10,11 +10,8 @@
|
|
|
10
10
|
* flows from the mounted `<CardInput/>` (`<CardField/>`) into `confirmPayment`
|
|
11
11
|
* natively — there is no element to register the way web does.
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* / Stripe redirect methods (`providerPage`, `createRedirectPaymentMethod`) and
|
|
16
|
-
* Xendit tokenization (`tokenizeXenditCard`). They need `react-native-webview`,
|
|
17
|
-
* which this package injects rather than depends on; wiring them is a follow-up.
|
|
13
|
+
* CheckoutProvider owns provider-page windows for PayPal, Razorpay, Xendit and
|
|
14
|
+
* Stripe redirect rails. Imperative hosts supply their own providerPage port.
|
|
18
15
|
*/
|
|
19
16
|
import { Linking } from 'react-native'
|
|
20
17
|
import {
|
|
@@ -29,10 +26,14 @@ import {
|
|
|
29
26
|
type BillingDetails,
|
|
30
27
|
} from '@stripe/stripe-react-native'
|
|
31
28
|
|
|
32
|
-
import
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
29
|
+
import {
|
|
30
|
+
nativeStripeErrorOutcome,
|
|
31
|
+
confirmedStripeIntentStatus,
|
|
32
|
+
paymentProviderError,
|
|
33
|
+
type CheckoutBilling,
|
|
34
|
+
type CheckoutPorts,
|
|
35
|
+
type NativeStripeErrorLike,
|
|
36
|
+
type PaymentConfirmer,
|
|
36
37
|
} from 'tf-checkout-shared'
|
|
37
38
|
|
|
38
39
|
/** `CheckoutBilling` → the native SDK's `BillingDetails`. Mirrors the web
|
|
@@ -55,6 +56,18 @@ const toBillingDetails = (billing: CheckoutBilling): BillingDetails => ({
|
|
|
55
56
|
const failed = (message: string | undefined, fallback: string): Error =>
|
|
56
57
|
new Error(message || fallback)
|
|
57
58
|
|
|
59
|
+
/** A stripe-react-native error as the error the engine classifies: only an
|
|
60
|
+
* explicit decline or cancel lets the checkout reset
|
|
61
|
+
* (`nativeStripeErrorOutcome`). */
|
|
62
|
+
const stripeFailure = (
|
|
63
|
+
error: NativeStripeErrorLike & { message?: string },
|
|
64
|
+
fallback: string
|
|
65
|
+
): Error =>
|
|
66
|
+
paymentProviderError(error.message || fallback, nativeStripeErrorOutcome(error), {
|
|
67
|
+
type: error.type,
|
|
68
|
+
code: error.stripeErrorCode ?? error.code,
|
|
69
|
+
})
|
|
70
|
+
|
|
58
71
|
/**
|
|
59
72
|
* The confirmer every `useCheckout`/`<CardInput/>` binds to. `init()` is
|
|
60
73
|
* idempotent per key so an event on a connected Stripe account re-initializes
|
|
@@ -62,6 +75,8 @@ const failed = (message: string | undefined, fallback: string): Error =>
|
|
|
62
75
|
*/
|
|
63
76
|
export const createRnStripeConfirmer = (): PaymentConfirmer => {
|
|
64
77
|
let initKey: string | null = null
|
|
78
|
+
let accountKey: string | null = null
|
|
79
|
+
const listeners = new Set<() => void>()
|
|
65
80
|
|
|
66
81
|
return {
|
|
67
82
|
init: async ({ publishableKey, accountId, merchantIdentifier, urlScheme }) => {
|
|
@@ -74,6 +89,18 @@ export const createRnStripeConfirmer = (): PaymentConfirmer => {
|
|
|
74
89
|
...(urlScheme ? { urlScheme } : {}),
|
|
75
90
|
})
|
|
76
91
|
initKey = key
|
|
92
|
+
accountKey = `${publishableKey}:${accountId ?? ''}`
|
|
93
|
+
listeners.forEach((listener) => listener())
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
// `publishableKey:accountId`, the shape `bundleConfirmerKey` expects.
|
|
97
|
+
currentKey: () => accountKey,
|
|
98
|
+
|
|
99
|
+
subscribe: (listener) => {
|
|
100
|
+
listeners.add(listener)
|
|
101
|
+
return () => {
|
|
102
|
+
listeners.delete(listener)
|
|
103
|
+
}
|
|
77
104
|
},
|
|
78
105
|
|
|
79
106
|
// Native statuses are already the port's PascalCase vocabulary — no normalize.
|
|
@@ -87,8 +114,8 @@ export const createRnStripeConfirmer = (): PaymentConfirmer => {
|
|
|
87
114
|
paymentMethodType: 'Card',
|
|
88
115
|
paymentMethodData: { billingDetails: toBillingDetails(billing) },
|
|
89
116
|
})
|
|
90
|
-
if (error) throw
|
|
91
|
-
return paymentIntent
|
|
117
|
+
if (error) throw stripeFailure(error, 'Payment failed.')
|
|
118
|
+
return confirmedStripeIntentStatus(paymentIntent)
|
|
92
119
|
},
|
|
93
120
|
|
|
94
121
|
confirmApplePay: async (clientSecret, total, currencyCode, merchant) => {
|
|
@@ -105,8 +132,8 @@ export const createRnStripeConfirmer = (): PaymentConfirmer => {
|
|
|
105
132
|
],
|
|
106
133
|
},
|
|
107
134
|
})
|
|
108
|
-
if (error) throw
|
|
109
|
-
return paymentIntent
|
|
135
|
+
if (error) throw stripeFailure(error, 'Apple Pay failed.')
|
|
136
|
+
return confirmedStripeIntentStatus(paymentIntent)
|
|
110
137
|
},
|
|
111
138
|
|
|
112
139
|
confirmGooglePay: async (clientSecret, total, currencyCode, merchant) => {
|
|
@@ -118,8 +145,8 @@ export const createRnStripeConfirmer = (): PaymentConfirmer => {
|
|
|
118
145
|
merchantName: merchant.merchantName ?? 'Order',
|
|
119
146
|
},
|
|
120
147
|
})
|
|
121
|
-
if (error) throw
|
|
122
|
-
return paymentIntent
|
|
148
|
+
if (error) throw stripeFailure(error, 'Google Pay failed.')
|
|
149
|
+
return confirmedStripeIntentStatus(paymentIntent)
|
|
123
150
|
},
|
|
124
151
|
}
|
|
125
152
|
}
|
|
@@ -130,9 +157,9 @@ export const createRnStripeConfirmer = (): PaymentConfirmer => {
|
|
|
130
157
|
* `Linking`. WebView rails are intentionally absent (see file header).
|
|
131
158
|
*/
|
|
132
159
|
export const createRnPorts = (): Omit<CheckoutPorts, 'confirmer'> => ({
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
160
|
+
// The promise is handed back: a URL the device cannot open rejects, and the
|
|
161
|
+
// caller shows its failure (for example "We could not open the payment page").
|
|
162
|
+
openUrl: (url: string) => Linking.openURL(url).then(() => undefined),
|
|
136
163
|
|
|
137
164
|
createCardPaymentMethod: async (billing) => {
|
|
138
165
|
const { paymentMethod, error } = await createPaymentMethod({
|
|
@@ -143,9 +170,74 @@ export const createRnPorts = (): Omit<CheckoutPorts, 'confirmer'> => ({
|
|
|
143
170
|
return paymentMethod.id
|
|
144
171
|
},
|
|
145
172
|
|
|
173
|
+
createRedirectPaymentMethod: async (methodType, billing) => {
|
|
174
|
+
const details = toBillingDetails(billing)
|
|
175
|
+
const result = await (() => {
|
|
176
|
+
switch (methodType) {
|
|
177
|
+
case 'klarna':
|
|
178
|
+
return createPaymentMethod({
|
|
179
|
+
paymentMethodType: 'Klarna',
|
|
180
|
+
paymentMethodData: { billingDetails: details },
|
|
181
|
+
})
|
|
182
|
+
case 'afterpay_clearpay':
|
|
183
|
+
return createPaymentMethod({
|
|
184
|
+
paymentMethodType: 'AfterpayClearpay',
|
|
185
|
+
paymentMethodData: { billingDetails: details, shippingDetails: details },
|
|
186
|
+
})
|
|
187
|
+
case 'affirm':
|
|
188
|
+
return createPaymentMethod({
|
|
189
|
+
paymentMethodType: 'Affirm',
|
|
190
|
+
paymentMethodData: { billingDetails: details },
|
|
191
|
+
})
|
|
192
|
+
case 'ideal':
|
|
193
|
+
return createPaymentMethod({
|
|
194
|
+
paymentMethodType: 'Ideal',
|
|
195
|
+
paymentMethodData: { billingDetails: details },
|
|
196
|
+
})
|
|
197
|
+
case 'bancontact':
|
|
198
|
+
return createPaymentMethod({
|
|
199
|
+
paymentMethodType: 'Bancontact',
|
|
200
|
+
paymentMethodData: { billingDetails: details },
|
|
201
|
+
})
|
|
202
|
+
case 'eps':
|
|
203
|
+
return createPaymentMethod({
|
|
204
|
+
paymentMethodType: 'Eps',
|
|
205
|
+
paymentMethodData: { billingDetails: details },
|
|
206
|
+
})
|
|
207
|
+
case 'p24':
|
|
208
|
+
return createPaymentMethod({
|
|
209
|
+
paymentMethodType: 'P24',
|
|
210
|
+
paymentMethodData: { billingDetails: details },
|
|
211
|
+
})
|
|
212
|
+
case 'alipay':
|
|
213
|
+
return createPaymentMethod({ paymentMethodType: 'Alipay' })
|
|
214
|
+
case 'grabpay':
|
|
215
|
+
return createPaymentMethod({
|
|
216
|
+
paymentMethodType: 'GrabPay',
|
|
217
|
+
paymentMethodData: { billingDetails: details },
|
|
218
|
+
})
|
|
219
|
+
case 'cashapp':
|
|
220
|
+
return createPaymentMethod({
|
|
221
|
+
paymentMethodType: 'CashApp',
|
|
222
|
+
paymentMethodData: { billingDetails: details },
|
|
223
|
+
})
|
|
224
|
+
case 'paypal':
|
|
225
|
+
return createPaymentMethod({
|
|
226
|
+
paymentMethodType: 'PayPal',
|
|
227
|
+
paymentMethodData: { billingDetails: details },
|
|
228
|
+
})
|
|
229
|
+
default:
|
|
230
|
+
throw new Error('This payment method is not available on this device.')
|
|
231
|
+
}
|
|
232
|
+
})()
|
|
233
|
+
if (result.error || !result.paymentMethod?.id)
|
|
234
|
+
throw failed(result.error?.message, 'Unable to start this payment method.')
|
|
235
|
+
return result.paymentMethod.id
|
|
236
|
+
},
|
|
237
|
+
|
|
146
238
|
handleNextAction: async (clientSecret) => {
|
|
147
239
|
const { error } = await handleNextAction(clientSecret)
|
|
148
|
-
if (error) throw
|
|
240
|
+
if (error) throw stripeFailure(error, 'Payment failed')
|
|
149
241
|
},
|
|
150
242
|
|
|
151
243
|
confirmOxxo: async (clientSecret, billing) => {
|
|
@@ -175,6 +267,6 @@ export const createRnPorts = (): Omit<CheckoutPorts, 'confirmer'> => ({
|
|
|
175
267
|
paymentMethodType: 'Card',
|
|
176
268
|
paymentMethodData: { paymentMethodId },
|
|
177
269
|
})
|
|
178
|
-
if (error) throw
|
|
270
|
+
if (error) throw stripeFailure(error, 'Payment failed')
|
|
179
271
|
},
|
|
180
272
|
})
|