@tagadapay/plugin-sdk 3.1.5 → 3.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +1129 -1129
  2. package/build-cdn.js +220 -113
  3. package/dist/external-tracker.js +1225 -558
  4. package/dist/external-tracker.min.js +2 -2
  5. package/dist/external-tracker.min.js.map +4 -4
  6. package/dist/react/hooks/useApplePay.js +25 -36
  7. package/dist/react/hooks/usePaymentPolling.d.ts +9 -3
  8. package/dist/react/providers/TagadaProvider.js +5 -5
  9. package/dist/react/utils/money.d.ts +4 -3
  10. package/dist/react/utils/money.js +39 -6
  11. package/dist/react/utils/trackingUtils.js +1 -0
  12. package/dist/tagada-sdk.js +10142 -0
  13. package/dist/tagada-sdk.min.js +43 -0
  14. package/dist/tagada-sdk.min.js.map +7 -0
  15. package/dist/v2/core/client.js +34 -2
  16. package/dist/v2/core/config/environment.js +9 -2
  17. package/dist/v2/core/funnelClient.d.ts +180 -2
  18. package/dist/v2/core/funnelClient.js +289 -6
  19. package/dist/v2/core/resources/apiClient.js +1 -1
  20. package/dist/v2/core/resources/checkout.d.ts +68 -0
  21. package/dist/v2/core/resources/funnel.d.ts +25 -0
  22. package/dist/v2/core/resources/payments.d.ts +70 -3
  23. package/dist/v2/core/resources/payments.js +72 -7
  24. package/dist/v2/core/utils/index.d.ts +1 -0
  25. package/dist/v2/core/utils/index.js +2 -0
  26. package/dist/v2/core/utils/pluginConfig.d.ts +8 -0
  27. package/dist/v2/core/utils/pluginConfig.js +68 -5
  28. package/dist/v2/core/utils/previewMode.d.ts +7 -0
  29. package/dist/v2/core/utils/previewMode.js +72 -14
  30. package/dist/v2/core/utils/previewModeIndicator.d.ts +19 -0
  31. package/dist/v2/core/utils/previewModeIndicator.js +414 -0
  32. package/dist/v2/core/utils/tokenStorage.d.ts +4 -0
  33. package/dist/v2/core/utils/tokenStorage.js +15 -1
  34. package/dist/v2/index.d.ts +9 -3
  35. package/dist/v2/index.js +8 -3
  36. package/dist/v2/react/components/ApplePayButton.d.ts +22 -123
  37. package/dist/v2/react/components/ApplePayButton.js +247 -317
  38. package/dist/v2/react/components/FunnelScriptInjector.d.ts +3 -1
  39. package/dist/v2/react/components/FunnelScriptInjector.js +255 -162
  40. package/dist/v2/react/components/GooglePayButton.d.ts +2 -0
  41. package/dist/v2/react/components/GooglePayButton.js +80 -64
  42. package/dist/v2/react/components/PreviewModeIndicator.d.ts +46 -0
  43. package/dist/v2/react/components/PreviewModeIndicator.js +113 -0
  44. package/dist/v2/react/hooks/useApplePayCheckout.d.ts +16 -0
  45. package/dist/v2/react/hooks/useApplePayCheckout.js +193 -0
  46. package/dist/v2/react/hooks/useFunnel.d.ts +48 -6
  47. package/dist/v2/react/hooks/useFunnel.js +25 -5
  48. package/dist/v2/react/hooks/useGoogleAutocomplete.d.ts +10 -0
  49. package/dist/v2/react/hooks/useGoogleAutocomplete.js +48 -0
  50. package/dist/v2/react/hooks/useGooglePayCheckout.d.ts +21 -0
  51. package/dist/v2/react/hooks/useGooglePayCheckout.js +198 -0
  52. package/dist/v2/react/hooks/usePaymentPolling.d.ts +15 -3
  53. package/dist/v2/react/hooks/usePaymentPolling.js +31 -9
  54. package/dist/v2/react/hooks/usePaymentQuery.d.ts +34 -2
  55. package/dist/v2/react/hooks/usePaymentQuery.js +731 -7
  56. package/dist/v2/react/hooks/usePaymentRetrieve.d.ts +26 -0
  57. package/dist/v2/react/hooks/usePaymentRetrieve.js +175 -0
  58. package/dist/v2/react/hooks/usePixelTracking.d.ts +56 -0
  59. package/dist/v2/react/hooks/usePixelTracking.js +508 -0
  60. package/dist/v2/react/hooks/useStepConfig.d.ts +64 -0
  61. package/dist/v2/react/hooks/useStepConfig.js +53 -0
  62. package/dist/v2/react/index.d.ts +15 -5
  63. package/dist/v2/react/index.js +8 -2
  64. package/dist/v2/react/providers/ExpressPaymentMethodsProvider.d.ts +1 -0
  65. package/dist/v2/react/providers/ExpressPaymentMethodsProvider.js +41 -13
  66. package/dist/v2/react/providers/TagadaProvider.js +24 -23
  67. package/dist/v2/standalone/external-tracker.d.ts +2 -0
  68. package/dist/v2/standalone/external-tracker.js +6 -3
  69. package/package.json +112 -112
  70. package/dist/v2/react/hooks/useApplePay.d.ts +0 -16
  71. package/dist/v2/react/hooks/useApplePay.js +0 -247
@@ -10,8 +10,12 @@ import { PaymentsResource } from '../../core/resources/payments';
10
10
  import { usePaymentPolling } from './usePaymentPolling';
11
11
  import { useThreeds } from './useThreeds';
12
12
  import { getGlobalApiClient } from './useApiQuery';
13
- export function usePaymentQuery() {
13
+ import { useStoreConfigQuery } from './useStoreConfigQuery';
14
+ import { getAssignedPaymentFlowId } from '../../core/funnelClient';
15
+ export function usePaymentQuery(hookOptions) {
14
16
  const { environment } = useTagadaContext();
17
+ // Get store config to auto-detect 3DS setting from payment flow
18
+ const { storeConfig } = useStoreConfigQuery();
15
19
  // Create payments resource client
16
20
  const paymentsResource = useMemo(() => {
17
21
  try {
@@ -24,10 +28,17 @@ export function usePaymentQuery() {
24
28
  const [isLoading, setIsLoading] = useState(false);
25
29
  const [error, setError] = useState(null);
26
30
  const [currentPaymentId, setCurrentPaymentId] = useState(null);
31
+ // Store hook-level callbacks in ref to avoid re-renders
32
+ const hookOptionsRef = useRef(hookOptions);
33
+ useEffect(() => {
34
+ hookOptionsRef.current = hookOptions;
35
+ }, [hookOptions]);
27
36
  const { startPolling, stopPolling } = usePaymentPolling();
28
37
  const { createSession, startChallenge } = useThreeds();
29
38
  // Track challenge in progress to prevent multiple challenges
30
39
  const challengeInProgressRef = useRef(false);
40
+ // Track if we've already processed a redirect return (prevents double-processing)
41
+ const redirectReturnProcessedRef = useRef(false);
31
42
  // Get API key from embedded configuration with proper environment detection
32
43
  const apiKey = useMemo(() => getBasisTheoryApiKey(), []); // Auto-detects environment
33
44
  // Initialize BasisTheory using React wrapper
@@ -83,7 +94,7 @@ export function usePaymentQuery() {
83
94
  onRequireAction: (updatedPayment) => {
84
95
  void handlePaymentAction(updatedPayment, options);
85
96
  },
86
- onSuccess: (successPayment) => {
97
+ onSuccess: async (successPayment) => {
87
98
  setIsLoading(false);
88
99
  const response = {
89
100
  paymentId: successPayment.id,
@@ -91,14 +102,27 @@ export function usePaymentQuery() {
91
102
  // Extract order from payment if available (for funnel path resolution)
92
103
  order: successPayment.order,
93
104
  };
105
+ // Hook-level callback (universal handler)
106
+ if (hookOptionsRef.current?.onPaymentCompleted) {
107
+ await hookOptionsRef.current.onPaymentCompleted(successPayment, {
108
+ isRedirectReturn: false,
109
+ order: response.order,
110
+ });
111
+ }
94
112
  // Legacy callback (backwards compatibility)
95
113
  options.onSuccess?.(response);
96
114
  // Funnel-aligned callback (recommended)
97
115
  options.onPaymentSuccess?.(response);
98
116
  },
99
- onFailure: (errorMsg) => {
117
+ onFailure: async (errorMsg) => {
100
118
  setError(errorMsg);
101
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
+ }
102
126
  // Legacy callback (backwards compatibility)
103
127
  options.onFailure?.(errorMsg);
104
128
  // Funnel-aligned callback (recommended)
@@ -150,6 +174,13 @@ export function usePaymentQuery() {
150
174
  // Extract order from payment if available (for funnel path resolution)
151
175
  order: payment.order,
152
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
+ }
153
184
  // Legacy callback (backwards compatibility)
154
185
  options.onSuccess?.(response);
155
186
  // Funnel-aligned callback (recommended)
@@ -172,9 +203,646 @@ export function usePaymentQuery() {
172
203
  });
173
204
  break;
174
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
+ }
175
527
  }
176
528
  options.onRequireAction?.(payment);
177
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]);
178
846
  // Create card payment instrument - matches old implementation
179
847
  const createCardPaymentInstrument = useCallback((cardData) => {
180
848
  return paymentsResource.createCardPaymentInstrument(basisTheory, cardData);
@@ -183,12 +851,19 @@ export function usePaymentQuery() {
183
851
  const createApplePayPaymentInstrument = useCallback((applePayToken) => {
184
852
  return paymentsResource.createApplePayPaymentInstrument(basisTheory, applePayToken);
185
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]);
186
858
  // Process payment directly with checkout session - matches old implementation
187
859
  const processPaymentDirect = useCallback(async (checkoutSessionId, paymentInstrumentId, threedsSessionId, options = {}) => {
188
860
  try {
861
+ // Get paymentFlowId: priority is options > stepConfig > undefined (uses store default)
862
+ const paymentFlowId = options.paymentFlowId || getAssignedPaymentFlowId();
189
863
  const response = await paymentsResource.processPaymentDirect(checkoutSessionId, paymentInstrumentId, threedsSessionId, {
190
864
  initiatedBy: options.initiatedBy,
191
865
  source: options.source,
866
+ paymentFlowId,
192
867
  });
193
868
  setCurrentPaymentId(response.payment?.id);
194
869
  if (response.payment.requireAction !== 'none') {
@@ -201,6 +876,14 @@ export function usePaymentQuery() {
201
876
  ...response,
202
877
  order: response.order || response.payment.order,
203
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
+ }
204
887
  // Legacy callback (backwards compatibility)
205
888
  options.onSuccess?.(successResponse);
206
889
  // Funnel-aligned callback (recommended)
@@ -212,7 +895,7 @@ export function usePaymentQuery() {
212
895
  onRequireAction: (payment) => {
213
896
  void handlePaymentAction(payment, options);
214
897
  },
215
- onSuccess: (payment) => {
898
+ onSuccess: async (payment) => {
216
899
  setIsLoading(false);
217
900
  const successResponse = {
218
901
  paymentId: payment.id,
@@ -220,14 +903,28 @@ export function usePaymentQuery() {
220
903
  // Extract order from payment if available (for funnel path resolution)
221
904
  order: payment.order,
222
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
+ }
223
914
  // Legacy callback (backwards compatibility)
224
915
  options.onSuccess?.(successResponse);
225
916
  // Funnel-aligned callback (recommended)
226
917
  options.onPaymentSuccess?.(successResponse);
227
918
  },
228
- onFailure: (errorMsg) => {
919
+ onFailure: async (errorMsg) => {
229
920
  setError(errorMsg);
230
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
+ }
231
928
  // Legacy callback (backwards compatibility)
232
929
  options.onFailure?.(errorMsg);
233
930
  // Funnel-aligned callback (recommended)
@@ -262,9 +959,10 @@ export function usePaymentQuery() {
262
959
  try {
263
960
  // 1. Create payment instrument
264
961
  const paymentInstrument = await createCardPaymentInstrument(cardData);
265
- // 2. Create 3DS session if enabled
962
+ // 2. Create 3DS session if enabled (use payment flow setting if not explicitly provided)
963
+ const shouldCreateThreedsSession = storeConfig?.computed?.threedsEnabled;
266
964
  let threedsSessionId;
267
- if (options.enableThreeds !== false) {
965
+ if (shouldCreateThreedsSession) {
268
966
  try {
269
967
  const threedsSession = await createSession(paymentInstrument, {
270
968
  provider: options.threedsProvider || 'basis_theory',
@@ -315,6 +1013,30 @@ export function usePaymentQuery() {
315
1013
  throw _error;
316
1014
  }
317
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]);
318
1040
  // Process payment with existing instrument - matches old implementation
319
1041
  const processPaymentWithInstrument = useCallback(async (checkoutSessionId, paymentInstrumentId, options = {}) => {
320
1042
  setIsLoading(true);
@@ -354,9 +1076,11 @@ export function usePaymentQuery() {
354
1076
  return {
355
1077
  processCardPayment,
356
1078
  processApplePayPayment,
1079
+ processGooglePayPayment,
357
1080
  processPaymentWithInstrument,
358
1081
  createCardPaymentInstrument,
359
1082
  createApplePayPaymentInstrument,
1083
+ createGooglePayPaymentInstrument,
360
1084
  getCardPaymentInstruments,
361
1085
  isLoading: isLoading || !basisTheory, // Indicate loading if BasisTheory is not initialized
362
1086
  error,