@tagadapay/plugin-sdk 3.1.8 → 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.
- package/README.md +1129 -1129
- package/build-cdn.js +220 -113
- package/dist/external-tracker.js +135 -81
- package/dist/external-tracker.min.js +2 -2
- package/dist/external-tracker.min.js.map +4 -4
- package/dist/react/providers/TagadaProvider.js +5 -5
- package/dist/tagada-sdk.js +10142 -0
- package/dist/tagada-sdk.min.js +43 -0
- package/dist/tagada-sdk.min.js.map +7 -0
- package/dist/v2/core/funnelClient.d.ts +91 -4
- package/dist/v2/core/funnelClient.js +42 -3
- package/dist/v2/core/resources/funnel.d.ts +10 -0
- package/dist/v2/core/resources/payments.d.ts +21 -1
- package/dist/v2/core/resources/payments.js +34 -0
- package/dist/v2/core/utils/index.d.ts +1 -0
- package/dist/v2/core/utils/index.js +2 -0
- package/dist/v2/core/utils/pluginConfig.d.ts +8 -0
- package/dist/v2/core/utils/pluginConfig.js +28 -0
- package/dist/v2/core/utils/previewMode.d.ts +4 -0
- package/dist/v2/core/utils/previewMode.js +28 -0
- package/dist/v2/core/utils/previewModeIndicator.js +101 -101
- package/dist/v2/index.d.ts +7 -6
- package/dist/v2/index.js +6 -6
- package/dist/v2/react/components/ApplePayButton.d.ts +1 -2
- package/dist/v2/react/components/ApplePayButton.js +57 -58
- package/dist/v2/react/components/FunnelScriptInjector.js +161 -172
- package/dist/v2/react/components/GooglePayButton.d.ts +2 -0
- package/dist/v2/react/components/GooglePayButton.js +80 -64
- package/dist/v2/react/hooks/useFunnel.d.ts +8 -2
- package/dist/v2/react/hooks/useFunnel.js +2 -2
- package/dist/v2/react/hooks/useGoogleAutocomplete.d.ts +10 -0
- package/dist/v2/react/hooks/useGoogleAutocomplete.js +48 -0
- package/dist/v2/react/hooks/useGooglePayCheckout.d.ts +21 -0
- package/dist/v2/react/hooks/useGooglePayCheckout.js +198 -0
- package/dist/v2/react/hooks/usePaymentPolling.d.ts +7 -1
- package/dist/v2/react/hooks/usePaymentQuery.d.ts +2 -0
- package/dist/v2/react/hooks/usePaymentQuery.js +435 -8
- package/dist/v2/react/hooks/usePixelTracking.d.ts +56 -0
- package/dist/v2/react/hooks/usePixelTracking.js +508 -0
- package/dist/v2/react/hooks/useStepConfig.d.ts +8 -6
- package/dist/v2/react/hooks/useStepConfig.js +3 -2
- package/dist/v2/react/index.d.ts +6 -2
- package/dist/v2/react/index.js +3 -1
- package/dist/v2/react/providers/ExpressPaymentMethodsProvider.d.ts +1 -0
- package/dist/v2/react/providers/ExpressPaymentMethodsProvider.js +33 -13
- package/dist/v2/react/providers/TagadaProvider.js +22 -21
- package/package.json +112 -112
|
@@ -37,6 +37,8 @@ export function usePaymentQuery(hookOptions) {
|
|
|
37
37
|
const { createSession, startChallenge } = useThreeds();
|
|
38
38
|
// Track challenge in progress to prevent multiple challenges
|
|
39
39
|
const challengeInProgressRef = useRef(false);
|
|
40
|
+
// Track if we've already processed a redirect return (prevents double-processing)
|
|
41
|
+
const redirectReturnProcessedRef = useRef(false);
|
|
40
42
|
// Get API key from embedded configuration with proper environment detection
|
|
41
43
|
const apiKey = useMemo(() => getBasisTheoryApiKey(), []); // Auto-detects environment
|
|
42
44
|
// Initialize BasisTheory using React wrapper
|
|
@@ -284,9 +286,27 @@ export function usePaymentQuery(hookOptions) {
|
|
|
284
286
|
const resumedPayment = await paymentsResource.completePaymentAfterAction(payment.id);
|
|
285
287
|
console.log('Payment resumed after Finix radar:', resumedPayment);
|
|
286
288
|
// Handle the resumed payment response
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
+
}
|
|
290
310
|
}
|
|
291
311
|
else if (resumedPayment.status === 'succeeded') {
|
|
292
312
|
// Payment succeeded
|
|
@@ -299,6 +319,10 @@ export function usePaymentQuery(hookOptions) {
|
|
|
299
319
|
options.onSuccess?.(response);
|
|
300
320
|
options.onPaymentSuccess?.(response);
|
|
301
321
|
}
|
|
322
|
+
else if (resumedPayment.requireAction !== 'none' && resumedPayment.requireActionData) {
|
|
323
|
+
// Payment requires another action (e.g., 3DS)
|
|
324
|
+
await handlePaymentAction(resumedPayment, options);
|
|
325
|
+
}
|
|
302
326
|
else {
|
|
303
327
|
// Start polling for final status
|
|
304
328
|
startPolling(resumedPayment.id, {
|
|
@@ -342,35 +366,408 @@ export function usePaymentQuery(hookOptions) {
|
|
|
342
366
|
}
|
|
343
367
|
break;
|
|
344
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
|
+
}
|
|
345
527
|
}
|
|
346
528
|
options.onRequireAction?.(payment);
|
|
347
529
|
}, [paymentsResource, startPolling, startChallenge]);
|
|
348
|
-
// Auto-detect payment action from URL parameters (after redirect from Stripe, PayPal, etc.)
|
|
530
|
+
// Auto-detect payment action from URL parameters (after redirect from Stripe, PayPal, Airwallex, etc.)
|
|
349
531
|
useEffect(() => {
|
|
350
532
|
if (typeof window === 'undefined')
|
|
351
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
|
+
}
|
|
352
539
|
const urlParams = new URLSearchParams(window.location.search);
|
|
353
540
|
const paymentAction = urlParams.get('paymentAction');
|
|
354
541
|
const paymentActionStatus = urlParams.get('paymentActionStatus');
|
|
355
542
|
const paymentIdFromUrl = urlParams.get('paymentId');
|
|
356
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');
|
|
357
548
|
console.log('🔍 [usePayment] Checking for payment redirect return...', {
|
|
358
549
|
paymentAction,
|
|
359
550
|
paymentActionStatus,
|
|
360
551
|
paymentId: paymentIdFromUrl,
|
|
361
552
|
mode: paymentMode,
|
|
553
|
+
paymentIntentId,
|
|
554
|
+
succeeded,
|
|
555
|
+
processorType,
|
|
362
556
|
url: window.location.href,
|
|
363
557
|
});
|
|
364
|
-
//
|
|
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
|
|
365
760
|
if (paymentMode === 'retrieve') {
|
|
366
|
-
console.log('⏭️ [usePayment] Skipping - retrieve mode detected');
|
|
761
|
+
console.log('⏭️ [usePayment] Skipping - retrieve mode detected (handled by usePaymentRetrieve)');
|
|
367
762
|
return;
|
|
368
763
|
}
|
|
369
|
-
// Check if returning from a payment redirect
|
|
764
|
+
// Check if returning from a payment redirect (generic handling)
|
|
370
765
|
if (paymentAction === 'requireAction' && paymentActionStatus === 'completed' && paymentIdFromUrl) {
|
|
371
766
|
console.log('✅ [usePayment] Payment redirect return detected! Starting auto-polling...', {
|
|
372
767
|
paymentId: paymentIdFromUrl,
|
|
373
768
|
});
|
|
769
|
+
// Mark as processed immediately to prevent double-processing
|
|
770
|
+
redirectReturnProcessedRef.current = true;
|
|
374
771
|
setIsLoading(true);
|
|
375
772
|
setCurrentPaymentId(paymentIdFromUrl);
|
|
376
773
|
// Start polling for the payment status
|
|
@@ -445,7 +842,7 @@ export function usePaymentQuery(hookOptions) {
|
|
|
445
842
|
else {
|
|
446
843
|
console.log('⏭️ [usePayment] No payment redirect detected - normal page load');
|
|
447
844
|
}
|
|
448
|
-
}, [startPolling, handlePaymentAction, setIsLoading, setError, setCurrentPaymentId]);
|
|
845
|
+
}, [paymentsResource, startPolling, handlePaymentAction, setIsLoading, setError, setCurrentPaymentId]);
|
|
449
846
|
// Create card payment instrument - matches old implementation
|
|
450
847
|
const createCardPaymentInstrument = useCallback((cardData) => {
|
|
451
848
|
return paymentsResource.createCardPaymentInstrument(basisTheory, cardData);
|
|
@@ -454,6 +851,10 @@ export function usePaymentQuery(hookOptions) {
|
|
|
454
851
|
const createApplePayPaymentInstrument = useCallback((applePayToken) => {
|
|
455
852
|
return paymentsResource.createApplePayPaymentInstrument(basisTheory, applePayToken);
|
|
456
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]);
|
|
457
858
|
// Process payment directly with checkout session - matches old implementation
|
|
458
859
|
const processPaymentDirect = useCallback(async (checkoutSessionId, paymentInstrumentId, threedsSessionId, options = {}) => {
|
|
459
860
|
try {
|
|
@@ -612,6 +1013,30 @@ export function usePaymentQuery(hookOptions) {
|
|
|
612
1013
|
throw _error;
|
|
613
1014
|
}
|
|
614
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]);
|
|
615
1040
|
// Process payment with existing instrument - matches old implementation
|
|
616
1041
|
const processPaymentWithInstrument = useCallback(async (checkoutSessionId, paymentInstrumentId, options = {}) => {
|
|
617
1042
|
setIsLoading(true);
|
|
@@ -651,9 +1076,11 @@ export function usePaymentQuery(hookOptions) {
|
|
|
651
1076
|
return {
|
|
652
1077
|
processCardPayment,
|
|
653
1078
|
processApplePayPayment,
|
|
1079
|
+
processGooglePayPayment,
|
|
654
1080
|
processPaymentWithInstrument,
|
|
655
1081
|
createCardPaymentInstrument,
|
|
656
1082
|
createApplePayPaymentInstrument,
|
|
1083
|
+
createGooglePayPaymentInstrument,
|
|
657
1084
|
getCardPaymentInstruments,
|
|
658
1085
|
isLoading: isLoading || !basisTheory, // Indicate loading if BasisTheory is not initialized
|
|
659
1086
|
error,
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* usePixelTracking Hook & Provider
|
|
3
|
+
*
|
|
4
|
+
* SDK-level pixel tracking based on runtime stepConfig.pixels injected
|
|
5
|
+
* by the CRM. This mirrors the CMS pixel context pattern but uses the
|
|
6
|
+
* funnel step configuration as the source of truth instead of store
|
|
7
|
+
* integrations.
|
|
8
|
+
*/
|
|
9
|
+
import React from 'react';
|
|
10
|
+
export type StandardPixelEvent = 'PageView' | 'ViewContent' | 'AddToCart' | 'InitiateCheckout' | 'AddPaymentInfo' | 'Purchase' | 'Lead' | 'CompleteRegistration' | 'Conversion';
|
|
11
|
+
export interface PixelTrackingContextValue {
|
|
12
|
+
track: (eventName: StandardPixelEvent, parameters?: Record<string, any>) => void;
|
|
13
|
+
pixelsInitialized: boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Provider that initializes pixels based on stepConfig.pixels
|
|
17
|
+
* and exposes a simple track(event, params) API.
|
|
18
|
+
*/
|
|
19
|
+
export declare function PixelTrackingProvider({ children }: {
|
|
20
|
+
children: React.ReactNode;
|
|
21
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
22
|
+
/**
|
|
23
|
+
* Hook to access SDK pixel tracking.
|
|
24
|
+
* Must be used within TagadaProvider (which wraps PixelTrackingProvider).
|
|
25
|
+
*/
|
|
26
|
+
export declare function usePixelTracking(): PixelTrackingContextValue;
|
|
27
|
+
declare global {
|
|
28
|
+
interface FacebookPixelFunction {
|
|
29
|
+
(...args: unknown[]): void;
|
|
30
|
+
callMethod?: (...args: unknown[]) => void;
|
|
31
|
+
queue?: unknown[];
|
|
32
|
+
loaded?: boolean;
|
|
33
|
+
version?: string;
|
|
34
|
+
}
|
|
35
|
+
interface TikTokPixelFunction {
|
|
36
|
+
(...args: unknown[]): void;
|
|
37
|
+
methods?: string[];
|
|
38
|
+
setAndDefer?: (target: TikTokPixelFunction, method: string) => void;
|
|
39
|
+
load?: (pixelId: string) => void;
|
|
40
|
+
page?: () => void;
|
|
41
|
+
track?: (eventName: string, params: Record<string, unknown>) => void;
|
|
42
|
+
queue?: unknown[];
|
|
43
|
+
}
|
|
44
|
+
interface SnapchatPixelFunction {
|
|
45
|
+
(...args: unknown[]): void;
|
|
46
|
+
handleRequest?: (...args: unknown[]) => void;
|
|
47
|
+
queue?: unknown[];
|
|
48
|
+
}
|
|
49
|
+
interface Window {
|
|
50
|
+
fbq?: FacebookPixelFunction;
|
|
51
|
+
_fbq?: FacebookPixelFunction;
|
|
52
|
+
ttq?: TikTokPixelFunction;
|
|
53
|
+
snaptr?: SnapchatPixelFunction;
|
|
54
|
+
dataLayer?: any[];
|
|
55
|
+
}
|
|
56
|
+
}
|