@tagadapay/plugin-sdk 3.1.24 → 4.0.0

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 (69) hide show
  1. package/README.md +1129 -1129
  2. package/build-cdn.js +499 -499
  3. package/dist/external-tracker.js +247 -2875
  4. package/dist/external-tracker.min.js +2 -2
  5. package/dist/external-tracker.min.js.map +4 -4
  6. package/dist/react/config/payment.d.ts +2 -2
  7. package/dist/react/config/payment.js +5 -5
  8. package/dist/react/hooks/useCheckout.js +7 -2
  9. package/dist/react/hooks/usePayment.d.ts +7 -0
  10. package/dist/react/hooks/usePayment.js +1 -0
  11. package/dist/react/providers/TagadaProvider.js +5 -5
  12. package/dist/tagada-react-sdk-minimal.min.js +2 -2
  13. package/dist/tagada-react-sdk-minimal.min.js.map +4 -4
  14. package/dist/tagada-react-sdk.js +1680 -1172
  15. package/dist/tagada-react-sdk.min.js +2 -2
  16. package/dist/tagada-react-sdk.min.js.map +4 -4
  17. package/dist/tagada-sdk.js +1701 -3410
  18. package/dist/tagada-sdk.min.js +2 -2
  19. package/dist/tagada-sdk.min.js.map +4 -4
  20. package/dist/v2/core/client.js +1 -0
  21. package/dist/v2/core/config/environment.d.ts +3 -3
  22. package/dist/v2/core/config/environment.js +7 -7
  23. package/dist/v2/core/funnelClient.d.ts +10 -0
  24. package/dist/v2/core/funnelClient.js +1 -1
  25. package/dist/v2/core/resources/apiClient.d.ts +18 -14
  26. package/dist/v2/core/resources/apiClient.js +151 -109
  27. package/dist/v2/core/resources/checkout.d.ts +1 -1
  28. package/dist/v2/core/resources/funnel.d.ts +1 -1
  29. package/dist/v2/core/resources/geo.d.ts +50 -0
  30. package/dist/v2/core/resources/geo.js +35 -0
  31. package/dist/v2/core/resources/index.d.ts +1 -1
  32. package/dist/v2/core/resources/index.js +1 -1
  33. package/dist/v2/core/resources/offers.js +4 -4
  34. package/dist/v2/core/resources/payments.d.ts +20 -1
  35. package/dist/v2/core/resources/payments.js +8 -0
  36. package/dist/v2/core/utils/currency.d.ts +3 -0
  37. package/dist/v2/core/utils/currency.js +40 -2
  38. package/dist/v2/core/utils/deviceInfo.d.ts +1 -0
  39. package/dist/v2/core/utils/deviceInfo.js +1 -0
  40. package/dist/v2/core/utils/previewMode.js +12 -0
  41. package/dist/v2/core/utils/previewModeIndicator.js +101 -101
  42. package/dist/v2/react/components/ApplePayButton.js +39 -16
  43. package/dist/v2/react/components/FunnelScriptInjector.js +167 -19
  44. package/dist/v2/react/components/StripeExpressButton.d.ts +8 -0
  45. package/dist/v2/react/components/StripeExpressButton.js +23 -3
  46. package/dist/v2/react/hooks/payment-actions/useAirwallexRadarAction.js +1 -0
  47. package/dist/v2/react/hooks/payment-actions/useNgeniusThreedsAction.d.ts +15 -0
  48. package/dist/v2/react/hooks/payment-actions/useNgeniusThreedsAction.js +166 -0
  49. package/dist/v2/react/hooks/payment-actions/usePaymentActionHandler.js +12 -0
  50. package/dist/v2/react/hooks/payment-processing/usePaymentProcessors.js +1 -0
  51. package/dist/v2/react/hooks/useApiQuery.d.ts +1 -1
  52. package/dist/v2/react/hooks/useApiQuery.js +1 -1
  53. package/dist/v2/react/hooks/useCheckoutQuery.js +6 -2
  54. package/dist/v2/react/hooks/useISOData.js +25 -7
  55. package/dist/v2/react/hooks/usePaymentPolling.d.ts +1 -1
  56. package/dist/v2/react/hooks/usePreviewOffer.js +1 -1
  57. package/dist/v2/react/providers/ExpressPaymentMethodsProvider.d.ts +7 -0
  58. package/dist/v2/react/providers/ExpressPaymentMethodsProvider.js +105 -9
  59. package/dist/v2/react/providers/TagadaProvider.js +6 -6
  60. package/dist/v2/standalone/apple-pay-service.d.ts +12 -0
  61. package/dist/v2/standalone/apple-pay-service.js +12 -0
  62. package/dist/v2/standalone/external-tracker.d.ts +1 -1
  63. package/dist/v2/standalone/google-pay-service.d.ts +9 -0
  64. package/dist/v2/standalone/google-pay-service.js +9 -0
  65. package/dist/v2/standalone/index.d.ts +8 -1
  66. package/dist/v2/standalone/index.js +7 -0
  67. package/dist/v2/standalone/payment-service.d.ts +18 -5
  68. package/dist/v2/standalone/payment-service.js +63 -9
  69. package/package.json +115 -114
@@ -1,6 +1,8 @@
1
1
  'use client';
2
2
  import { useCallback, useEffect, useMemo, useRef } from 'react';
3
3
  import { getAssignedStepConfig } from '../../core';
4
+ import { useCheckoutQuery } from '../hooks/useCheckoutQuery';
5
+ import { useOrderQuery } from '../hooks/useOrderQuery';
4
6
  /**
5
7
  * Parse script content that may contain multiple <script> and <noscript> tags.
6
8
  * Handles: external scripts (<script src="...">), inline scripts, noscript blocks,
@@ -56,15 +58,15 @@ function parseScriptContent(content) {
56
58
  }
57
59
  /** Wrap inline JS in an error-handling IIFE */
58
60
  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
- })();
61
+ return `
62
+ (function() {
63
+ try {
64
+ // Script: ${scriptName}
65
+ ${code}
66
+ } catch (error) {
67
+ console.error('[TagadaPay] StepConfig script "${scriptName}" error:', error);
68
+ }
69
+ })();
68
70
  `;
69
71
  }
70
72
  /** Inject a DOM element at the specified position */
@@ -110,6 +112,62 @@ export function FunnelScriptInjector({ context, isInitialized }) {
110
112
  // Track last injected scripts to prevent duplicate execution
111
113
  const lastInjectedScriptRef = useRef(null);
112
114
  const lastInjectedStepConfigScriptsRef = useRef(null);
115
+ // ─── Rich data bridging for window.Tagada (V1 parity) ───────────────
116
+ // The funnel context's resources only hold thin references. The rich
117
+ // checkout session (customer, items, amounts) and order data live in
118
+ // the shared React Query cache via useCheckoutQuery / useOrderQuery.
119
+ // Subscribe here and push onto window.Tagada so legacy scripts see the
120
+ // full shape they got in V1. useCheckoutQuery needs an explicit
121
+ // checkoutToken — read it from URL first, then fall back to the
122
+ // funnel-context resources.
123
+ const checkoutTokenFromUrlOrResources = useMemo(() => {
124
+ if (typeof window !== 'undefined') {
125
+ const fromUrl = new URLSearchParams(window.location.search).get('checkoutToken');
126
+ if (fromUrl)
127
+ return fromUrl;
128
+ }
129
+ const fromResources = context?.resources?.checkoutToken;
130
+ return typeof fromResources === 'string' ? fromResources : undefined;
131
+ }, [context?.resources]);
132
+ const { checkout: richCheckoutData } = useCheckoutQuery({
133
+ checkoutToken: checkoutTokenFromUrlOrResources,
134
+ enabled: !!checkoutTokenFromUrlOrResources,
135
+ });
136
+ const orderIdFromUrl = useMemo(() => {
137
+ if (typeof window === 'undefined')
138
+ return undefined;
139
+ const fromUrl = new URLSearchParams(window.location.search).get('orderId');
140
+ if (fromUrl)
141
+ return fromUrl;
142
+ // native-checkout v2 puts orderId in path /thankyou/<id>, not query.
143
+ // Funnel context resources already carry it — use that as fallback.
144
+ const fromResources = context?.resources?.order;
145
+ return fromResources?.id;
146
+ }, [context?.resources]);
147
+ const { order: richOrderData } = useOrderQuery({ orderId: orderIdFromUrl, enabled: !!orderIdFromUrl });
148
+ useEffect(() => {
149
+ if (typeof window === 'undefined')
150
+ return;
151
+ // @ts-expect-error - accessing window property
152
+ const T = window.Tagada;
153
+ if (!T)
154
+ return;
155
+ const prevRichCheckoutSession = T._richCheckoutSession || null;
156
+ const prevRichSummary = T._richOrderSummary || null;
157
+ const prevRichOrder = T._richOrder || null;
158
+ T._richCheckoutSession = richCheckoutData?.checkoutSession || null;
159
+ T._richOrderSummary = richCheckoutData?.summary || null;
160
+ T._richOrder = richOrderData || null;
161
+ if (T._richCheckoutSession !== prevRichCheckoutSession) {
162
+ T._fireCheckoutSessionUpdate?.({ checkoutSession: T._richCheckoutSession });
163
+ }
164
+ if (T._richOrderSummary !== prevRichSummary) {
165
+ T._fireOrderSummaryUpdate?.({ orderSummary: T._richOrderSummary });
166
+ }
167
+ if (T._richOrder !== prevRichOrder) {
168
+ T._fireOrderUpdate?.({ order: T._richOrder });
169
+ }
170
+ }, [richCheckoutData, richOrderData]);
113
171
  // Get stepConfig scripts from HTML injection or local config
114
172
  // Re-compute when initialized (local config loads async, so we need to re-check)
115
173
  const stepConfigScripts = useMemo(() => {
@@ -127,17 +185,47 @@ export function FunnelScriptInjector({ context, isInitialized }) {
127
185
  return;
128
186
  // @ts-expect-error - Adding utilities to window
129
187
  if (window.Tagada) {
188
+ // @ts-expect-error - Accessing window property
189
+ const prev = window.Tagada;
190
+ const prevResources = (prev.ressources || null);
191
+ const nextResources = (context?.resources || null);
130
192
  // Update properties if Tagada already exists
131
- if (context?.currentStepId) {
132
- // @ts-expect-error - Updating window property
133
- window.Tagada.pageType = context.currentStepId;
193
+ prev.pageType = context?.currentStepId || null;
194
+ prev.isInitialized = isInitialized;
195
+ prev.ressources = nextResources;
196
+ prev.stepConfig = getAssignedStepConfig() || null;
197
+ prev.funnel = context
198
+ ? {
199
+ sessionId: context.sessionId,
200
+ funnelId: context.funnelId,
201
+ currentStepId: context.currentStepId,
202
+ previousStepId: context.previousStepId,
203
+ ressources: context.resources,
204
+ }
205
+ : null;
206
+ // Fire V1-compat listeners when specific resources change.
207
+ // Support both V1 ("checkout") and native V2 ("checkoutSession") keys.
208
+ const prevCheckout = prevResources?.checkoutSession || prevResources?.checkout;
209
+ const nextCheckout = nextResources?.checkoutSession || nextResources?.checkout;
210
+ if (prevCheckout !== nextCheckout) {
211
+ prev._fireCheckoutSessionUpdate?.({ checkoutSession: nextCheckout || null });
212
+ }
213
+ const prevSummary = prevResources?.orderSummary || prevResources?.summary;
214
+ const nextSummary = nextResources?.orderSummary || nextResources?.summary;
215
+ if (prevSummary !== nextSummary) {
216
+ prev._fireOrderSummaryUpdate?.({ orderSummary: nextSummary || null });
217
+ }
218
+ const prevOrder = prevResources?.order;
219
+ const nextOrder = nextResources?.order;
220
+ if (prevOrder !== nextOrder) {
221
+ prev._fireOrderUpdate?.({ order: nextOrder || null });
134
222
  }
135
- // @ts-expect-error - Updating window property
136
- window.Tagada.isInitialized = isInitialized;
137
- // @ts-expect-error - Updating window property
138
- window.Tagada.ressources = context?.resources || null;
139
223
  return;
140
224
  }
225
+ // Listener registries (captured via closures — outlive re-renders on window.Tagada)
226
+ const checkoutSessionListeners = new Set();
227
+ const orderSummaryListeners = new Set();
228
+ const orderListeners = new Set();
141
229
  // @ts-expect-error - Adding utilities to window
142
230
  window.Tagada = {
143
231
  // Wait for DOM to be ready
@@ -261,9 +349,36 @@ export function FunnelScriptInjector({ context, isInitialized }) {
261
349
  pageType: context?.currentStepId || null,
262
350
  isInitialized: isInitialized,
263
351
  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; },
352
+ // Rich data caches bridged from React Query (useCheckoutQuery / useOrderQuery).
353
+ // Populated by the useEffect above. Prefer these over thin funnel-context refs.
354
+ _richCheckoutSession: null,
355
+ _richOrderSummary: null,
356
+ _richOrder: null,
357
+ // V1-compat: Tagada.order and Tagada.checkout.session return full data.
358
+ // Prefer React-Query-fetched data, fall back to funnel context resources.
359
+ get order() {
360
+ return this._richOrder || this.ressources?.order || null;
361
+ },
362
+ get checkout() {
363
+ const ressources = this.ressources;
364
+ const richSession = this._richCheckoutSession;
365
+ const richSummary = this._richOrderSummary;
366
+ const resource = richSession ||
367
+ ressources?.checkoutSession ||
368
+ ressources?.checkout ||
369
+ null;
370
+ const orderSummary = richSummary ||
371
+ ressources?.orderSummary ||
372
+ ressources?.summary ||
373
+ null;
374
+ if (!resource && !orderSummary)
375
+ return null;
376
+ return {
377
+ ...(resource || {}),
378
+ session: resource,
379
+ orderSummary,
380
+ };
381
+ },
267
382
  funnel: context
268
383
  ? {
269
384
  sessionId: context.sessionId,
@@ -274,6 +389,39 @@ export function FunnelScriptInjector({ context, isInitialized }) {
274
389
  }
275
390
  : null,
276
391
  stepConfig: getAssignedStepConfig() || null,
392
+ // V1-compat reactive listeners. Fire when the corresponding resource
393
+ // reference changes in the funnel context (see update branch above).
394
+ onCheckoutSessionUpdate: (cb) => {
395
+ checkoutSessionListeners.add(cb);
396
+ return () => checkoutSessionListeners.delete(cb);
397
+ },
398
+ onOrderSummaryUpdate: (cb) => {
399
+ orderSummaryListeners.add(cb);
400
+ return () => orderSummaryListeners.delete(cb);
401
+ },
402
+ onOrderUpdate: (cb) => {
403
+ orderListeners.add(cb);
404
+ return () => orderListeners.delete(cb);
405
+ },
406
+ // Internal fire hooks used by the update branch
407
+ _fireCheckoutSessionUpdate: (data) => checkoutSessionListeners.forEach(cb => { try {
408
+ cb(data);
409
+ }
410
+ catch (e) {
411
+ console.error('[Tagada] onCheckoutSessionUpdate listener threw:', e);
412
+ } }),
413
+ _fireOrderSummaryUpdate: (data) => orderSummaryListeners.forEach(cb => { try {
414
+ cb(data);
415
+ }
416
+ catch (e) {
417
+ console.error('[Tagada] onOrderSummaryUpdate listener threw:', e);
418
+ } }),
419
+ _fireOrderUpdate: (data) => orderListeners.forEach(cb => { try {
420
+ cb(data);
421
+ }
422
+ catch (e) {
423
+ console.error('[Tagada] onOrderUpdate listener threw:', e);
424
+ } }),
277
425
  };
278
426
  }, [context, isInitialized]);
279
427
  useEffect(() => {
@@ -8,6 +8,14 @@ export interface StripeExpressButtonProps {
8
8
  }) => void;
9
9
  onError?: (error: string) => void;
10
10
  onCancel?: () => void;
11
+ /**
12
+ * Locale forwarded to Stripe Elements. Controls the wording inside
13
+ * sheet-based wallets like PayPal, Klarna, and Link (Apple Pay /
14
+ * Google Pay are localized by the OS and ignore this value).
15
+ * Pass a 2-letter or BCP47 code (e.g. 'fr', 'pt-BR'); defaults to
16
+ * 'auto' which uses the visitor's browser language.
17
+ */
18
+ locale?: string;
11
19
  }
12
20
  export declare const StripeExpressButton: React.FC<StripeExpressButtonProps>;
13
21
  export default StripeExpressButton;
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Elements, ExpressCheckoutElement, useElements, useStripe } from '@stripe/react-stripe-js';
3
3
  import { loadStripe } from '@stripe/stripe-js';
4
- import { useMemo, useRef, useState } from 'react';
4
+ import { useEffect, useMemo, useRef, useState } from 'react';
5
5
  import { PaymentsResource } from '../../core/resources/payments';
6
6
  import { useExpressPaymentMethods } from '../hooks/useExpressPaymentMethods';
7
7
  import { usePaymentPolling } from '../hooks/usePaymentPolling';
@@ -9,7 +9,7 @@ import { getGlobalApiClient } from '../hooks/useApiQuery';
9
9
  // Express method keys — drives processorGroup detection and ECE paymentMethods options.
10
10
  // 'klarna_express' is the CRM config key for Klarna via ECE, distinct from 'klarna' (redirect flow).
11
11
  // Backward compatible: existing configs with only apple_pay/google_pay continue to match.
12
- const EXPRESS_METHOD_KEYS = ['apple_pay', 'google_pay', 'paypal', 'link', 'klarna_express'];
12
+ const EXPRESS_METHOD_KEYS = ['apple_pay', 'google_pay', 'link', 'klarna_express'];
13
13
  // Inner component — must be a child of <Elements> to use useStripe() and useElements()
14
14
  function StripeExpressButtonInner({ checkout, processorId, enabledExpressMethods, onSuccess, onError, onCancel, }) {
15
15
  const stripe = useStripe();
@@ -19,6 +19,17 @@ function StripeExpressButtonInner({ checkout, processorId, enabledExpressMethods
19
19
  const isProcessingRef = useRef(false);
20
20
  const [visible, setVisible] = useState(false);
21
21
  const paymentsResource = useMemo(() => new PaymentsResource(getGlobalApiClient()), []);
22
+ // Push amount/currency changes (e.g. shipping cost added once address resolved,
23
+ // promo applied, quantity changed) to the mounted ExpressCheckoutElement.
24
+ // Re-rendering <Elements> with new options does not update the underlying
25
+ // element — Stripe requires elements.update() for live updates.
26
+ const liveAmount = Math.round(checkout.summary?.totalAdjustedAmount ?? 0);
27
+ const liveCurrency = (checkout.summary?.currency || 'usd').toLowerCase();
28
+ useEffect(() => {
29
+ if (!elements)
30
+ return;
31
+ elements.update({ amount: liveAmount, currency: liveCurrency });
32
+ }, [elements, liveAmount, liveCurrency]);
22
33
  const onConfirm = async (event) => {
23
34
  if (!stripe || !elements || isProcessingRef.current)
24
35
  return;
@@ -65,11 +76,16 @@ function StripeExpressButtonInner({ checkout, processorId, enabledExpressMethods
65
76
  }
66
77
  // Step 3: call backend via existing APM path
67
78
  // paymentsResource.processPaymentDirect returns the raw PaymentResponse (no action routing)
79
+ // Forward the session's selected shipping rate so the order is created
80
+ // with shipping even if the session lookup is racing with this request.
81
+ const shippingRateId = checkout.checkoutSession.shippingRateId
82
+ || checkout.checkoutSession.shippingRate?.id;
68
83
  const response = await paymentsResource.processPaymentDirect(checkout.checkoutSession.id, '', // empty — backend identifies via processorId + paymentMethod
69
84
  undefined, {
70
85
  processorId,
71
86
  paymentMethod,
72
87
  isExpress: true,
88
+ shippingRateId,
73
89
  });
74
90
  const clientSecret = response?.payment?.requireActionData?.metadata?.stripeExpressCheckout?.clientSecret;
75
91
  if (!clientSecret) {
@@ -141,7 +157,6 @@ function StripeExpressButtonInner({ checkout, processorId, enabledExpressMethods
141
157
  applePay: enabledExpressMethods.includes('apple_pay') ? 'always' : 'never',
142
158
  googlePay: enabledExpressMethods.includes('google_pay') ? 'always' : 'never',
143
159
  link: enabledExpressMethods.includes('link') ? 'auto' : 'never',
144
- paypal: enabledExpressMethods.includes('paypal') ? 'auto' : 'never',
145
160
  klarna: enabledExpressMethods.includes('klarna_express') ? 'auto' : 'never',
146
161
  },
147
162
  emailRequired: true,
@@ -165,6 +180,11 @@ export const StripeExpressButton = (props) => {
165
180
  mode: 'payment',
166
181
  amount,
167
182
  currency,
183
+ // 'auto' lets Stripe Elements derive the locale from navigator.language;
184
+ // an explicit code (e.g. 'fr', 'pt-BR') forces a specific translation
185
+ // for PayPal / Klarna / Link sheets. Stripe filters unsupported codes
186
+ // back to English internally, so it's safe to forward any string.
187
+ locale: (props.locale ?? 'auto'),
168
188
  };
169
189
  return (_jsx(Elements, { stripe: stripePromise, options: elementsOptions, children: _jsx(StripeExpressButtonInner, { ...props, processorId: processorId, enabledExpressMethods: enabledExpressMethods }) }));
170
190
  };
@@ -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,
@@ -0,0 +1,15 @@
1
+ import type { Payment, PaymentOptions } from '../../../core/resources/payments';
2
+ import type { PaymentsResource } from '../../../core/resources/payments';
3
+ import type { UsePaymentOptions } from '../usePaymentQuery';
4
+ export declare const NGENIUS_3DS_CONTAINER_ID = "ngenius-3ds-container";
5
+ interface UseNgeniusThreedsActionParams {
6
+ paymentsResource: PaymentsResource;
7
+ startPolling: any;
8
+ setError: (error: string | null) => void;
9
+ setIsLoading: (loading: boolean) => void;
10
+ hookOptionsRef: React.MutableRefObject<UsePaymentOptions | undefined>;
11
+ }
12
+ export declare function useNgeniusThreedsAction({ paymentsResource, startPolling, setError, setIsLoading, hookOptionsRef, }: UseNgeniusThreedsActionParams): {
13
+ handleNgeniusThreeds: (payment: Payment, actionData: any, options?: PaymentOptions) => Promise<void>;
14
+ };
15
+ export {};
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Hook for handling N-Genius 3DS authentication via the N-Genius WebSDK.
3
+ *
4
+ * The WebSDK mounts a challenge iframe into a DOM element identified by
5
+ * NGENIUS_3DS_CONTAINER_ID and exposes `window.NI.handlePaymentResponse`,
6
+ * which returns a Promise that resolves once 3DS completes. After completion,
7
+ * we poll the Ngenius API (every 3 seconds, up to 10 times) to verify payment status.
8
+ */
9
+ import { useCallback } from 'react';
10
+ export const NGENIUS_3DS_CONTAINER_ID = 'ngenius-3ds-container';
11
+ const NGENIUS_SDK_URL_SANDBOX = 'https://paypage.sandbox.ngenius-payments.com/hosted-sessions/sdk.js';
12
+ const NGENIUS_SDK_URL_PRODUCTION = 'https://paypage.ngenius-payments.com/hosted-sessions/sdk.js';
13
+ const NGENIUS_SDK_SCRIPT_ID = 'ngenius-websdk';
14
+ function ensureNgeniusContainer() {
15
+ let container = document.getElementById(NGENIUS_3DS_CONTAINER_ID);
16
+ if (!container) {
17
+ container = document.createElement('div');
18
+ container.id = NGENIUS_3DS_CONTAINER_ID;
19
+ container.style.cssText = `
20
+ position: fixed;
21
+ top: 0;
22
+ left: 0;
23
+ width: 100%;
24
+ height: 100%;
25
+ background-color: rgba(0, 0, 0, 0.5);
26
+ z-index: 9999;
27
+ display: flex;
28
+ align-items: center;
29
+ justify-content: center;
30
+ `;
31
+ document.body.appendChild(container);
32
+ console.log('[N-Genius 3DS] Container created and appended to body');
33
+ }
34
+ return container;
35
+ }
36
+ function loadNgeniusSdk(isSandboxed) {
37
+ return new Promise((resolve, reject) => {
38
+ if (document.getElementById(NGENIUS_SDK_SCRIPT_ID)) {
39
+ resolve();
40
+ return;
41
+ }
42
+ const script = document.createElement('script');
43
+ script.id = NGENIUS_SDK_SCRIPT_ID;
44
+ script.src = isSandboxed ? NGENIUS_SDK_URL_SANDBOX : NGENIUS_SDK_URL_PRODUCTION;
45
+ script.onload = () => resolve();
46
+ script.onerror = () => reject(new Error('Failed to load N-Genius WebSDK'));
47
+ document.head.appendChild(script);
48
+ });
49
+ }
50
+ export function useNgeniusThreedsAction({ paymentsResource, startPolling, setError, setIsLoading, hookOptionsRef, }) {
51
+ const handleNgeniusThreeds = useCallback(async (payment, actionData, options = {}) => {
52
+ const sdk = actionData.metadata?.sdk;
53
+ if (!sdk?.paymentResponse || !sdk.orderReference || !sdk.paymentReference) {
54
+ const msg = 'N-Genius 3DS: missing SDK metadata in requireAction';
55
+ console.error(msg, actionData);
56
+ setError(msg);
57
+ setIsLoading(false);
58
+ options.onFailure?.(msg);
59
+ return;
60
+ }
61
+ try {
62
+ // Ensure the container exists in the DOM
63
+ ensureNgeniusContainer();
64
+ await loadNgeniusSdk(sdk.isSandboxed);
65
+ const NI = window.NI;
66
+ if (!NI?.handlePaymentResponse) {
67
+ throw new Error('N-Genius WebSDK did not expose window.NI.handlePaymentResponse');
68
+ }
69
+ console.log('[N-Genius 3DS] Starting WebSDK challenge flow');
70
+ // Turn off checkout loading overlay - the 3DS modal will be the only overlay
71
+ setIsLoading(false);
72
+ const outcome = await NI.handlePaymentResponse(sdk.paymentResponse, {
73
+ mountId: NGENIUS_3DS_CONTAINER_ID,
74
+ style: { width: '100%', height: 500 },
75
+ });
76
+ console.log('[N-Genius 3DS] WebSDK outcome:', outcome.status);
77
+ console.log('[N-Genius 3DS] 3DS complete - polling Ngenius API for status');
78
+ // Clean up the 3DS container
79
+ const container = document.getElementById(NGENIUS_3DS_CONTAINER_ID);
80
+ if (container) {
81
+ container.style.display = 'none';
82
+ container.innerHTML = '';
83
+ console.log('[N-Genius 3DS] Container hidden and cleared');
84
+ }
85
+ // Turn loading back on while we poll the Ngenius API
86
+ setIsLoading(true);
87
+ // Poll Ngenius API until payment is no longer pending
88
+ const maxAttempts = 10; // 10 attempts
89
+ const pollInterval = 3000; // 3 seconds
90
+ let attempts = 0;
91
+ const pollNgeniusStatus = async () => {
92
+ attempts++;
93
+ console.log(`[N-Genius 3DS] Polling Ngenius API - attempt ${attempts}/${maxAttempts}`);
94
+ try {
95
+ const completedPayment = await paymentsResource.ngeniusThreedsComplete({
96
+ paymentId: payment.id,
97
+ orderReference: sdk.orderReference,
98
+ paymentReference: sdk.paymentReference,
99
+ });
100
+ console.log('[N-Genius 3DS] Ngenius API response:', {
101
+ status: completedPayment.status,
102
+ subStatus: completedPayment.subStatus,
103
+ });
104
+ // If payment succeeded
105
+ if (completedPayment.status === 'succeeded') {
106
+ setIsLoading(false);
107
+ const response = { paymentId: completedPayment.id, payment: completedPayment, order: completedPayment.order };
108
+ await hookOptionsRef.current?.onPaymentCompleted?.(completedPayment, { isRedirectReturn: false, order: response.order });
109
+ options.onSuccess?.(response);
110
+ options.onPaymentSuccess?.(response);
111
+ return;
112
+ }
113
+ // If payment failed
114
+ if (completedPayment.status === 'declined' || completedPayment.status === 'failed') {
115
+ const errorMsg = completedPayment.error?.message || 'Payment failed';
116
+ setError(errorMsg);
117
+ setIsLoading(false);
118
+ await hookOptionsRef.current?.onPaymentFailed?.(errorMsg, { isRedirectReturn: false });
119
+ options.onFailure?.(errorMsg);
120
+ options.onPaymentFailed?.({ code: 'PAYMENT_FAILED', message: errorMsg, payment: completedPayment });
121
+ return;
122
+ }
123
+ // If still pending and we haven't exceeded max attempts, poll again
124
+ if (attempts < maxAttempts) {
125
+ setTimeout(() => pollNgeniusStatus(), pollInterval);
126
+ }
127
+ else {
128
+ // Max attempts reached
129
+ const errorMsg = 'Payment verification timeout - please check your order status';
130
+ setError(errorMsg);
131
+ setIsLoading(false);
132
+ await hookOptionsRef.current?.onPaymentFailed?.(errorMsg, { isRedirectReturn: false });
133
+ options.onFailure?.(errorMsg);
134
+ options.onPaymentFailed?.({ code: 'PAYMENT_TIMEOUT', message: errorMsg, payment: completedPayment });
135
+ }
136
+ }
137
+ catch (error) {
138
+ console.error('[N-Genius 3DS] Error polling Ngenius API:', error);
139
+ const errorMsg = error instanceof Error ? error.message : 'Failed to verify payment status';
140
+ setError(errorMsg);
141
+ setIsLoading(false);
142
+ await hookOptionsRef.current?.onPaymentFailed?.(errorMsg, { isRedirectReturn: false });
143
+ options.onFailure?.(errorMsg);
144
+ options.onPaymentFailed?.({ code: 'NGENIUS_POLL_ERROR', message: errorMsg, payment });
145
+ }
146
+ };
147
+ // Start polling
148
+ await pollNgeniusStatus();
149
+ }
150
+ catch (err) {
151
+ const errorMsg = err instanceof Error ? err.message : 'N-Genius 3DS failed';
152
+ console.error('[N-Genius 3DS] Error:', err);
153
+ // Clean up container on error
154
+ const container = document.getElementById(NGENIUS_3DS_CONTAINER_ID);
155
+ if (container) {
156
+ container.style.display = 'none';
157
+ container.innerHTML = '';
158
+ }
159
+ setError(errorMsg);
160
+ setIsLoading(false);
161
+ options.onFailure?.(errorMsg);
162
+ options.onPaymentFailed?.({ code: 'NGENIUS_3DS_ERROR', message: errorMsg, payment });
163
+ }
164
+ }, [paymentsResource, startPolling, setError, setIsLoading, hookOptionsRef]);
165
+ return { handleNgeniusThreeds };
166
+ }
@@ -12,6 +12,7 @@ import { useStripeRadarAction } from './useStripeRadarAction';
12
12
  import { useKessPayAction } from './useKessPayAction';
13
13
  import { useTrustFlowAction } from './useTrustFlowAction';
14
14
  import { useMasterCardAction } from './useMasterCardAction';
15
+ import { useNgeniusThreedsAction } from './useNgeniusThreedsAction';
15
16
  export function usePaymentActionHandler({ paymentsResource, startChallenge, startPolling, setIsLoading, setError, hookOptionsRef, }) {
16
17
  // Track challenge in progress to prevent multiple challenges
17
18
  const challengeInProgressRef = useRef(false);
@@ -71,6 +72,13 @@ export function usePaymentActionHandler({ paymentsResource, startChallenge, star
71
72
  setIsLoading,
72
73
  hookOptionsRef,
73
74
  });
75
+ const { handleNgeniusThreeds } = useNgeniusThreedsAction({
76
+ paymentsResource,
77
+ startPolling,
78
+ setError,
79
+ setIsLoading,
80
+ hookOptionsRef,
81
+ });
74
82
  // Main handler that routes to specific action handlers
75
83
  const handlePaymentAction = useCallback(async (payment, options = {}) => {
76
84
  if (payment.requireAction === 'none')
@@ -123,6 +131,9 @@ export function usePaymentActionHandler({ paymentsResource, startChallenge, star
123
131
  await handleAirwallexRadar(payment, actionData, options, handlePaymentAction);
124
132
  }
125
133
  break;
134
+ case 'ngenius_3ds':
135
+ await handleNgeniusThreeds(payment, actionData, options);
136
+ break;
126
137
  }
127
138
  options.onRequireAction?.(payment);
128
139
  }, [
@@ -137,6 +148,7 @@ export function usePaymentActionHandler({ paymentsResource, startChallenge, star
137
148
  handleKessPayAuth,
138
149
  handleTrustFlowAuth,
139
150
  handleMasterCardAuth,
151
+ handleNgeniusThreeds,
140
152
  ]);
141
153
  return { handlePaymentAction };
142
154
  }
@@ -14,6 +14,7 @@ export function usePaymentProcessors({ paymentsResource, createCardPaymentInstru
14
14
  paymentFlowId,
15
15
  processorId: options.processorId,
16
16
  paymentMethod: options.paymentMethod,
17
+ shippingRateId: options.shippingRateId,
17
18
  });
18
19
  setCurrentPaymentId(response.payment?.id);
19
20
  if (response.payment.requireAction !== 'none') {
@@ -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';
@@ -80,10 +80,14 @@ export function useCheckoutQuery(options = {}) {
80
80
  }, [providedToken, internalToken]);
81
81
  // Use provided token or internal token
82
82
  const checkoutToken = providedToken || internalToken;
83
+ // Only send currency to the backend when explicitly set via URL param or persisted storage.
84
+ // When not explicit, the backend uses the session's own selectedPresentmentCurrency --
85
+ // the frontend then reads the currency from the response, never guesses.
86
+ const explicitCurrency = currency.isExplicit ? currency.code : undefined;
83
87
  // Main checkout query
84
88
  const { data: checkout, isLoading, error, isSuccess, refetch, } = useQuery({
85
- queryKey: ['checkout', checkoutToken, currency.code],
86
- queryFn: () => checkoutResource.getCheckout(checkoutToken, currency.code),
89
+ queryKey: ['checkout', checkoutToken, explicitCurrency],
90
+ queryFn: () => checkoutResource.getCheckout(checkoutToken, explicitCurrency),
87
91
  enabled: enabled && !!checkoutToken && isSessionInitialized,
88
92
  staleTime: 30000, // 30 seconds
89
93
  refetchOnWindowFocus: false,