@tagadapay/plugin-sdk 3.1.22 → 3.1.25

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 (89) hide show
  1. package/build-cdn.js +274 -6
  2. package/dist/external-tracker.js +476 -6774
  3. package/dist/external-tracker.min.js +2 -25
  4. package/dist/external-tracker.min.js.map +4 -4
  5. package/dist/react/config/payment.d.ts +14 -4
  6. package/dist/react/config/payment.js +47 -9
  7. package/dist/react/hooks/useCheckout.d.ts +3 -0
  8. package/dist/react/hooks/useCheckout.js +11 -3
  9. package/dist/react/hooks/usePluginConfig.js +9 -10
  10. package/dist/react/providers/TagadaProvider.js +1 -1
  11. package/dist/tagada-react-sdk-minimal.min.js +36 -0
  12. package/dist/tagada-react-sdk-minimal.min.js.map +7 -0
  13. package/dist/tagada-react-sdk.js +37988 -0
  14. package/dist/tagada-react-sdk.min.js +78 -0
  15. package/dist/tagada-react-sdk.min.js.map +7 -0
  16. package/dist/tagada-sdk.js +7847 -6420
  17. package/dist/tagada-sdk.min.js +4 -22
  18. package/dist/tagada-sdk.min.js.map +4 -4
  19. package/dist/v2/cdn-react-minimal.d.ts +23 -0
  20. package/dist/v2/cdn-react-minimal.js +26 -0
  21. package/dist/v2/core/client.js +2 -1
  22. package/dist/v2/core/config/environment.js +2 -1
  23. package/dist/v2/core/funnelClient.d.ts +106 -10
  24. package/dist/v2/core/funnelClient.js +122 -28
  25. package/dist/v2/core/index.d.ts +0 -1
  26. package/dist/v2/core/index.js +0 -2
  27. package/dist/v2/core/isoData.d.ts +4 -4
  28. package/dist/v2/core/isoData.js +7 -7
  29. package/dist/v2/core/pixelMapping.js +64 -26
  30. package/dist/v2/core/resources/apiClient.d.ts +18 -14
  31. package/dist/v2/core/resources/apiClient.js +151 -109
  32. package/dist/v2/core/resources/checkout.d.ts +10 -0
  33. package/dist/v2/core/resources/checkout.js +6 -0
  34. package/dist/v2/core/resources/expressPaymentMethods.d.ts +1 -0
  35. package/dist/v2/core/resources/index.d.ts +1 -1
  36. package/dist/v2/core/resources/index.js +1 -1
  37. package/dist/v2/core/resources/offers.js +4 -4
  38. package/dist/v2/core/resources/payments.d.ts +8 -2
  39. package/dist/v2/core/resources/payments.js +1 -0
  40. package/dist/v2/core/resources/postPurchases.d.ts +17 -0
  41. package/dist/v2/core/resources/postPurchases.js +20 -0
  42. package/dist/v2/core/utils/currency.d.ts +3 -0
  43. package/dist/v2/core/utils/currency.js +40 -2
  44. package/dist/v2/core/utils/deviceInfo.d.ts +1 -10
  45. package/dist/v2/core/utils/deviceInfo.js +153 -76
  46. package/dist/v2/core/utils/order.d.ts +2 -0
  47. package/dist/v2/core/utils/pluginConfig.js +18 -22
  48. package/dist/v2/core/utils/previewMode.js +12 -0
  49. package/dist/v2/index.d.ts +4 -3
  50. package/dist/v2/index.js +4 -2
  51. package/dist/v2/react/components/ApplePayButton.js +39 -16
  52. package/dist/v2/react/components/FunnelScriptInjector.js +145 -77
  53. package/dist/v2/react/components/StripeExpressButton.d.ts +13 -0
  54. package/dist/v2/react/components/StripeExpressButton.js +170 -0
  55. package/dist/v2/react/components/WhopCheckout.js +7 -1
  56. package/dist/v2/react/hooks/payment-actions/useAirwallexRadarAction.js +1 -0
  57. package/dist/v2/react/hooks/payment-actions/useProcessorAuthAction.js +21 -3
  58. package/dist/v2/react/hooks/useApiQuery.d.ts +1 -1
  59. package/dist/v2/react/hooks/useApiQuery.js +1 -1
  60. package/dist/v2/react/hooks/useApplePayCheckout.js +8 -8
  61. package/dist/v2/react/hooks/useCheckoutQuery.d.ts +10 -0
  62. package/dist/v2/react/hooks/useCheckoutQuery.js +27 -15
  63. package/dist/v2/react/hooks/useFunnel.d.ts +15 -4
  64. package/dist/v2/react/hooks/useFunnel.js +8 -4
  65. package/dist/v2/react/hooks/useGoogleAutocomplete.d.ts +2 -0
  66. package/dist/v2/react/hooks/useGoogleAutocomplete.js +29 -15
  67. package/dist/v2/react/hooks/useISOData.d.ts +2 -5
  68. package/dist/v2/react/hooks/useISOData.js +25 -26
  69. package/dist/v2/react/hooks/usePaymentPolling.d.ts +2 -2
  70. package/dist/v2/react/hooks/usePixelTracking.js +151 -70
  71. package/dist/v2/react/hooks/usePostPurchasesQuery.js +34 -2
  72. package/dist/v2/react/hooks/usePreviewOffer.js +1 -1
  73. package/dist/v2/react/hooks/useRemappableParams.d.ts +2 -6
  74. package/dist/v2/react/hooks/useRemappableParams.js +23 -23
  75. package/dist/v2/react/hooks/useSetPaymentMethod.d.ts +16 -0
  76. package/dist/v2/react/hooks/useSetPaymentMethod.js +33 -0
  77. package/dist/v2/react/hooks/useStepConfig.d.ts +23 -6
  78. package/dist/v2/react/hooks/useStepConfig.js +14 -7
  79. package/dist/v2/react/hooks/useTranslation.js +23 -8
  80. package/dist/v2/react/index.d.ts +8 -1
  81. package/dist/v2/react/index.js +3 -0
  82. package/dist/v2/react/providers/ExpressPaymentMethodsProvider.d.ts +8 -0
  83. package/dist/v2/react/providers/ExpressPaymentMethodsProvider.js +106 -10
  84. package/dist/v2/react/providers/TagadaProvider.js +5 -5
  85. package/dist/v2/standalone/index.d.ts +21 -3
  86. package/dist/v2/standalone/index.js +25 -3
  87. package/dist/v2/standalone/payment-service.d.ts +134 -0
  88. package/dist/v2/standalone/payment-service.js +929 -0
  89. package/package.json +4 -2
@@ -24,6 +24,30 @@ const applePayContactToAddress = (contact) => {
24
24
  email: contact?.emailAddress || '',
25
25
  };
26
26
  };
27
+ const APPLE_PAY_SDK_URL = 'https://applepay.cdn-apple.com/jsapi/1.latest/apple-pay-sdk.js';
28
+ let applePaySdkPromise = null;
29
+ function loadApplePaySdk() {
30
+ if (applePaySdkPromise)
31
+ return applePaySdkPromise;
32
+ // Check if the script is already in the DOM (e.g. from a previous load)
33
+ if (document.querySelector(`script[src="${APPLE_PAY_SDK_URL}"]`)) {
34
+ applePaySdkPromise = Promise.resolve();
35
+ return applePaySdkPromise;
36
+ }
37
+ applePaySdkPromise = new Promise((resolve, reject) => {
38
+ const script = document.createElement('script');
39
+ script.src = APPLE_PAY_SDK_URL;
40
+ script.crossOrigin = 'anonymous';
41
+ script.async = true;
42
+ script.onload = () => resolve();
43
+ script.onerror = () => {
44
+ applePaySdkPromise = null;
45
+ reject(new Error('[ApplePay] Failed to load Apple Pay SDK'));
46
+ };
47
+ document.head.appendChild(script);
48
+ });
49
+ return applePaySdkPromise;
50
+ }
27
51
  export const ApplePayButton = ({ checkout, onSuccess, onError, onCancel }) => {
28
52
  const { applePayPaymentMethod, reComputeOrderSummary, shippingMethods, lineItems, handleAddExpressId, updateCheckoutSessionValues, updateCustomerEmail, setError: setContextError, } = useExpressPaymentMethods();
29
53
  const [processingPayment, setProcessingPayment] = useState(false);
@@ -38,25 +62,24 @@ export const ApplePayButton = ({ checkout, onSuccess, onError, onCancel }) => {
38
62
  if (!applePayPaymentMethod) {
39
63
  return null;
40
64
  }
41
- // Check Apple Pay availability (matches CMS pattern - useApplePayAvailable hook)
65
+ // Load SDK on demand, then check Apple Pay availability
42
66
  useEffect(() => {
43
- const addExpress = () => handleAddExpressId('apple_pay');
44
- try {
45
- // Apple Pay requires a secure context (HTTPS). On HTTP (like localhost),
46
- // calling canMakePayments() throws InvalidAccessError
47
- if (window?.ApplePaySession && ApplePaySession.canMakePayments()) {
48
- setIsApplePayAvailable(true);
49
- addExpress();
67
+ let cancelled = false;
68
+ const checkAvailability = () => {
69
+ try {
70
+ if (!cancelled && window?.ApplePaySession && ApplePaySession.canMakePayments()) {
71
+ setIsApplePayAvailable(true);
72
+ handleAddExpressId('apple_pay');
73
+ }
50
74
  }
51
- else {
52
- setIsApplePayAvailable(false);
75
+ catch (error) {
76
+ console.warn('[ApplePay] Apple Pay not available:', error);
53
77
  }
54
- }
55
- catch (error) {
56
- // Likely "Trying to start an Apple Pay session from an insecure document"
57
- console.warn('[ApplePay] Apple Pay not available:', error);
58
- setIsApplePayAvailable(false);
59
- }
78
+ };
79
+ loadApplePaySdk()
80
+ .then(checkAvailability)
81
+ .catch(() => { });
82
+ return () => { cancelled = true; };
60
83
  }, [handleAddExpressId]);
61
84
  // Helper to convert minor units to currency string
62
85
  const minorUnitsToCurrencyString = useCallback((amountMinor, currency) => {
@@ -1,6 +1,100 @@
1
1
  'use client';
2
2
  import { useCallback, useEffect, useMemo, useRef } from 'react';
3
3
  import { getAssignedStepConfig } from '../../core';
4
+ /**
5
+ * Parse script content that may contain multiple <script> and <noscript> tags.
6
+ * Handles: external scripts (<script src="...">), inline scripts, noscript blocks,
7
+ * and bare JS without any HTML tags.
8
+ */
9
+ function parseScriptContent(content) {
10
+ const trimmed = content.trim();
11
+ // If content doesn't contain any HTML script/noscript tags, treat as raw JS
12
+ if (!/<(?:script|noscript)[\s>]/i.test(trimmed)) {
13
+ return trimmed ? [{ type: 'inline', code: trimmed }] : [];
14
+ }
15
+ const elements = [];
16
+ const tagRegex = /<(script|noscript)([^>]*)>([\s\S]*?)<\/\1>/gi;
17
+ let lastIndex = 0;
18
+ let match;
19
+ while ((match = tagRegex.exec(trimmed)) !== null) {
20
+ // Capture any bare text between tags (could be JS or HTML comments)
21
+ const between = trimmed.slice(lastIndex, match.index).trim();
22
+ // Skip HTML comments and empty strings
23
+ if (between && !/^<!--[\s\S]*?-->$/.test(between)) {
24
+ elements.push({ type: 'inline', code: between });
25
+ }
26
+ const [, tagName, attrs, body] = match;
27
+ if (tagName.toLowerCase() === 'noscript') {
28
+ if (body.trim()) {
29
+ elements.push({ type: 'noscript', html: body.trim() });
30
+ }
31
+ }
32
+ else {
33
+ // Script tag — check for src attribute (external script)
34
+ const srcMatch = attrs.match(/src=["']([^"']+)["']/i);
35
+ if (srcMatch) {
36
+ elements.push({
37
+ type: 'external',
38
+ src: srcMatch[1],
39
+ async: /\basync\b/i.test(attrs),
40
+ defer: /\bdefer\b/i.test(attrs),
41
+ });
42
+ }
43
+ // Inline content (can exist alongside src, though unusual)
44
+ if (body.trim()) {
45
+ elements.push({ type: 'inline', code: body.trim() });
46
+ }
47
+ }
48
+ lastIndex = match.index + match[0].length;
49
+ }
50
+ // Trailing content after last tag
51
+ const trailing = trimmed.slice(lastIndex).trim();
52
+ if (trailing && !/^<!--[\s\S]*?-->$/.test(trailing)) {
53
+ elements.push({ type: 'inline', code: trailing });
54
+ }
55
+ return elements;
56
+ }
57
+ /** Wrap inline JS in an error-handling IIFE */
58
+ function wrapInErrorHandler(scriptName, code) {
59
+ return `
60
+ (function() {
61
+ try {
62
+ // Script: ${scriptName}
63
+ ${code}
64
+ } catch (error) {
65
+ console.error('[TagadaPay] StepConfig script "${scriptName}" error:', error);
66
+ }
67
+ })();
68
+ `;
69
+ }
70
+ /** Inject a DOM element at the specified position */
71
+ function injectAtPosition(element, position) {
72
+ switch (position) {
73
+ case 'head-start':
74
+ if (document.head.firstChild) {
75
+ document.head.insertBefore(element, document.head.firstChild);
76
+ }
77
+ else {
78
+ document.head.appendChild(element);
79
+ }
80
+ break;
81
+ case 'head-end':
82
+ document.head.appendChild(element);
83
+ break;
84
+ case 'body-start':
85
+ if (document.body.firstChild) {
86
+ document.body.insertBefore(element, document.body.firstChild);
87
+ }
88
+ else {
89
+ document.body.appendChild(element);
90
+ }
91
+ break;
92
+ case 'body-end':
93
+ default:
94
+ document.body.appendChild(element);
95
+ break;
96
+ }
97
+ }
4
98
  /**
5
99
  * FunnelScriptInjector - Handles injection of funnel scripts into the page.
6
100
  *
@@ -167,6 +261,9 @@ export function FunnelScriptInjector({ context, isInitialized }) {
167
261
  pageType: context?.currentStepId || null,
168
262
  isInitialized: isInitialized,
169
263
  ressources: context?.resources || null,
264
+ // Convenience getters so scripts can use Tagada.order / Tagada.checkout
265
+ get order() { return this.ressources?.order || null; },
266
+ get checkout() { return this.ressources?.checkout || null; },
170
267
  funnel: context
171
268
  ? {
172
269
  sessionId: context.sessionId,
@@ -220,28 +317,18 @@ export function FunnelScriptInjector({ context, isInitialized }) {
220
317
  existingScript.remove();
221
318
  }
222
319
  // Wrap script content with error handling and context checks
223
- const wrappedScript = `
224
- (function() {
225
- try {
226
- // Check if we have basic DOM access
227
- if (typeof document === 'undefined') {
228
- console.error('[TagadaPay] Document not available');
229
- return;
230
- }
231
-
232
- // Check if we have Tagada
233
- if (!window.Tagada) {
234
- console.error('[TagadaPay] Tagada not available');
235
- return;
236
- }
237
-
238
- // Execute the original script
239
- ${scriptBody}
240
- } catch (error) {
241
- console.error('[TagadaPay] Script execution error:', error);
242
- }
243
- })();
244
- `;
320
+ // NOTE: We use indirect eval (0,eval)() so that SyntaxErrors in user scripts
321
+ // are thrown at runtime (catchable by try/catch) rather than at parse time (uncatchable).
322
+ // JSON.stringify safely escapes the script content (backticks, quotes, ${...}, etc.)
323
+ const wrappedScript = '(function() {\n' +
324
+ ' try {\n' +
325
+ ' if (typeof document === "undefined") { console.error("[TagadaPay] Document not available"); return; }\n' +
326
+ ' if (!window.Tagada) { console.error("[TagadaPay] Tagada not available"); return; }\n' +
327
+ ' (0, eval)(' + JSON.stringify(scriptBody) + ');\n' +
328
+ ' } catch (error) {\n' +
329
+ ' console.error("[TagadaPay] Script execution error:", error);\n' +
330
+ ' }\n' +
331
+ '})();';
245
332
  // Create and inject new script element
246
333
  const scriptElement = document.createElement('script');
247
334
  scriptElement.id = scriptId;
@@ -284,62 +371,43 @@ export function FunnelScriptInjector({ context, isInitialized }) {
284
371
  // Inject each enabled script at its correct position
285
372
  stepConfigScripts.forEach((script, index) => {
286
373
  const position = script.position || 'head-end';
287
- const scriptId = `tagada-stepconfig-script-${index}`;
288
- // Extract script content (remove <script> tags if present)
289
- let scriptBody = script.content.trim();
290
- const scriptTagMatch = scriptBody.match(/^<script[^>]*>([\s\S]*)<\/script>$/i);
291
- if (scriptTagMatch) {
292
- scriptBody = scriptTagMatch[1].trim();
293
- }
294
- if (!scriptBody)
374
+ const content = script.content.trim();
375
+ if (!content)
295
376
  return;
296
- // Wrap script content with error handling
297
- const wrappedScript = `
298
- (function() {
299
- try {
300
- // Script: ${script.name}
301
- ${scriptBody}
302
- } catch (error) {
303
- console.error('[TagadaPay] StepConfig script "${script.name}" error:', error);
304
- }
305
- })();
306
- `;
307
- // Create script element
308
- const scriptElement = document.createElement('script');
309
- scriptElement.id = scriptId;
310
- scriptElement.setAttribute('data-tagada-stepconfig-script', 'true');
311
- scriptElement.setAttribute('data-script-name', script.name);
312
- scriptElement.textContent = wrappedScript;
313
- // Inject at the correct position
314
- switch (position) {
315
- case 'head-start':
316
- // Insert at the beginning of <head>
317
- if (document.head.firstChild) {
318
- document.head.insertBefore(scriptElement, document.head.firstChild);
319
- }
320
- else {
321
- document.head.appendChild(scriptElement);
322
- }
323
- break;
324
- case 'head-end':
325
- // Insert at the end of <head>
326
- document.head.appendChild(scriptElement);
327
- break;
328
- case 'body-start':
329
- // Insert at the beginning of <body>
330
- if (document.body.firstChild) {
331
- document.body.insertBefore(scriptElement, document.body.firstChild);
332
- }
333
- else {
334
- document.body.appendChild(scriptElement);
335
- }
336
- break;
337
- case 'body-end':
338
- default:
339
- // Insert at the end of <body>
340
- document.body.appendChild(scriptElement);
341
- break;
342
- }
377
+ // Parse content into individual elements (handles multi-tag scripts)
378
+ const parsed = parseScriptContent(content);
379
+ parsed.forEach((element, elemIndex) => {
380
+ const elemId = `tagada-stepconfig-script-${index}-${elemIndex}`;
381
+ if (element.type === 'external') {
382
+ const el = document.createElement('script');
383
+ el.id = elemId;
384
+ el.setAttribute('data-tagada-stepconfig-script', 'true');
385
+ el.setAttribute('data-script-name', script.name);
386
+ el.src = element.src;
387
+ if (element.async)
388
+ el.async = true;
389
+ if (element.defer)
390
+ el.defer = true;
391
+ injectAtPosition(el, position);
392
+ }
393
+ else if (element.type === 'inline') {
394
+ // Inline script wrap in error handler
395
+ const el = document.createElement('script');
396
+ el.id = elemId;
397
+ el.setAttribute('data-tagada-stepconfig-script', 'true');
398
+ el.setAttribute('data-script-name', script.name);
399
+ el.textContent = wrapInErrorHandler(script.name, element.code);
400
+ injectAtPosition(el, position);
401
+ }
402
+ else if (element.type === 'noscript') {
403
+ // Noscript block — inject as <noscript> element
404
+ const el = document.createElement('noscript');
405
+ el.id = elemId;
406
+ el.setAttribute('data-tagada-stepconfig-script', 'true');
407
+ el.innerHTML = element.html;
408
+ injectAtPosition(el, position);
409
+ }
410
+ });
343
411
  });
344
412
  // Track injected scripts to prevent re-injection
345
413
  lastInjectedStepConfigScriptsRef.current = scriptsHash;
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+ import type { CheckoutData } from '../../core/resources/checkout';
3
+ export interface StripeExpressButtonProps {
4
+ checkout: CheckoutData;
5
+ onSuccess?: (result: {
6
+ payment: any;
7
+ order: any;
8
+ }) => void;
9
+ onError?: (error: string) => void;
10
+ onCancel?: () => void;
11
+ }
12
+ export declare const StripeExpressButton: React.FC<StripeExpressButtonProps>;
13
+ export default StripeExpressButton;
@@ -0,0 +1,170 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Elements, ExpressCheckoutElement, useElements, useStripe } from '@stripe/react-stripe-js';
3
+ import { loadStripe } from '@stripe/stripe-js';
4
+ import { useMemo, useRef, useState } from 'react';
5
+ import { PaymentsResource } from '../../core/resources/payments';
6
+ import { useExpressPaymentMethods } from '../hooks/useExpressPaymentMethods';
7
+ import { usePaymentPolling } from '../hooks/usePaymentPolling';
8
+ import { getGlobalApiClient } from '../hooks/useApiQuery';
9
+ // Express method keys — drives processorGroup detection and ECE paymentMethods options.
10
+ // 'klarna_express' is the CRM config key for Klarna via ECE, distinct from 'klarna' (redirect flow).
11
+ // Backward compatible: existing configs with only apple_pay/google_pay continue to match.
12
+ const EXPRESS_METHOD_KEYS = ['apple_pay', 'google_pay', 'link', 'klarna_express'];
13
+ // Inner component — must be a child of <Elements> to use useStripe() and useElements()
14
+ function StripeExpressButtonInner({ checkout, processorId, enabledExpressMethods, onSuccess, onError, onCancel, }) {
15
+ const stripe = useStripe();
16
+ const elements = useElements();
17
+ const { handleAddExpressId, updateCheckoutSessionValues, updateCustomerEmail } = useExpressPaymentMethods();
18
+ const { startPolling } = usePaymentPolling();
19
+ const isProcessingRef = useRef(false);
20
+ const [visible, setVisible] = useState(false);
21
+ const paymentsResource = useMemo(() => new PaymentsResource(getGlobalApiClient()), []);
22
+ const onConfirm = async (event) => {
23
+ if (!stripe || !elements || isProcessingRef.current)
24
+ return;
25
+ isProcessingRef.current = true;
26
+ // Use the wallet type directly from Stripe's event — 'apple_pay', 'google_pay', 'paypal', etc.
27
+ const paymentMethod = event.expressPaymentType;
28
+ try {
29
+ // Step 1: save billing address + email from the wallet to the session/customer record.
30
+ // This mirrors what ApplePayButton and GooglePayButton do before processing payment,
31
+ // and ensures customer.email is present for the backend email validation.
32
+ const billing = event.billingDetails;
33
+ if (billing) {
34
+ const nameParts = (billing.name ?? '').trim().split(/\s+/);
35
+ const firstName = nameParts[0] ?? '';
36
+ const lastName = nameParts.slice(1).join(' ');
37
+ const billingAddress = {
38
+ firstName,
39
+ lastName,
40
+ address1: billing.address?.line1 ?? '',
41
+ address2: billing.address?.line2 ?? '',
42
+ city: billing.address?.city ?? '',
43
+ state: billing.address?.state ?? '',
44
+ country: billing.address?.country ?? '',
45
+ postal: billing.address?.postal_code ?? '',
46
+ phone: billing.phone ?? '',
47
+ email: billing.email ?? '',
48
+ };
49
+ await updateCheckoutSessionValues({
50
+ data: {
51
+ shippingAddress: billingAddress,
52
+ billingAddress,
53
+ },
54
+ });
55
+ }
56
+ if (billing?.email) {
57
+ await updateCustomerEmail({ data: { email: billing.email } });
58
+ }
59
+ // Step 2: validate the Elements form
60
+ const { error: submitError } = await elements.submit();
61
+ if (submitError) {
62
+ onError?.(submitError.message ?? 'Validation failed');
63
+ isProcessingRef.current = false;
64
+ return;
65
+ }
66
+ // Step 3: call backend via existing APM path
67
+ // paymentsResource.processPaymentDirect returns the raw PaymentResponse (no action routing)
68
+ const response = await paymentsResource.processPaymentDirect(checkout.checkoutSession.id, '', // empty — backend identifies via processorId + paymentMethod
69
+ undefined, {
70
+ processorId,
71
+ paymentMethod,
72
+ isExpress: true,
73
+ });
74
+ const clientSecret = response?.payment?.requireActionData?.metadata?.stripeExpressCheckout?.clientSecret;
75
+ if (!clientSecret) {
76
+ onError?.('Express checkout configuration missing — no client secret returned');
77
+ isProcessingRef.current = false;
78
+ return;
79
+ }
80
+ // Step 4: confirm the PaymentIntent inline — must happen while the Apple Pay sheet is open
81
+ const { error: confirmError } = await stripe.confirmPayment({
82
+ elements,
83
+ clientSecret,
84
+ confirmParams: {
85
+ // redirect: 'if_required' avoids a full-page redirect for wallet payments
86
+ return_url: window.location.href,
87
+ },
88
+ redirect: 'if_required',
89
+ });
90
+ if (confirmError) {
91
+ onError?.(confirmError.message ?? 'Payment confirmation failed');
92
+ isProcessingRef.current = false;
93
+ return;
94
+ }
95
+ // Step 5: poll for the final status — the webhook (payment_intent.succeeded) updates the DB
96
+ const paymentId = response.payment.id;
97
+ startPolling(paymentId, {
98
+ onRequireAction: (payment, stop) => {
99
+ // The payment still has requireAction='stripe_express_checkout' from when we created
100
+ // the intent — this has already been handled inline by stripe.confirmPayment.
101
+ // Ignore it and let polling continue waiting for the webhook to mark it succeeded.
102
+ if (payment.requireActionData?.type === 'stripe_express_checkout')
103
+ return;
104
+ // Any other unexpected action type: stop and surface as error
105
+ stop();
106
+ isProcessingRef.current = false;
107
+ onError?.('Unexpected payment action required');
108
+ },
109
+ onSuccess: async (completedPayment) => {
110
+ isProcessingRef.current = false;
111
+ onSuccess?.({ payment: completedPayment, order: completedPayment.order });
112
+ },
113
+ onFailure: async (errorMsg) => {
114
+ isProcessingRef.current = false;
115
+ onError?.(errorMsg);
116
+ },
117
+ });
118
+ }
119
+ catch (err) {
120
+ isProcessingRef.current = false;
121
+ const msg = err instanceof Error ? err.message : 'Express checkout failed';
122
+ console.error('[StripeExpressButton] Payment error:', err);
123
+ onError?.(msg);
124
+ }
125
+ };
126
+ return (_jsx("div", { style: { visibility: visible ? 'visible' : 'hidden' }, children: _jsx(ExpressCheckoutElement, { onReady: ({ availablePaymentMethods }) => {
127
+ if (availablePaymentMethods) {
128
+ setVisible(true);
129
+ handleAddExpressId('stripe_express_checkout');
130
+ }
131
+ }, onConfirm: onConfirm, onCancel: () => {
132
+ isProcessingRef.current = false;
133
+ onCancel?.();
134
+ }, options: {
135
+ buttonType: {
136
+ applePay: 'buy',
137
+ googlePay: 'buy',
138
+ },
139
+ buttonHeight: 48,
140
+ paymentMethods: {
141
+ applePay: enabledExpressMethods.includes('apple_pay') ? 'always' : 'never',
142
+ googlePay: enabledExpressMethods.includes('google_pay') ? 'always' : 'never',
143
+ link: enabledExpressMethods.includes('link') ? 'auto' : 'never',
144
+ klarna: enabledExpressMethods.includes('klarna_express') ? 'auto' : 'never',
145
+ },
146
+ emailRequired: true,
147
+ billingAddressRequired: true,
148
+ } }) }));
149
+ }
150
+ // Outer component — resolves Stripe instance and provides <Elements> context
151
+ export const StripeExpressButton = (props) => {
152
+ const { stripeExpressPaymentMethod } = useExpressPaymentMethods();
153
+ const processorGroup = stripeExpressPaymentMethod?.settings?.processors?.find((g) => EXPRESS_METHOD_KEYS.some((key) => g?.methods?.[key]?.enabled === true));
154
+ const processorId = processorGroup?.processorId;
155
+ const publishableKey = stripeExpressPaymentMethod?.settings?.publishableKey;
156
+ // Derive which express methods are enabled in this processor group
157
+ const enabledExpressMethods = EXPRESS_METHOD_KEYS.filter((key) => processorGroup?.methods?.[key]?.enabled === true);
158
+ const stripePromise = useMemo(() => (publishableKey ? loadStripe(publishableKey) : null), [publishableKey]);
159
+ if (!stripeExpressPaymentMethod || !processorId || !stripePromise)
160
+ return null;
161
+ const amount = Math.round(props.checkout.summary?.totalAdjustedAmount ?? 0);
162
+ const currency = (props.checkout.summary?.currency || 'usd').toLowerCase();
163
+ const elementsOptions = {
164
+ mode: 'payment',
165
+ amount,
166
+ currency,
167
+ };
168
+ return (_jsx(Elements, { stripe: stripePromise, options: elementsOptions, children: _jsx(StripeExpressButtonInner, { ...props, processorId: processorId, enabledExpressMethods: enabledExpressMethods }) }));
169
+ };
170
+ export default StripeExpressButton;
@@ -128,9 +128,15 @@ export const WhopCheckout = memo(forwardRef(({ checkoutSessionId, storeId, custo
128
128
  isWhopPaymentSubmitted.current = true;
129
129
  setError(null);
130
130
  try {
131
+ if (!planId) {
132
+ isWhopPaymentSubmitted.current = false;
133
+ setError('No plan available for payment.');
134
+ onPaymentFailed?.('No plan available for payment.');
135
+ return;
136
+ }
131
137
  const result = await apiService.fetch(`/api/v1/checkout-sessions/${checkoutSessionId}/whop-prepare-order`, {
132
138
  method: 'POST',
133
- body: JSON.stringify({ checkoutSessionId, storeId }),
139
+ body: JSON.stringify({ checkoutSessionId, storeId, planId }),
134
140
  headers: { 'Content-Type': 'application/json' },
135
141
  });
136
142
  if (!result) {
@@ -62,6 +62,7 @@ export function useAirwallexRadarAction({ paymentsResource, startPolling, setErr
62
62
  console.log('Airwallex device fingerprint session created:', sessionId);
63
63
  // Save radar session to database
64
64
  await paymentsResource.saveRadarSession({
65
+ paymentId: payment.id,
65
66
  checkoutSessionId,
66
67
  orderId,
67
68
  airwallexRadarSessionId: sessionId,
@@ -4,9 +4,27 @@
4
4
  import { useCallback } from 'react';
5
5
  export function useProcessorAuthAction() {
6
6
  const handleProcessorAuth = useCallback(async (actionData) => {
7
- // Always auto-redirect for processor auth (e.g., 3DS, external payment flows)
8
- if (actionData.metadata?.redirect?.redirectUrl) {
9
- window.location.href = actionData.metadata.redirect.redirectUrl;
7
+ const redirect = actionData.metadata?.redirect;
8
+ if (!redirect?.redirectUrl)
9
+ return;
10
+ if (redirect.method === 'POST') {
11
+ const form = document.createElement('form');
12
+ form.method = 'POST';
13
+ form.action = redirect.redirectUrl;
14
+ if (redirect.data && typeof redirect.data === 'object') {
15
+ Object.entries(redirect.data).forEach(([key, value]) => {
16
+ const input = document.createElement('input');
17
+ input.type = 'hidden';
18
+ input.name = key;
19
+ input.value = String(value);
20
+ form.appendChild(input);
21
+ });
22
+ }
23
+ document.body.appendChild(form);
24
+ form.submit();
25
+ }
26
+ else {
27
+ window.location.href = redirect.redirectUrl;
10
28
  }
11
29
  }, []);
12
30
  return { handleProcessorAuth };
@@ -1,5 +1,5 @@
1
1
  /**
2
- * API Query Hook using TanStack Query + Axios
2
+ * API Query Hook using TanStack Query + fetch
3
3
  * Facade pattern for React SDK
4
4
  */
5
5
  import { UseMutationOptions, UseQueryOptions } from '@tanstack/react-query';
@@ -1,5 +1,5 @@
1
1
  /**
2
- * API Query Hook using TanStack Query + Axios
2
+ * API Query Hook using TanStack Query + fetch
3
3
  * Facade pattern for React SDK
4
4
  */
5
5
  import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
@@ -120,12 +120,17 @@ export function useApplePayCheckout({ checkout, onSuccess, onError, onCancel, })
120
120
  setProcessingPayment(true);
121
121
  // Tokenize payment
122
122
  const applePayToken = await tokenizeApplePay(event);
123
- // Complete Apple Pay sheet
123
+ // Complete Apple Pay sheet immediately after tokenization
124
+ // Apple Pay requires completePayment within ~30s of authorization
124
125
  session.completePayment(window.ApplePaySession.STATUS_SUCCESS);
125
126
  // Process payment via SDK hook
126
- const result = await processApplePayPayment(checkout.checkoutSession.id, applePayToken, {
127
+ // onSuccess/redirect is gated through onPaymentSuccess callback only,
128
+ // so we never redirect until the backend confirms success
129
+ await processApplePayPayment(checkout.checkoutSession.id, applePayToken, {
127
130
  onPaymentSuccess: (response) => {
128
- // Keep processing state true during navigation
131
+ if (onSuccess) {
132
+ onSuccess(response);
133
+ }
129
134
  },
130
135
  onPaymentFailed: (err) => {
131
136
  setProcessingPayment(false);
@@ -135,14 +140,9 @@ export function useApplePayCheckout({ checkout, onSuccess, onError, onCancel, })
135
140
  }
136
141
  },
137
142
  });
138
- // Call success callback
139
- if (onSuccess) {
140
- onSuccess(result);
141
- }
142
143
  }
143
144
  catch (error) {
144
145
  console.error('Payment failed:', error);
145
- session.completePayment(window.ApplePaySession.STATUS_FAILURE);
146
146
  setProcessingPayment(false);
147
147
  const errorMsg = error instanceof Error ? error.message : 'Payment failed';
148
148
  setError(errorMsg);
@@ -3,9 +3,19 @@
3
3
  * Replaces the coordinator pattern with automatic cache invalidation
4
4
  */
5
5
  import { CheckoutData, CheckoutInitParams, CheckoutLineItem, CheckoutSessionPreview } from '../../core/resources/checkout';
6
+ export interface CheckoutMessages {
7
+ /** Error shown when CMS session takes too long to initialize. Default: 'Session initialization timeout. Please refresh the page and try again.' */
8
+ sessionTimeout?: string;
9
+ /** Error shown when checkout resource fails to initialize. Default: 'Failed to initialize checkout resource' */
10
+ initFailed?: string;
11
+ /** Error shown when an operation is attempted without an active checkout session. Default: 'No checkout session available' */
12
+ noCheckoutSession?: string;
13
+ }
6
14
  export interface UseCheckoutQueryOptions {
7
15
  checkoutToken?: string;
8
16
  enabled?: boolean;
17
+ /** Override default error messages (e.g. for i18n). Use with useTranslation's t() to pass translated strings. */
18
+ messages?: CheckoutMessages;
9
19
  }
10
20
  export interface UseCheckoutQueryResult {
11
21
  checkout: CheckoutData | undefined;