@tagadapay/plugin-sdk 3.1.11 → 3.1.22
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 +1129 -1129
- package/build-cdn.js +231 -228
- package/dist/data/iso3166.d.ts +23 -33
- package/dist/data/iso3166.js +134 -198
- package/dist/data/languages.d.ts +5 -64
- package/dist/data/languages.js +23 -143
- package/dist/external-tracker.js +968 -102
- package/dist/external-tracker.min.js +2 -2
- package/dist/external-tracker.min.js.map +4 -4
- package/dist/react/hooks/useISOData.js +1 -1
- package/dist/react/hooks/usePaymentPolling.d.ts +3 -3
- package/dist/react/hooks/useShippingRates.d.ts +6 -0
- package/dist/react/hooks/useShippingRates.js +38 -0
- package/dist/react/providers/TagadaProvider.js +5 -5
- package/dist/react/services/apiService.d.ts +21 -0
- package/dist/react/services/apiService.js +10 -0
- package/dist/tagada-sdk.js +2079 -179
- package/dist/tagada-sdk.min.js +4 -2
- package/dist/tagada-sdk.min.js.map +4 -4
- package/dist/v2/core/client.d.ts +4 -2
- package/dist/v2/core/client.js +4 -3
- package/dist/v2/core/errors.d.ts +75 -0
- package/dist/v2/core/errors.js +104 -0
- package/dist/v2/core/funnelClient.d.ts +16 -15
- package/dist/v2/core/funnelClient.js +1 -1
- package/dist/v2/core/index.d.ts +1 -0
- package/dist/v2/core/index.js +2 -0
- package/dist/v2/core/pixelMapping.d.ts +49 -0
- package/dist/v2/core/pixelMapping.js +325 -0
- package/dist/v2/core/resources/apiClient.d.ts +2 -0
- package/dist/v2/core/resources/apiClient.js +52 -9
- package/dist/v2/core/resources/checkout.d.ts +89 -30
- package/dist/v2/core/resources/checkout.js +8 -0
- package/dist/v2/core/resources/customer.d.ts +20 -19
- package/dist/v2/core/resources/funnel.d.ts +17 -17
- package/dist/v2/core/resources/payments.d.ts +84 -13
- package/dist/v2/core/resources/payments.js +26 -9
- package/dist/v2/core/resources/shippingRates.d.ts +15 -0
- package/dist/v2/core/resources/shippingRates.js +11 -0
- package/dist/v2/core/types.d.ts +50 -12
- package/dist/v2/core/types.js +0 -3
- package/dist/v2/core/utils/checkout.d.ts +2 -2
- package/dist/v2/core/utils/checkout.js +7 -2
- package/dist/v2/core/utils/order.d.ts +11 -9
- package/dist/v2/core/utils/previewModeIndicator.js +101 -101
- package/dist/v2/index.d.ts +4 -2
- package/dist/v2/index.js +1 -1
- package/dist/v2/react/components/ApplePayButton.js +13 -4
- package/dist/v2/react/components/FunnelScriptInjector.js +51 -30
- package/dist/v2/react/components/WhopCheckout.d.ts +24 -0
- package/dist/v2/react/components/WhopCheckout.js +231 -0
- package/dist/v2/react/hooks/__examples__/FunnelContextExample.js +1 -1
- package/dist/v2/react/hooks/payment-actions/useAirwallexRadarAction.d.ts +14 -0
- package/dist/v2/react/hooks/payment-actions/useAirwallexRadarAction.js +181 -0
- package/dist/v2/react/hooks/payment-actions/useErrorAction.d.ts +9 -0
- package/dist/v2/react/hooks/payment-actions/useErrorAction.js +21 -0
- package/dist/v2/react/hooks/payment-actions/useFinixRadarAction.d.ts +14 -0
- package/dist/v2/react/hooks/payment-actions/useFinixRadarAction.js +187 -0
- package/dist/v2/react/hooks/payment-actions/useKessPayAction.d.ts +11 -0
- package/dist/v2/react/hooks/payment-actions/useKessPayAction.js +91 -0
- package/dist/v2/react/hooks/payment-actions/useMasterCardAction.d.ts +24 -0
- package/dist/v2/react/hooks/payment-actions/useMasterCardAction.js +221 -0
- package/dist/v2/react/hooks/payment-actions/usePaymentActionHandler.d.ts +15 -0
- package/dist/v2/react/hooks/payment-actions/usePaymentActionHandler.js +142 -0
- package/dist/v2/react/hooks/payment-actions/useProcessorAuthAction.d.ts +3 -0
- package/dist/v2/react/hooks/payment-actions/useProcessorAuthAction.js +13 -0
- package/dist/v2/react/hooks/payment-actions/useRedirectAction.d.ts +10 -0
- package/dist/v2/react/hooks/payment-actions/useRedirectAction.js +35 -0
- package/dist/v2/react/hooks/payment-actions/useStripeRadarAction.d.ts +14 -0
- package/dist/v2/react/hooks/payment-actions/useStripeRadarAction.js +192 -0
- package/dist/v2/react/hooks/payment-actions/useThreedsAuthAction.d.ts +14 -0
- package/dist/v2/react/hooks/payment-actions/useThreedsAuthAction.js +81 -0
- package/dist/v2/react/hooks/payment-actions/useTrustFlowAction.d.ts +11 -0
- package/dist/v2/react/hooks/payment-actions/useTrustFlowAction.js +84 -0
- package/dist/v2/react/hooks/payment-processing/usePaymentInstruments.d.ts +14 -0
- package/dist/v2/react/hooks/payment-processing/usePaymentInstruments.js +36 -0
- package/dist/v2/react/hooks/payment-processing/usePaymentProcessors.d.ts +31 -0
- package/dist/v2/react/hooks/payment-processing/usePaymentProcessors.js +212 -0
- package/dist/v2/react/hooks/payment-redirect/useAirwallex3dsReturn.d.ts +14 -0
- package/dist/v2/react/hooks/payment-redirect/useAirwallex3dsReturn.js +207 -0
- package/dist/v2/react/hooks/payment-redirect/useGenericPaymentReturn.d.ts +12 -0
- package/dist/v2/react/hooks/payment-redirect/useGenericPaymentReturn.js +101 -0
- package/dist/v2/react/hooks/useCheckoutQuery.d.ts +6 -0
- package/dist/v2/react/hooks/useCheckoutQuery.js +45 -0
- package/dist/v2/react/hooks/useFunnel.d.ts +1 -2
- package/dist/v2/react/hooks/useGeoLocation.d.ts +2 -1
- package/dist/v2/react/hooks/useGeoLocation.js +4 -2
- package/dist/v2/react/hooks/useGoogleAutocomplete.js +82 -33
- package/dist/v2/react/hooks/useISOData.js +1 -1
- package/dist/v2/react/hooks/usePaymentPolling.d.ts +3 -3
- package/dist/v2/react/hooks/usePaymentQuery.d.ts +18 -5
- package/dist/v2/react/hooks/usePaymentQuery.js +63 -1015
- package/dist/v2/react/hooks/usePaymentRetrieve.d.ts +3 -2
- package/dist/v2/react/hooks/usePaymentRetrieve.js +3 -1
- package/dist/v2/react/hooks/usePixelTracking.d.ts +5 -43
- package/dist/v2/react/hooks/usePixelTracking.js +213 -407
- package/dist/v2/react/hooks/useShippingRatesQuery.d.ts +6 -0
- package/dist/v2/react/hooks/useShippingRatesQuery.js +47 -4
- package/dist/v2/react/hooks/useStepConfig.d.ts +2 -8
- package/dist/v2/react/hooks/useStepConfig.js +1 -1
- package/dist/v2/react/hooks/useWhopPaymentPolling.d.ts +30 -0
- package/dist/v2/react/hooks/useWhopPaymentPolling.js +61 -0
- package/dist/v2/react/index.d.ts +7 -0
- package/dist/v2/react/index.js +4 -0
- package/dist/v2/react/providers/ExpressPaymentMethodsProvider.d.ts +2 -1
- package/dist/v2/react/providers/ExpressPaymentMethodsProvider.js +3 -1
- package/dist/v2/react/providers/TagadaProvider.js +76 -7
- package/dist/v2/standalone/external-tracker.d.ts +52 -46
- package/dist/v2/standalone/external-tracker.js +205 -98
- package/dist/v2/standalone/index.d.ts +22 -0
- package/dist/v2/standalone/index.js +125 -0
- package/package.json +112 -112
- package/dist/react/utils/__tests__/urlUtils.test.d.ts +0 -1
- package/dist/react/utils/__tests__/urlUtils.test.js +0 -189
- package/dist/v2/core/__tests__/pathRemapping.test.d.ts +0 -11
- package/dist/v2/core/__tests__/pathRemapping.test.js +0 -776
|
@@ -7,11 +7,16 @@ import { useBasisTheory } from '@basis-theory/basis-theory-react';
|
|
|
7
7
|
import { getBasisTheoryApiKey } from '../../../react/config/payment';
|
|
8
8
|
import { useTagadaContext } from '../providers/TagadaProvider';
|
|
9
9
|
import { PaymentsResource } from '../../core/resources/payments';
|
|
10
|
+
import { TagadaError, TagadaErrorCode } from '../../core/errors';
|
|
10
11
|
import { usePaymentPolling } from './usePaymentPolling';
|
|
11
12
|
import { useThreeds } from './useThreeds';
|
|
12
13
|
import { getGlobalApiClient } from './useApiQuery';
|
|
13
14
|
import { useStoreConfigQuery } from './useStoreConfigQuery';
|
|
14
|
-
import {
|
|
15
|
+
import { usePaymentActionHandler } from './payment-actions/usePaymentActionHandler';
|
|
16
|
+
import { useAirwallex3dsReturn } from './payment-redirect/useAirwallex3dsReturn';
|
|
17
|
+
import { useGenericPaymentReturn } from './payment-redirect/useGenericPaymentReturn';
|
|
18
|
+
import { usePaymentInstruments } from './payment-processing/usePaymentInstruments';
|
|
19
|
+
import { usePaymentProcessors } from './payment-processing/usePaymentProcessors';
|
|
15
20
|
export function usePaymentQuery(hookOptions) {
|
|
16
21
|
const { environment } = useTagadaContext();
|
|
17
22
|
// Get store config to auto-detect 3DS setting from payment flow
|
|
@@ -35,8 +40,6 @@ export function usePaymentQuery(hookOptions) {
|
|
|
35
40
|
}, [hookOptions]);
|
|
36
41
|
const { startPolling, stopPolling } = usePaymentPolling();
|
|
37
42
|
const { createSession, startChallenge } = useThreeds();
|
|
38
|
-
// Track challenge in progress to prevent multiple challenges
|
|
39
|
-
const challengeInProgressRef = useRef(false);
|
|
40
43
|
// Track if we've already processed a redirect return (prevents double-processing)
|
|
41
44
|
const redirectReturnProcessedRef = useRef(false);
|
|
42
45
|
// Get API key from embedded configuration with proper environment detection
|
|
@@ -60,1030 +63,75 @@ export function usePaymentQuery(hookOptions) {
|
|
|
60
63
|
stopPolling();
|
|
61
64
|
};
|
|
62
65
|
}, [stopPolling]);
|
|
63
|
-
//
|
|
64
|
-
const handlePaymentAction =
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
},
|
|
117
|
-
onFailure: async (errorMsg) => {
|
|
118
|
-
setError(errorMsg);
|
|
119
|
-
setIsLoading(false);
|
|
120
|
-
// Hook-level callback (universal handler)
|
|
121
|
-
if (hookOptionsRef.current?.onPaymentFailed) {
|
|
122
|
-
await hookOptionsRef.current.onPaymentFailed(errorMsg, {
|
|
123
|
-
isRedirectReturn: false,
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
// Legacy callback (backwards compatibility)
|
|
127
|
-
options.onFailure?.(errorMsg);
|
|
128
|
-
// Funnel-aligned callback (recommended)
|
|
129
|
-
options.onPaymentFailed?.({
|
|
130
|
-
code: 'PAYMENT_FAILED',
|
|
131
|
-
message: errorMsg,
|
|
132
|
-
payment,
|
|
133
|
-
});
|
|
134
|
-
},
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
catch (_error) {
|
|
139
|
-
challengeInProgressRef.current = false;
|
|
140
|
-
const errorMsg = _error instanceof Error ? _error.message : 'Failed to start 3DS challenge';
|
|
141
|
-
setError(errorMsg);
|
|
142
|
-
setIsLoading(false);
|
|
143
|
-
// Legacy callback (backwards compatibility)
|
|
144
|
-
options.onFailure?.(errorMsg);
|
|
145
|
-
// Funnel-aligned callback (recommended)
|
|
146
|
-
options.onPaymentFailed?.({
|
|
147
|
-
code: '3DS_CHALLENGE_FAILED',
|
|
148
|
-
message: errorMsg,
|
|
149
|
-
payment,
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
break;
|
|
154
|
-
case 'processor_auth': {
|
|
155
|
-
// Always auto-redirect for processor auth (e.g., 3DS, external payment flows)
|
|
156
|
-
if (actionData.metadata?.redirect?.redirectUrl) {
|
|
157
|
-
window.location.href = actionData.metadata.redirect.redirectUrl;
|
|
158
|
-
}
|
|
159
|
-
break;
|
|
160
|
-
}
|
|
161
|
-
case 'redirect': {
|
|
162
|
-
// For redirect type, let funnel orchestrator handle navigation via callbacks
|
|
163
|
-
// Only auto-redirect if explicitly enabled (disableAutoRedirect: false)
|
|
164
|
-
const shouldAutoRedirect = options.disableAutoRedirect === false;
|
|
165
|
-
if (shouldAutoRedirect && actionData.metadata?.redirect?.redirectUrl) {
|
|
166
|
-
window.location.href = actionData.metadata.redirect.redirectUrl;
|
|
167
|
-
}
|
|
168
|
-
else if (payment.status === 'succeeded') {
|
|
169
|
-
// Payment succeeded - call success callbacks for funnel navigation
|
|
170
|
-
setIsLoading(false);
|
|
171
|
-
const response = {
|
|
172
|
-
paymentId: payment.id,
|
|
173
|
-
payment,
|
|
174
|
-
// Extract order from payment if available (for funnel path resolution)
|
|
175
|
-
order: payment.order,
|
|
176
|
-
};
|
|
177
|
-
// Hook-level callback (universal handler)
|
|
178
|
-
if (hookOptionsRef.current?.onPaymentCompleted) {
|
|
179
|
-
await hookOptionsRef.current.onPaymentCompleted(payment, {
|
|
180
|
-
isRedirectReturn: false,
|
|
181
|
-
order: response.order,
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
// Legacy callback (backwards compatibility)
|
|
185
|
-
options.onSuccess?.(response);
|
|
186
|
-
// Funnel-aligned callback (recommended)
|
|
187
|
-
options.onPaymentSuccess?.(response);
|
|
188
|
-
}
|
|
189
|
-
break;
|
|
190
|
-
}
|
|
191
|
-
case 'error': {
|
|
192
|
-
const errorMsg = actionData.message || 'Payment processing failed';
|
|
193
|
-
const errorCode = actionData.errorCode || 'PAYMENT_FAILED';
|
|
194
|
-
setError(errorMsg);
|
|
195
|
-
setIsLoading(false);
|
|
196
|
-
// Legacy callback (backwards compatibility)
|
|
197
|
-
options.onFailure?.(errorMsg);
|
|
198
|
-
// Funnel-aligned callback (recommended)
|
|
199
|
-
options.onPaymentFailed?.({
|
|
200
|
-
code: errorCode,
|
|
201
|
-
message: errorMsg,
|
|
202
|
-
payment,
|
|
203
|
-
});
|
|
204
|
-
break;
|
|
205
|
-
}
|
|
206
|
-
case 'finix_radar': {
|
|
207
|
-
// Handle Finix fraud detection - collect device fingerprint
|
|
208
|
-
const radarConfig = actionData.metadata?.radar;
|
|
209
|
-
if (!radarConfig) {
|
|
210
|
-
console.error('Finix radar config missing from payment action');
|
|
211
|
-
break;
|
|
212
|
-
}
|
|
213
|
-
try {
|
|
214
|
-
// Dynamically load Finix SDK if not already loaded
|
|
215
|
-
if (typeof window !== 'undefined' && typeof window.Finix?.Auth !== 'function') {
|
|
216
|
-
const existingScript = document.querySelector('script[src="https://js.finix.com/v/1/finix.js"]');
|
|
217
|
-
if (!existingScript) {
|
|
218
|
-
const script = document.createElement('script');
|
|
219
|
-
script.src = 'https://js.finix.com/v/1/finix.js';
|
|
220
|
-
script.async = true;
|
|
221
|
-
document.head.appendChild(script);
|
|
222
|
-
await new Promise((resolve, reject) => {
|
|
223
|
-
script.onload = () => {
|
|
224
|
-
console.log('Finix SDK loaded successfully');
|
|
225
|
-
resolve();
|
|
226
|
-
};
|
|
227
|
-
script.onerror = () => reject(new Error('Failed to load Finix SDK'));
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
|
-
else {
|
|
231
|
-
// Wait for existing script to load
|
|
232
|
-
await new Promise((resolve, reject) => {
|
|
233
|
-
const checkFinix = () => {
|
|
234
|
-
if (typeof window.Finix?.Auth === 'function') {
|
|
235
|
-
resolve();
|
|
236
|
-
}
|
|
237
|
-
else {
|
|
238
|
-
setTimeout(checkFinix, 100);
|
|
239
|
-
}
|
|
240
|
-
};
|
|
241
|
-
checkFinix();
|
|
242
|
-
setTimeout(() => reject(new Error('Timeout waiting for Finix SDK')), 10000);
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
// Get session key from Finix using callback to ensure initialization is complete
|
|
247
|
-
const sessionKey = await new Promise((resolve, reject) => {
|
|
248
|
-
const timeoutId = setTimeout(() => {
|
|
249
|
-
reject(new Error('Timeout waiting for Finix Auth initialization'));
|
|
250
|
-
}, 10000);
|
|
251
|
-
// Initialize Finix Auth with callback
|
|
252
|
-
const FinixAuth = window.Finix.Auth(radarConfig.environment, radarConfig.merchantId, () => {
|
|
253
|
-
// Callback fired when Finix Auth is ready
|
|
254
|
-
clearTimeout(timeoutId);
|
|
255
|
-
const key = FinixAuth.getSessionKey();
|
|
256
|
-
console.log('Finix Auth initialized, session key:', key);
|
|
257
|
-
if (key) {
|
|
258
|
-
resolve(key);
|
|
259
|
-
}
|
|
260
|
-
else {
|
|
261
|
-
reject(new Error('No session key returned from Finix after initialization'));
|
|
262
|
-
}
|
|
263
|
-
});
|
|
264
|
-
// Also try getting session key immediately in case it's already ready
|
|
265
|
-
const immediateKey = FinixAuth.getSessionKey();
|
|
266
|
-
if (immediateKey) {
|
|
267
|
-
clearTimeout(timeoutId);
|
|
268
|
-
console.log('Finix session key obtained immediately:', immediateKey);
|
|
269
|
-
resolve(immediateKey);
|
|
270
|
-
}
|
|
271
|
-
});
|
|
272
|
-
console.log('Finix fraud session key obtained:', sessionKey);
|
|
273
|
-
// Save radar session to database
|
|
274
|
-
await paymentsResource.saveRadarSession({
|
|
275
|
-
orderId: radarConfig.orderId,
|
|
276
|
-
finixRadarSessionId: sessionKey,
|
|
277
|
-
finixRadarSessionData: {
|
|
278
|
-
sessionKey,
|
|
279
|
-
merchantId: radarConfig.merchantId,
|
|
280
|
-
environment: radarConfig.environment,
|
|
281
|
-
createdAt: new Date().toISOString(),
|
|
282
|
-
},
|
|
283
|
-
});
|
|
284
|
-
console.log('Finix radar session saved to database');
|
|
285
|
-
// Resume payment by calling completePaymentAfterAction
|
|
286
|
-
const resumedPayment = await paymentsResource.completePaymentAfterAction(payment.id);
|
|
287
|
-
console.log('Payment resumed after Finix radar:', resumedPayment);
|
|
288
|
-
// Handle the resumed payment response
|
|
289
|
-
// This is because a failed 3DS can return with status=declined but still have requireAction set
|
|
290
|
-
if (resumedPayment.status === 'declined' || resumedPayment.status === 'failed') {
|
|
291
|
-
// Payment declined or failed - extract error message from response
|
|
292
|
-
const errorMsg = resumedPayment.error?.message
|
|
293
|
-
|| resumedPayment.error?.processorMessage
|
|
294
|
-
|| 'Payment declined';
|
|
295
|
-
console.error('❌ [usePayment] Payment declined after Finix radar:', errorMsg);
|
|
296
|
-
setError(errorMsg);
|
|
297
|
-
setIsLoading(false);
|
|
298
|
-
options.onFailure?.(errorMsg);
|
|
299
|
-
options.onPaymentFailed?.({
|
|
300
|
-
code: resumedPayment.error?.code || 'PAYMENT_DECLINED',
|
|
301
|
-
message: errorMsg,
|
|
302
|
-
payment: resumedPayment,
|
|
303
|
-
});
|
|
304
|
-
// Hook-level callback (universal handler)
|
|
305
|
-
if (hookOptionsRef.current?.onPaymentFailed) {
|
|
306
|
-
hookOptionsRef.current.onPaymentFailed(errorMsg, {
|
|
307
|
-
isRedirectReturn: false,
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
else if (resumedPayment.status === 'succeeded') {
|
|
312
|
-
// Payment succeeded
|
|
313
|
-
setIsLoading(false);
|
|
314
|
-
const response = {
|
|
315
|
-
paymentId: resumedPayment.id,
|
|
316
|
-
payment: resumedPayment,
|
|
317
|
-
order: resumedPayment.order,
|
|
318
|
-
};
|
|
319
|
-
options.onSuccess?.(response);
|
|
320
|
-
options.onPaymentSuccess?.(response);
|
|
321
|
-
}
|
|
322
|
-
else if (resumedPayment.requireAction !== 'none' && resumedPayment.requireActionData) {
|
|
323
|
-
// Payment requires another action (e.g., 3DS)
|
|
324
|
-
await handlePaymentAction(resumedPayment, options);
|
|
325
|
-
}
|
|
326
|
-
else {
|
|
327
|
-
// Start polling for final status
|
|
328
|
-
startPolling(resumedPayment.id, {
|
|
329
|
-
onRequireAction: (updatedPayment) => {
|
|
330
|
-
void handlePaymentAction(updatedPayment, options);
|
|
331
|
-
},
|
|
332
|
-
onSuccess: (successPayment) => {
|
|
333
|
-
setIsLoading(false);
|
|
334
|
-
const response = {
|
|
335
|
-
paymentId: successPayment.id,
|
|
336
|
-
payment: successPayment,
|
|
337
|
-
order: successPayment.order,
|
|
338
|
-
};
|
|
339
|
-
options.onSuccess?.(response);
|
|
340
|
-
options.onPaymentSuccess?.(response);
|
|
341
|
-
},
|
|
342
|
-
onFailure: (errorMsg) => {
|
|
343
|
-
setError(errorMsg);
|
|
344
|
-
setIsLoading(false);
|
|
345
|
-
options.onFailure?.(errorMsg);
|
|
346
|
-
options.onPaymentFailed?.({
|
|
347
|
-
code: 'PAYMENT_FAILED',
|
|
348
|
-
message: errorMsg,
|
|
349
|
-
payment,
|
|
350
|
-
});
|
|
351
|
-
},
|
|
352
|
-
});
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
catch (radarError) {
|
|
356
|
-
const errorMsg = radarError instanceof Error ? radarError.message : 'Finix fraud detection failed';
|
|
357
|
-
console.error('Finix radar error:', radarError);
|
|
358
|
-
setError(errorMsg);
|
|
359
|
-
setIsLoading(false);
|
|
360
|
-
options.onFailure?.(errorMsg);
|
|
361
|
-
options.onPaymentFailed?.({
|
|
362
|
-
code: 'FINIX_RADAR_FAILED',
|
|
363
|
-
message: errorMsg,
|
|
364
|
-
payment,
|
|
365
|
-
});
|
|
366
|
-
}
|
|
367
|
-
break;
|
|
368
|
-
}
|
|
369
|
-
case 'radar': {
|
|
370
|
-
// Handle generic radar - check provider for specific implementation
|
|
371
|
-
if (actionData.metadata?.provider === 'airwallex') {
|
|
372
|
-
// Handle Airwallex fraud detection - collect device fingerprint
|
|
373
|
-
const isTest = actionData.metadata?.isTest || false;
|
|
374
|
-
const orderId = payment.order?.id;
|
|
375
|
-
const checkoutSessionId = payment.order?.checkoutSessionId;
|
|
376
|
-
if (!orderId || !checkoutSessionId) {
|
|
377
|
-
console.error('Airwallex radar: missing order or checkoutSessionId');
|
|
378
|
-
setError('Missing order information for fraud detection');
|
|
379
|
-
setIsLoading(false);
|
|
380
|
-
options.onFailure?.('Missing order information for fraud detection');
|
|
381
|
-
options.onPaymentFailed?.({
|
|
382
|
-
code: 'AIRWALLEX_RADAR_MISSING_ORDER',
|
|
383
|
-
message: 'Missing order information for fraud detection',
|
|
384
|
-
payment,
|
|
385
|
-
});
|
|
386
|
-
break;
|
|
387
|
-
}
|
|
388
|
-
try {
|
|
389
|
-
// Generate a unique session ID for Airwallex device fingerprinting
|
|
390
|
-
const generateAirwallexSessionId = () => {
|
|
391
|
-
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
|
392
|
-
const r = (Math.random() * 16) | 0;
|
|
393
|
-
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
394
|
-
return v.toString(16);
|
|
395
|
-
});
|
|
396
|
-
};
|
|
397
|
-
const sessionId = generateAirwallexSessionId();
|
|
398
|
-
// Load Airwallex fraud API script if not already loaded
|
|
399
|
-
const existingScript = document.getElementById('airwallex-fraud-api');
|
|
400
|
-
if (!existingScript) {
|
|
401
|
-
const script = document.createElement('script');
|
|
402
|
-
script.type = 'text/javascript';
|
|
403
|
-
script.async = true;
|
|
404
|
-
script.id = 'airwallex-fraud-api';
|
|
405
|
-
script.setAttribute('data-order-session-id', sessionId);
|
|
406
|
-
// Use demo URL for testing, production URL for live
|
|
407
|
-
const baseUrl = isTest ? 'https://static-demo.airwallex.com' : 'https://static.airwallex.com';
|
|
408
|
-
script.src = `${baseUrl}/webapp/fraud/device-fingerprint/index.js`;
|
|
409
|
-
// Wait for script to load
|
|
410
|
-
await new Promise((resolve, reject) => {
|
|
411
|
-
script.onload = () => {
|
|
412
|
-
console.log('Airwallex fraud API script loaded successfully');
|
|
413
|
-
resolve();
|
|
414
|
-
};
|
|
415
|
-
script.onerror = () => {
|
|
416
|
-
reject(new Error('Failed to load Airwallex fraud API script'));
|
|
417
|
-
};
|
|
418
|
-
// Append script to body
|
|
419
|
-
document.body.appendChild(script);
|
|
420
|
-
});
|
|
421
|
-
}
|
|
422
|
-
else {
|
|
423
|
-
// Update existing script with new session ID
|
|
424
|
-
existingScript.setAttribute('data-order-session-id', sessionId);
|
|
425
|
-
console.log('Updated existing Airwallex script with new session ID');
|
|
426
|
-
}
|
|
427
|
-
console.log('Airwallex device fingerprint session created:', sessionId);
|
|
428
|
-
// Save radar session to database
|
|
429
|
-
await paymentsResource.saveRadarSession({
|
|
430
|
-
checkoutSessionId,
|
|
431
|
-
orderId,
|
|
432
|
-
airwallexRadarSessionId: sessionId,
|
|
433
|
-
});
|
|
434
|
-
console.log('Airwallex radar session saved to database');
|
|
435
|
-
// Resume payment by calling completePaymentAfterAction
|
|
436
|
-
const resumedPayment = await paymentsResource.completePaymentAfterAction(payment.id);
|
|
437
|
-
console.log('Payment resumed after Airwallex radar:', resumedPayment);
|
|
438
|
-
console.log('📊 [usePayment] Resumed payment details:', {
|
|
439
|
-
status: resumedPayment.status,
|
|
440
|
-
requireAction: resumedPayment.requireAction,
|
|
441
|
-
hasRequireActionData: !!resumedPayment.requireActionData,
|
|
442
|
-
error: resumedPayment.error,
|
|
443
|
-
});
|
|
444
|
-
// Handle the resumed payment response
|
|
445
|
-
// IMPORTANT: Check for declined/failed status FIRST, before checking requireAction
|
|
446
|
-
// This is because a failed 3DS can return with status=declined but still have requireAction set
|
|
447
|
-
if (resumedPayment.status === 'declined' || resumedPayment.status === 'failed') {
|
|
448
|
-
// Payment declined or failed - extract error message from response
|
|
449
|
-
const errorMsg = resumedPayment.error?.message
|
|
450
|
-
|| resumedPayment.error?.processorMessage
|
|
451
|
-
|| 'Payment declined';
|
|
452
|
-
console.error('❌ [usePayment] Payment declined after Airwallex radar:', errorMsg);
|
|
453
|
-
setError(errorMsg);
|
|
454
|
-
setIsLoading(false);
|
|
455
|
-
options.onFailure?.(errorMsg);
|
|
456
|
-
options.onPaymentFailed?.({
|
|
457
|
-
code: resumedPayment.error?.code || 'PAYMENT_DECLINED',
|
|
458
|
-
message: errorMsg,
|
|
459
|
-
payment: resumedPayment,
|
|
460
|
-
});
|
|
461
|
-
// Hook-level callback (universal handler)
|
|
462
|
-
if (hookOptionsRef.current?.onPaymentFailed) {
|
|
463
|
-
hookOptionsRef.current.onPaymentFailed(errorMsg, {
|
|
464
|
-
isRedirectReturn: false,
|
|
465
|
-
});
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
else if (resumedPayment.status === 'succeeded') {
|
|
469
|
-
// Payment succeeded
|
|
470
|
-
setIsLoading(false);
|
|
471
|
-
const response = {
|
|
472
|
-
paymentId: resumedPayment.id,
|
|
473
|
-
payment: resumedPayment,
|
|
474
|
-
order: resumedPayment.order,
|
|
475
|
-
};
|
|
476
|
-
options.onSuccess?.(response);
|
|
477
|
-
options.onPaymentSuccess?.(response);
|
|
478
|
-
}
|
|
479
|
-
else if (resumedPayment.requireAction !== 'none' && resumedPayment.requireActionData) {
|
|
480
|
-
// Payment requires another action (e.g., 3DS)
|
|
481
|
-
await handlePaymentAction(resumedPayment, options);
|
|
482
|
-
}
|
|
483
|
-
else {
|
|
484
|
-
// Start polling for final status
|
|
485
|
-
startPolling(resumedPayment.id, {
|
|
486
|
-
onRequireAction: (updatedPayment) => {
|
|
487
|
-
void handlePaymentAction(updatedPayment, options);
|
|
488
|
-
},
|
|
489
|
-
onSuccess: (successPayment) => {
|
|
490
|
-
setIsLoading(false);
|
|
491
|
-
const response = {
|
|
492
|
-
paymentId: successPayment.id,
|
|
493
|
-
payment: successPayment,
|
|
494
|
-
order: successPayment.order,
|
|
495
|
-
};
|
|
496
|
-
options.onSuccess?.(response);
|
|
497
|
-
options.onPaymentSuccess?.(response);
|
|
498
|
-
},
|
|
499
|
-
onFailure: (errorMsg) => {
|
|
500
|
-
setError(errorMsg);
|
|
501
|
-
setIsLoading(false);
|
|
502
|
-
options.onFailure?.(errorMsg);
|
|
503
|
-
options.onPaymentFailed?.({
|
|
504
|
-
code: 'PAYMENT_FAILED',
|
|
505
|
-
message: errorMsg,
|
|
506
|
-
payment,
|
|
507
|
-
});
|
|
508
|
-
},
|
|
509
|
-
});
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
catch (radarError) {
|
|
513
|
-
const errorMsg = radarError instanceof Error ? radarError.message : 'Airwallex fraud detection failed';
|
|
514
|
-
console.error('Airwallex radar error:', radarError);
|
|
515
|
-
setError(errorMsg);
|
|
516
|
-
setIsLoading(false);
|
|
517
|
-
options.onFailure?.(errorMsg);
|
|
518
|
-
options.onPaymentFailed?.({
|
|
519
|
-
code: 'AIRWALLEX_RADAR_FAILED',
|
|
520
|
-
message: errorMsg,
|
|
521
|
-
payment,
|
|
522
|
-
});
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
break;
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
options.onRequireAction?.(payment);
|
|
529
|
-
}, [paymentsResource, startPolling, startChallenge]);
|
|
530
|
-
// Auto-detect payment action from URL parameters (after redirect from Stripe, PayPal, Airwallex, etc.)
|
|
531
|
-
useEffect(() => {
|
|
532
|
-
if (typeof window === 'undefined')
|
|
533
|
-
return;
|
|
534
|
-
// Prevent double-processing of redirect returns
|
|
535
|
-
if (redirectReturnProcessedRef.current) {
|
|
536
|
-
console.log('⏭️ [usePayment] Redirect return already processed, skipping');
|
|
537
|
-
return;
|
|
538
|
-
}
|
|
539
|
-
const urlParams = new URLSearchParams(window.location.search);
|
|
540
|
-
const paymentAction = urlParams.get('paymentAction');
|
|
541
|
-
const paymentActionStatus = urlParams.get('paymentActionStatus');
|
|
542
|
-
const paymentIdFromUrl = urlParams.get('paymentId');
|
|
543
|
-
const paymentMode = urlParams.get('mode');
|
|
544
|
-
// Airwallex 3DS return parameters
|
|
545
|
-
const paymentIntentId = urlParams.get('payment_intent_id');
|
|
546
|
-
const succeeded = urlParams.get('succeeded');
|
|
547
|
-
const processorType = urlParams.get('processorType');
|
|
548
|
-
console.log('🔍 [usePayment] Checking for payment redirect return...', {
|
|
549
|
-
paymentAction,
|
|
550
|
-
paymentActionStatus,
|
|
551
|
-
paymentId: paymentIdFromUrl,
|
|
552
|
-
mode: paymentMode,
|
|
553
|
-
paymentIntentId,
|
|
554
|
-
succeeded,
|
|
555
|
-
processorType,
|
|
556
|
-
url: window.location.href,
|
|
557
|
-
});
|
|
558
|
-
// Handle Airwallex 3DS return (payment_intent_id + succeeded + processorType=airwallex)
|
|
559
|
-
// This must be checked BEFORE the generic retrieve mode check, because Airwallex 3DS returns
|
|
560
|
-
// include mode=retrieve but also need special handling for updating the 3DS status first
|
|
561
|
-
const hasAirwallexParams = paymentIntentId || succeeded !== null;
|
|
562
|
-
if (hasAirwallexParams && processorType === 'airwallex' && paymentIdFromUrl) {
|
|
563
|
-
console.log('✅ [usePayment] Airwallex 3DS return detected!', {
|
|
564
|
-
paymentIntentId,
|
|
565
|
-
succeeded,
|
|
566
|
-
paymentId: paymentIdFromUrl,
|
|
567
|
-
});
|
|
568
|
-
// Mark as processed immediately to prevent double-processing
|
|
569
|
-
redirectReturnProcessedRef.current = true;
|
|
570
|
-
const handleAirwallex3dsReturn = async () => {
|
|
571
|
-
setIsLoading(true);
|
|
572
|
-
setCurrentPaymentId(paymentIdFromUrl);
|
|
573
|
-
if (succeeded === 'true') {
|
|
574
|
-
try {
|
|
575
|
-
// Update 3DS session status
|
|
576
|
-
await paymentsResource.updateThreedsStatus({
|
|
577
|
-
paymentId: paymentIdFromUrl,
|
|
578
|
-
status: 'succeeded',
|
|
579
|
-
paymentIntentId: paymentIntentId || '',
|
|
580
|
-
});
|
|
581
|
-
console.log('✅ [usePayment] Airwallex 3DS status updated successfully');
|
|
582
|
-
// Clean up URL parameters
|
|
583
|
-
const cleanParams = new URLSearchParams(window.location.search);
|
|
584
|
-
const airwallexParams = ['payment_intent_id', 'succeeded', 'processorType', 'paymentId', 'paymentAction', 'paymentActionStatus', 'error_code', 'error_message', 'mode'];
|
|
585
|
-
airwallexParams.forEach(param => cleanParams.delete(param));
|
|
586
|
-
const newUrl = cleanParams.toString()
|
|
587
|
-
? `${window.location.pathname}?${cleanParams.toString()}`
|
|
588
|
-
: window.location.pathname;
|
|
589
|
-
window.history.replaceState({}, document.title, newUrl);
|
|
590
|
-
// Retrieve payment to trigger server-side check with Airwallex and finalize
|
|
591
|
-
console.log('🔄 [usePayment] Retrieving payment to finalize after Airwallex 3DS...');
|
|
592
|
-
const retrieveResult = await paymentsResource.retrievePayment(paymentIdFromUrl);
|
|
593
|
-
console.log('📊 [usePayment] Retrieve result:', retrieveResult);
|
|
594
|
-
// Check retrieve result
|
|
595
|
-
const retrieveStatus = retrieveResult?.retrieveResult?.status || retrieveResult?.status;
|
|
596
|
-
if (retrieveResult?.retrieveResult?.success && retrieveStatus === 'succeeded') {
|
|
597
|
-
// Payment succeeded - get full payment details
|
|
598
|
-
const payment = await paymentsResource.getPaymentStatus(paymentIdFromUrl);
|
|
599
|
-
console.log('✅ [usePayment] Payment succeeded after Airwallex 3DS!', payment);
|
|
600
|
-
setIsLoading(false);
|
|
601
|
-
if (hookOptionsRef.current?.onPaymentCompleted) {
|
|
602
|
-
try {
|
|
603
|
-
await hookOptionsRef.current.onPaymentCompleted(payment, {
|
|
604
|
-
isRedirectReturn: true,
|
|
605
|
-
order: payment.order,
|
|
606
|
-
});
|
|
607
|
-
}
|
|
608
|
-
catch (error) {
|
|
609
|
-
console.error('❌ [usePayment] Error in onPaymentCompleted callback:', error);
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
else if (retrieveStatus === 'declined' || retrieveStatus === 'error') {
|
|
614
|
-
// Payment failed
|
|
615
|
-
const errorMsg = retrieveResult?.retrieveResult?.message || retrieveResult?.message || 'Payment failed';
|
|
616
|
-
console.error('❌ [usePayment] Payment failed after Airwallex 3DS:', errorMsg);
|
|
617
|
-
setError(errorMsg);
|
|
618
|
-
setIsLoading(false);
|
|
619
|
-
if (hookOptionsRef.current?.onPaymentFailed) {
|
|
620
|
-
try {
|
|
621
|
-
await hookOptionsRef.current.onPaymentFailed(errorMsg, {
|
|
622
|
-
isRedirectReturn: true,
|
|
623
|
-
});
|
|
624
|
-
}
|
|
625
|
-
catch (error) {
|
|
626
|
-
console.error('❌ [usePayment] Error in onPaymentFailed callback:', error);
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
else {
|
|
631
|
-
// Payment still pending or other status - check payment details
|
|
632
|
-
const payment = await paymentsResource.getPaymentStatus(paymentIdFromUrl);
|
|
633
|
-
console.log('📊 [usePayment] Payment status after retrieve:', payment);
|
|
634
|
-
if (payment.status === 'declined' || payment.status === 'failed') {
|
|
635
|
-
// Payment was declined
|
|
636
|
-
const errorMsg = payment.error?.message || payment.error?.processorMessage || 'Payment declined';
|
|
637
|
-
console.error('❌ [usePayment] Payment declined after Airwallex 3DS:', errorMsg);
|
|
638
|
-
setError(errorMsg);
|
|
639
|
-
setIsLoading(false);
|
|
640
|
-
if (hookOptionsRef.current?.onPaymentFailed) {
|
|
641
|
-
try {
|
|
642
|
-
await hookOptionsRef.current.onPaymentFailed(errorMsg, {
|
|
643
|
-
isRedirectReturn: true,
|
|
644
|
-
});
|
|
645
|
-
}
|
|
646
|
-
catch (error) {
|
|
647
|
-
console.error('❌ [usePayment] Error in onPaymentFailed callback:', error);
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
else if (payment.requireAction !== 'none' && payment.requireActionData && !payment.requireActionData.processed) {
|
|
652
|
-
console.log('⚠️ [usePayment] Payment requires new action after Airwallex 3DS', payment);
|
|
653
|
-
void handlePaymentAction(payment, {});
|
|
654
|
-
}
|
|
655
|
-
else if (payment.status === 'succeeded' || (payment.status === 'pending' && payment.subStatus === 'authorized')) {
|
|
656
|
-
console.log('✅ [usePayment] Payment succeeded after Airwallex 3DS!', payment);
|
|
657
|
-
setIsLoading(false);
|
|
658
|
-
if (hookOptionsRef.current?.onPaymentCompleted) {
|
|
659
|
-
try {
|
|
660
|
-
await hookOptionsRef.current.onPaymentCompleted(payment, {
|
|
661
|
-
isRedirectReturn: true,
|
|
662
|
-
order: payment.order,
|
|
663
|
-
});
|
|
664
|
-
}
|
|
665
|
-
catch (error) {
|
|
666
|
-
console.error('❌ [usePayment] Error in onPaymentCompleted callback:', error);
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
else {
|
|
671
|
-
// Still pending, start polling
|
|
672
|
-
console.log('⏳ [usePayment] Payment still pending, starting polling...');
|
|
673
|
-
startPolling(paymentIdFromUrl, {
|
|
674
|
-
onRequireAction: (polledPayment) => {
|
|
675
|
-
console.log('⚠️ [usePayment] Payment requires new action after Airwallex 3DS', polledPayment);
|
|
676
|
-
void handlePaymentAction(polledPayment, {});
|
|
677
|
-
},
|
|
678
|
-
onSuccess: async (polledPayment) => {
|
|
679
|
-
console.log('✅ [usePayment] Payment succeeded after Airwallex 3DS polling!', polledPayment);
|
|
680
|
-
setIsLoading(false);
|
|
681
|
-
if (hookOptionsRef.current?.onPaymentCompleted) {
|
|
682
|
-
try {
|
|
683
|
-
await hookOptionsRef.current.onPaymentCompleted(polledPayment, {
|
|
684
|
-
isRedirectReturn: true,
|
|
685
|
-
order: polledPayment.order,
|
|
686
|
-
});
|
|
687
|
-
}
|
|
688
|
-
catch (error) {
|
|
689
|
-
console.error('❌ [usePayment] Error in onPaymentCompleted callback:', error);
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
},
|
|
693
|
-
onFailure: async (errorMsg) => {
|
|
694
|
-
console.error('❌ [usePayment] Payment failed after Airwallex 3DS polling:', errorMsg);
|
|
695
|
-
setError(errorMsg);
|
|
696
|
-
setIsLoading(false);
|
|
697
|
-
if (hookOptionsRef.current?.onPaymentFailed) {
|
|
698
|
-
try {
|
|
699
|
-
await hookOptionsRef.current.onPaymentFailed(errorMsg, {
|
|
700
|
-
isRedirectReturn: true,
|
|
701
|
-
});
|
|
702
|
-
}
|
|
703
|
-
catch (error) {
|
|
704
|
-
console.error('❌ [usePayment] Error in onPaymentFailed callback:', error);
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
},
|
|
708
|
-
});
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
catch (error) {
|
|
713
|
-
console.error('❌ [usePayment] Failed to update Airwallex 3DS status:', error);
|
|
714
|
-
setError('Failed to process 3DS authentication');
|
|
715
|
-
setIsLoading(false);
|
|
716
|
-
if (hookOptionsRef.current?.onPaymentFailed) {
|
|
717
|
-
try {
|
|
718
|
-
await hookOptionsRef.current.onPaymentFailed('Failed to process 3DS authentication', {
|
|
719
|
-
isRedirectReturn: true,
|
|
720
|
-
});
|
|
721
|
-
}
|
|
722
|
-
catch (cbError) {
|
|
723
|
-
console.error('❌ [usePayment] Error in onPaymentFailed callback:', cbError);
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
else {
|
|
729
|
-
// 3DS authentication failed
|
|
730
|
-
console.error('❌ [usePayment] Airwallex 3DS authentication failed');
|
|
731
|
-
const errorMsg = urlParams.get('error_message') || 'Authentication failed';
|
|
732
|
-
setError(errorMsg);
|
|
733
|
-
setIsLoading(false);
|
|
734
|
-
// Clean up URL parameters
|
|
735
|
-
const cleanParams = new URLSearchParams(window.location.search);
|
|
736
|
-
const airwallexParams = ['payment_intent_id', 'succeeded', 'processorType', 'paymentId', 'paymentAction', 'paymentActionStatus', 'error_code', 'error_message', 'mode'];
|
|
737
|
-
airwallexParams.forEach(param => cleanParams.delete(param));
|
|
738
|
-
const newUrl = cleanParams.toString()
|
|
739
|
-
? `${window.location.pathname}?${cleanParams.toString()}`
|
|
740
|
-
: window.location.pathname;
|
|
741
|
-
window.history.replaceState({}, document.title, newUrl);
|
|
742
|
-
if (hookOptionsRef.current?.onPaymentFailed) {
|
|
743
|
-
try {
|
|
744
|
-
await hookOptionsRef.current.onPaymentFailed(errorMsg, {
|
|
745
|
-
isRedirectReturn: true,
|
|
746
|
-
});
|
|
747
|
-
}
|
|
748
|
-
catch (error) {
|
|
749
|
-
console.error('❌ [usePayment] Error in onPaymentFailed callback:', error);
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
};
|
|
754
|
-
void handleAirwallex3dsReturn();
|
|
755
|
-
return;
|
|
756
|
-
}
|
|
757
|
-
// Skip if in retrieve mode (handled by usePaymentRetrieve hook)
|
|
758
|
-
// This check is AFTER the Airwallex handling because Airwallex 3DS returns include mode=retrieve
|
|
759
|
-
// but need special handling that usePaymentRetrieve doesn't provide
|
|
760
|
-
if (paymentMode === 'retrieve') {
|
|
761
|
-
console.log('⏭️ [usePayment] Skipping - retrieve mode detected (handled by usePaymentRetrieve)');
|
|
762
|
-
return;
|
|
763
|
-
}
|
|
764
|
-
// Check if returning from a payment redirect (generic handling)
|
|
765
|
-
if (paymentAction === 'requireAction' && paymentActionStatus === 'completed' && paymentIdFromUrl) {
|
|
766
|
-
console.log('✅ [usePayment] Payment redirect return detected! Starting auto-polling...', {
|
|
767
|
-
paymentId: paymentIdFromUrl,
|
|
768
|
-
});
|
|
769
|
-
// Mark as processed immediately to prevent double-processing
|
|
770
|
-
redirectReturnProcessedRef.current = true;
|
|
771
|
-
setIsLoading(true);
|
|
772
|
-
setCurrentPaymentId(paymentIdFromUrl);
|
|
773
|
-
// Start polling for the payment status
|
|
774
|
-
startPolling(paymentIdFromUrl, {
|
|
775
|
-
onRequireAction: (payment) => {
|
|
776
|
-
console.log('⚠️ [usePayment] Payment requires new action', payment);
|
|
777
|
-
void handlePaymentAction(payment, {});
|
|
778
|
-
},
|
|
779
|
-
onSuccess: async (payment) => {
|
|
780
|
-
console.log('✅ [usePayment] Payment succeeded after redirect!', {
|
|
781
|
-
paymentId: payment.id,
|
|
782
|
-
status: payment.status,
|
|
783
|
-
hasOrder: !!payment.order,
|
|
784
|
-
});
|
|
785
|
-
setIsLoading(false);
|
|
786
|
-
// Clean up ONLY payment-related query parameters (preserve funnel/checkout params)
|
|
787
|
-
const urlParams = new URLSearchParams(window.location.search);
|
|
788
|
-
const paymentParams = ['paymentAction', 'paymentActionStatus', 'paymentId', 'payment_intent', 'payment_intent_client_secret', 'source_type', 'redirect_status'];
|
|
789
|
-
paymentParams.forEach(param => urlParams.delete(param));
|
|
790
|
-
const newUrl = urlParams.toString()
|
|
791
|
-
? `${window.location.pathname}?${urlParams.toString()}`
|
|
792
|
-
: window.location.pathname;
|
|
793
|
-
window.history.replaceState({}, document.title, newUrl);
|
|
794
|
-
console.log('🧹 [usePayment] Payment URL parameters cleaned up (preserved funnel/checkout params)');
|
|
795
|
-
// Call hook-level onPaymentCompleted callback (if provided)
|
|
796
|
-
if (hookOptionsRef.current?.onPaymentCompleted) {
|
|
797
|
-
console.log('📞 [usePayment] Calling onPaymentCompleted callback...', {
|
|
798
|
-
isRedirectReturn: true,
|
|
799
|
-
});
|
|
800
|
-
try {
|
|
801
|
-
await hookOptionsRef.current.onPaymentCompleted(payment, {
|
|
802
|
-
isRedirectReturn: true,
|
|
803
|
-
order: payment.order,
|
|
804
|
-
});
|
|
805
|
-
console.log('✅ [usePayment] onPaymentCompleted callback completed successfully');
|
|
806
|
-
}
|
|
807
|
-
catch (error) {
|
|
808
|
-
console.error('❌ [usePayment] Error in onPaymentCompleted callback:', error);
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
else {
|
|
812
|
-
console.warn('⚠️ [usePayment] No onPaymentCompleted callback provided');
|
|
813
|
-
}
|
|
814
|
-
},
|
|
815
|
-
onFailure: async (errorMsg) => {
|
|
816
|
-
console.error('❌ [usePayment] Payment failed after redirect:', errorMsg);
|
|
817
|
-
setError(errorMsg);
|
|
818
|
-
setIsLoading(false);
|
|
819
|
-
// Clean up ONLY payment-related query parameters (preserve funnel/checkout params)
|
|
820
|
-
const urlParams = new URLSearchParams(window.location.search);
|
|
821
|
-
const paymentParams = ['paymentAction', 'paymentActionStatus', 'paymentId', 'payment_intent', 'payment_intent_client_secret', 'source_type', 'redirect_status'];
|
|
822
|
-
paymentParams.forEach(param => urlParams.delete(param));
|
|
823
|
-
const newUrl = urlParams.toString()
|
|
824
|
-
? `${window.location.pathname}?${urlParams.toString()}`
|
|
825
|
-
: window.location.pathname;
|
|
826
|
-
window.history.replaceState({}, document.title, newUrl);
|
|
827
|
-
// Call hook-level onPaymentFailed callback (if provided)
|
|
828
|
-
if (hookOptionsRef.current?.onPaymentFailed) {
|
|
829
|
-
console.log('📞 [usePayment] Calling onPaymentFailed callback...');
|
|
830
|
-
try {
|
|
831
|
-
await hookOptionsRef.current.onPaymentFailed(errorMsg, {
|
|
832
|
-
isRedirectReturn: true,
|
|
833
|
-
});
|
|
834
|
-
}
|
|
835
|
-
catch (error) {
|
|
836
|
-
console.error('❌ [usePayment] Error in onPaymentFailed callback:', error);
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
},
|
|
840
|
-
});
|
|
841
|
-
}
|
|
842
|
-
else {
|
|
843
|
-
console.log('⏭️ [usePayment] No payment redirect detected - normal page load');
|
|
844
|
-
}
|
|
845
|
-
}, [paymentsResource, startPolling, handlePaymentAction, setIsLoading, setError, setCurrentPaymentId]);
|
|
846
|
-
// Create card payment instrument - matches old implementation
|
|
847
|
-
const createCardPaymentInstrument = useCallback((cardData) => {
|
|
848
|
-
return paymentsResource.createCardPaymentInstrument(basisTheory, cardData);
|
|
849
|
-
}, [basisTheory, paymentsResource]);
|
|
850
|
-
// Create Apple Pay payment instrument - matches old implementation
|
|
851
|
-
const createApplePayPaymentInstrument = useCallback((applePayToken) => {
|
|
852
|
-
return paymentsResource.createApplePayPaymentInstrument(basisTheory, applePayToken);
|
|
853
|
-
}, [basisTheory, paymentsResource]);
|
|
854
|
-
// Create Google Pay payment instrument - matches express payment pattern
|
|
855
|
-
const createGooglePayPaymentInstrument = useCallback((googlePayToken) => {
|
|
856
|
-
return paymentsResource.createGooglePayPaymentInstrument(basisTheory, googlePayToken);
|
|
857
|
-
}, [basisTheory, paymentsResource]);
|
|
858
|
-
// Process payment directly with checkout session - matches old implementation
|
|
859
|
-
const processPaymentDirect = useCallback(async (checkoutSessionId, paymentInstrumentId, threedsSessionId, options = {}) => {
|
|
860
|
-
try {
|
|
861
|
-
// Get paymentFlowId: priority is options > stepConfig > undefined (uses store default)
|
|
862
|
-
const paymentFlowId = options.paymentFlowId || getAssignedPaymentFlowId();
|
|
863
|
-
const response = await paymentsResource.processPaymentDirect(checkoutSessionId, paymentInstrumentId, threedsSessionId, {
|
|
864
|
-
initiatedBy: options.initiatedBy,
|
|
865
|
-
source: options.source,
|
|
866
|
-
paymentFlowId,
|
|
867
|
-
});
|
|
868
|
-
setCurrentPaymentId(response.payment?.id);
|
|
869
|
-
if (response.payment.requireAction !== 'none') {
|
|
870
|
-
await handlePaymentAction(response.payment, options);
|
|
871
|
-
}
|
|
872
|
-
else if (response.payment.status === 'succeeded') {
|
|
873
|
-
setIsLoading(false);
|
|
874
|
-
// Ensure order is at response root (extract from payment if needed)
|
|
875
|
-
const successResponse = {
|
|
876
|
-
...response,
|
|
877
|
-
order: response.order || response.payment.order,
|
|
878
|
-
};
|
|
879
|
-
// Hook-level callback (universal handler)
|
|
880
|
-
if (hookOptionsRef.current?.onPaymentCompleted) {
|
|
881
|
-
await hookOptionsRef.current.onPaymentCompleted(response.payment, {
|
|
882
|
-
isRedirectReturn: false,
|
|
883
|
-
order: successResponse.order,
|
|
884
|
-
checkoutSessionId,
|
|
885
|
-
});
|
|
886
|
-
}
|
|
887
|
-
// Legacy callback (backwards compatibility)
|
|
888
|
-
options.onSuccess?.(successResponse);
|
|
889
|
-
// Funnel-aligned callback (recommended)
|
|
890
|
-
options.onPaymentSuccess?.(successResponse);
|
|
891
|
-
}
|
|
892
|
-
else {
|
|
893
|
-
// Start polling for payment status
|
|
894
|
-
startPolling(response.payment?.id, {
|
|
895
|
-
onRequireAction: (payment) => {
|
|
896
|
-
void handlePaymentAction(payment, options);
|
|
897
|
-
},
|
|
898
|
-
onSuccess: async (payment) => {
|
|
899
|
-
setIsLoading(false);
|
|
900
|
-
const successResponse = {
|
|
901
|
-
paymentId: payment.id,
|
|
902
|
-
payment,
|
|
903
|
-
// Extract order from payment if available (for funnel path resolution)
|
|
904
|
-
order: payment.order,
|
|
905
|
-
};
|
|
906
|
-
// Hook-level callback (universal handler)
|
|
907
|
-
if (hookOptionsRef.current?.onPaymentCompleted) {
|
|
908
|
-
await hookOptionsRef.current.onPaymentCompleted(payment, {
|
|
909
|
-
isRedirectReturn: false,
|
|
910
|
-
order: successResponse.order,
|
|
911
|
-
checkoutSessionId,
|
|
912
|
-
});
|
|
913
|
-
}
|
|
914
|
-
// Legacy callback (backwards compatibility)
|
|
915
|
-
options.onSuccess?.(successResponse);
|
|
916
|
-
// Funnel-aligned callback (recommended)
|
|
917
|
-
options.onPaymentSuccess?.(successResponse);
|
|
918
|
-
},
|
|
919
|
-
onFailure: async (errorMsg) => {
|
|
920
|
-
setError(errorMsg);
|
|
921
|
-
setIsLoading(false);
|
|
922
|
-
// Hook-level callback (universal handler)
|
|
923
|
-
if (hookOptionsRef.current?.onPaymentFailed) {
|
|
924
|
-
await hookOptionsRef.current.onPaymentFailed(errorMsg, {
|
|
925
|
-
isRedirectReturn: false,
|
|
926
|
-
});
|
|
927
|
-
}
|
|
928
|
-
// Legacy callback (backwards compatibility)
|
|
929
|
-
options.onFailure?.(errorMsg);
|
|
930
|
-
// Funnel-aligned callback (recommended)
|
|
931
|
-
options.onPaymentFailed?.({
|
|
932
|
-
code: 'PAYMENT_FAILED',
|
|
933
|
-
message: errorMsg,
|
|
934
|
-
payment: response.payment,
|
|
935
|
-
});
|
|
936
|
-
},
|
|
937
|
-
});
|
|
938
|
-
}
|
|
939
|
-
return response;
|
|
940
|
-
}
|
|
941
|
-
catch (_error) {
|
|
942
|
-
const errorMsg = _error instanceof Error ? _error.message : 'Payment failed';
|
|
943
|
-
setError(errorMsg);
|
|
944
|
-
setIsLoading(false);
|
|
945
|
-
// Legacy callback (backwards compatibility)
|
|
946
|
-
options.onFailure?.(errorMsg);
|
|
947
|
-
// Funnel-aligned callback (recommended)
|
|
948
|
-
options.onPaymentFailed?.({
|
|
949
|
-
code: 'PAYMENT_PROCESSING_ERROR',
|
|
950
|
-
message: errorMsg,
|
|
951
|
-
});
|
|
952
|
-
throw _error;
|
|
953
|
-
}
|
|
954
|
-
}, [paymentsResource, handlePaymentAction, startPolling]);
|
|
955
|
-
// Process card payment - matches old implementation
|
|
956
|
-
const processCardPayment = useCallback(async (checkoutSessionId, cardData, options = {}) => {
|
|
957
|
-
setIsLoading(true);
|
|
958
|
-
setError(null);
|
|
959
|
-
try {
|
|
960
|
-
// 1. Create payment instrument
|
|
961
|
-
const paymentInstrument = await createCardPaymentInstrument(cardData);
|
|
962
|
-
// 2. Create 3DS session if enabled (use payment flow setting if not explicitly provided)
|
|
963
|
-
const shouldCreateThreedsSession = storeConfig?.computed?.threedsEnabled;
|
|
964
|
-
let threedsSessionId;
|
|
965
|
-
if (shouldCreateThreedsSession) {
|
|
966
|
-
try {
|
|
967
|
-
const threedsSession = await createSession(paymentInstrument, {
|
|
968
|
-
provider: options.threedsProvider || 'basis_theory',
|
|
969
|
-
});
|
|
970
|
-
threedsSessionId = threedsSession.id;
|
|
971
|
-
}
|
|
972
|
-
catch (_error) {
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
// 3. Process payment directly
|
|
976
|
-
return await processPaymentDirect(checkoutSessionId, paymentInstrument.id, threedsSessionId, options);
|
|
977
|
-
}
|
|
978
|
-
catch (_error) {
|
|
979
|
-
setIsLoading(false);
|
|
980
|
-
const errorMsg = _error instanceof Error ? _error.message : 'Payment failed';
|
|
981
|
-
setError(errorMsg);
|
|
982
|
-
// Legacy callback (backwards compatibility)
|
|
983
|
-
options.onFailure?.(errorMsg);
|
|
984
|
-
// Funnel-aligned callback (recommended)
|
|
985
|
-
options.onPaymentFailed?.({
|
|
986
|
-
code: 'CARD_PAYMENT_ERROR',
|
|
987
|
-
message: errorMsg,
|
|
988
|
-
});
|
|
989
|
-
throw _error;
|
|
990
|
-
}
|
|
991
|
-
}, [createCardPaymentInstrument, createSession, processPaymentDirect]);
|
|
992
|
-
// Process Apple Pay payment - matches old implementation
|
|
993
|
-
const processApplePayPayment = useCallback(async (checkoutSessionId, applePayToken, options = {}) => {
|
|
994
|
-
setIsLoading(true);
|
|
995
|
-
setError(null);
|
|
996
|
-
try {
|
|
997
|
-
// 1. Create payment instrument
|
|
998
|
-
const paymentInstrument = await createApplePayPaymentInstrument(applePayToken);
|
|
999
|
-
// 2. Process payment directly (Apple Pay typically doesn't require 3DS)
|
|
1000
|
-
return await processPaymentDirect(checkoutSessionId, paymentInstrument.id, undefined, options);
|
|
1001
|
-
}
|
|
1002
|
-
catch (_error) {
|
|
1003
|
-
setIsLoading(false);
|
|
1004
|
-
const errorMsg = _error instanceof Error ? _error.message : 'Apple Pay payment failed';
|
|
1005
|
-
setError(errorMsg);
|
|
1006
|
-
// Legacy callback (backwards compatibility)
|
|
1007
|
-
options.onFailure?.(errorMsg);
|
|
1008
|
-
// Funnel-aligned callback (recommended)
|
|
1009
|
-
options.onPaymentFailed?.({
|
|
1010
|
-
code: 'APPLE_PAY_ERROR',
|
|
1011
|
-
message: errorMsg,
|
|
1012
|
-
});
|
|
1013
|
-
throw _error;
|
|
1014
|
-
}
|
|
1015
|
-
}, [createApplePayPaymentInstrument, processPaymentDirect]);
|
|
1016
|
-
// Process Google Pay payment - matches express payment pattern
|
|
1017
|
-
const processGooglePayPayment = useCallback(async (checkoutSessionId, googlePayToken, options = {}) => {
|
|
1018
|
-
setIsLoading(true);
|
|
1019
|
-
setError(null);
|
|
1020
|
-
try {
|
|
1021
|
-
// 1. Create payment instrument
|
|
1022
|
-
const paymentInstrument = await createGooglePayPaymentInstrument(googlePayToken);
|
|
1023
|
-
// 2. Process payment directly (Google Pay typically doesn't require 3DS)
|
|
1024
|
-
return await processPaymentDirect(checkoutSessionId, paymentInstrument.id, undefined, options);
|
|
1025
|
-
}
|
|
1026
|
-
catch (_error) {
|
|
1027
|
-
setIsLoading(false);
|
|
1028
|
-
const errorMsg = _error instanceof Error ? _error.message : 'Google Pay payment failed';
|
|
1029
|
-
setError(errorMsg);
|
|
1030
|
-
// Legacy callback (backwards compatibility)
|
|
1031
|
-
options.onFailure?.(errorMsg);
|
|
1032
|
-
// Funnel-aligned callback (recommended)
|
|
1033
|
-
options.onPaymentFailed?.({
|
|
1034
|
-
code: 'GOOGLE_PAY_ERROR',
|
|
1035
|
-
message: errorMsg,
|
|
1036
|
-
});
|
|
1037
|
-
throw _error;
|
|
1038
|
-
}
|
|
1039
|
-
}, [createGooglePayPaymentInstrument, processPaymentDirect]);
|
|
1040
|
-
// Process payment with existing instrument - matches old implementation
|
|
1041
|
-
const processPaymentWithInstrument = useCallback(async (checkoutSessionId, paymentInstrumentId, options = {}) => {
|
|
1042
|
-
setIsLoading(true);
|
|
1043
|
-
setError(null);
|
|
1044
|
-
try {
|
|
1045
|
-
return await processPaymentDirect(checkoutSessionId, paymentInstrumentId, undefined, options);
|
|
1046
|
-
}
|
|
1047
|
-
catch (_error) {
|
|
1048
|
-
setIsLoading(false);
|
|
1049
|
-
const errorMsg = _error instanceof Error ? _error.message : 'Payment failed';
|
|
1050
|
-
setError(errorMsg);
|
|
1051
|
-
// Legacy callback (backwards compatibility)
|
|
1052
|
-
options.onFailure?.(errorMsg);
|
|
1053
|
-
// Funnel-aligned callback (recommended)
|
|
1054
|
-
options.onPaymentFailed?.({
|
|
1055
|
-
code: 'PAYMENT_INSTRUMENT_ERROR',
|
|
1056
|
-
message: errorMsg,
|
|
1057
|
-
});
|
|
1058
|
-
throw _error;
|
|
1059
|
-
}
|
|
1060
|
-
}, [processPaymentDirect]);
|
|
1061
|
-
// Get card payment instruments - matches old implementation
|
|
1062
|
-
const getCardPaymentInstruments = useCallback(async () => {
|
|
1063
|
-
try {
|
|
1064
|
-
const response = await paymentsResource.getCardPaymentInstruments();
|
|
1065
|
-
return response;
|
|
1066
|
-
}
|
|
1067
|
-
catch (error) {
|
|
1068
|
-
const errorMsg = error instanceof Error ? error.message : 'Failed to fetch payment instruments';
|
|
1069
|
-
setError(errorMsg);
|
|
1070
|
-
throw error;
|
|
1071
|
-
}
|
|
1072
|
-
}, [paymentsResource]);
|
|
66
|
+
// Use payment action handler hook
|
|
67
|
+
const { handlePaymentAction } = usePaymentActionHandler({
|
|
68
|
+
paymentsResource,
|
|
69
|
+
startChallenge,
|
|
70
|
+
startPolling,
|
|
71
|
+
setIsLoading,
|
|
72
|
+
setError,
|
|
73
|
+
hookOptionsRef,
|
|
74
|
+
});
|
|
75
|
+
// Handle Airwallex 3DS redirect returns
|
|
76
|
+
useAirwallex3dsReturn({
|
|
77
|
+
paymentsResource,
|
|
78
|
+
startPolling,
|
|
79
|
+
handlePaymentAction,
|
|
80
|
+
setIsLoading,
|
|
81
|
+
setError,
|
|
82
|
+
setCurrentPaymentId,
|
|
83
|
+
hookOptionsRef,
|
|
84
|
+
redirectReturnProcessedRef,
|
|
85
|
+
});
|
|
86
|
+
// Handle generic payment redirect returns
|
|
87
|
+
useGenericPaymentReturn({
|
|
88
|
+
startPolling,
|
|
89
|
+
handlePaymentAction,
|
|
90
|
+
setIsLoading,
|
|
91
|
+
setError,
|
|
92
|
+
setCurrentPaymentId,
|
|
93
|
+
hookOptionsRef,
|
|
94
|
+
redirectReturnProcessedRef,
|
|
95
|
+
});
|
|
96
|
+
// Use payment instruments hook
|
|
97
|
+
const paymentInstruments = usePaymentInstruments({
|
|
98
|
+
basisTheory,
|
|
99
|
+
paymentsResource,
|
|
100
|
+
setError,
|
|
101
|
+
});
|
|
102
|
+
const { createCardPaymentInstrument, createApplePayPaymentInstrument, createGooglePayPaymentInstrument, getCardPaymentInstruments, } = paymentInstruments;
|
|
103
|
+
// Use payment processors hook
|
|
104
|
+
const paymentProcessors = usePaymentProcessors({
|
|
105
|
+
paymentsResource,
|
|
106
|
+
createCardPaymentInstrument,
|
|
107
|
+
createApplePayPaymentInstrument,
|
|
108
|
+
createGooglePayPaymentInstrument,
|
|
109
|
+
createSession,
|
|
110
|
+
handlePaymentAction,
|
|
111
|
+
startPolling,
|
|
112
|
+
setIsLoading,
|
|
113
|
+
setError,
|
|
114
|
+
setCurrentPaymentId,
|
|
115
|
+
hookOptionsRef,
|
|
116
|
+
storeConfig,
|
|
117
|
+
});
|
|
118
|
+
const { processCardPayment, processApplePayPayment, processGooglePayPayment, processPaymentWithInstrument, processApmPayment, } = paymentProcessors;
|
|
1073
119
|
const clearError = useCallback(() => {
|
|
1074
120
|
setError(null);
|
|
1075
121
|
}, []);
|
|
122
|
+
const tagadaError = useMemo(() => error ? new TagadaError(error, { code: TagadaErrorCode.PAYMENT_FAILED }) : null, [error]);
|
|
1076
123
|
return {
|
|
1077
124
|
processCardPayment,
|
|
1078
125
|
processApplePayPayment,
|
|
1079
126
|
processGooglePayPayment,
|
|
1080
127
|
processPaymentWithInstrument,
|
|
128
|
+
processApmPayment,
|
|
1081
129
|
createCardPaymentInstrument,
|
|
1082
130
|
createApplePayPaymentInstrument,
|
|
1083
131
|
createGooglePayPaymentInstrument,
|
|
1084
132
|
getCardPaymentInstruments,
|
|
1085
|
-
isLoading: isLoading || !basisTheory,
|
|
1086
|
-
error,
|
|
133
|
+
isLoading: isLoading || !basisTheory,
|
|
134
|
+
error: tagadaError,
|
|
1087
135
|
clearError,
|
|
1088
136
|
currentPaymentId,
|
|
1089
137
|
};
|