@tagadapay/plugin-sdk 3.1.25 → 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.
- package/README.md +1129 -1129
- package/build-cdn.js +499 -499
- package/dist/external-tracker.js +5 -5
- package/dist/external-tracker.min.js +2 -2
- package/dist/external-tracker.min.js.map +2 -2
- package/dist/react/config/payment.d.ts +2 -2
- package/dist/react/config/payment.js +5 -5
- package/dist/react/hooks/usePayment.d.ts +7 -0
- package/dist/react/hooks/usePayment.js +1 -0
- package/dist/react/providers/TagadaProvider.js +5 -5
- package/dist/tagada-react-sdk-minimal.min.js +2 -2
- package/dist/tagada-react-sdk-minimal.min.js.map +4 -4
- package/dist/tagada-react-sdk.js +1616 -1275
- package/dist/tagada-react-sdk.min.js +2 -2
- package/dist/tagada-react-sdk.min.js.map +4 -4
- package/dist/tagada-sdk.js +869 -27
- package/dist/tagada-sdk.min.js +2 -2
- package/dist/tagada-sdk.min.js.map +4 -4
- package/dist/v2/core/config/environment.d.ts +3 -3
- package/dist/v2/core/config/environment.js +7 -7
- package/dist/v2/core/funnelClient.d.ts +2 -0
- package/dist/v2/core/resources/funnel.d.ts +1 -1
- package/dist/v2/core/resources/geo.d.ts +50 -0
- package/dist/v2/core/resources/geo.js +35 -0
- package/dist/v2/core/resources/payments.d.ts +19 -1
- package/dist/v2/core/resources/payments.js +8 -0
- package/dist/v2/core/utils/previewModeIndicator.js +101 -101
- package/dist/v2/react/components/FunnelScriptInjector.js +167 -19
- package/dist/v2/react/components/StripeExpressButton.d.ts +8 -0
- package/dist/v2/react/components/StripeExpressButton.js +22 -1
- package/dist/v2/react/hooks/payment-actions/useNgeniusThreedsAction.d.ts +15 -0
- package/dist/v2/react/hooks/payment-actions/useNgeniusThreedsAction.js +166 -0
- package/dist/v2/react/hooks/payment-actions/usePaymentActionHandler.js +12 -0
- package/dist/v2/react/hooks/payment-processing/usePaymentProcessors.js +1 -0
- package/dist/v2/react/hooks/useISOData.js +25 -7
- package/dist/v2/react/hooks/usePaymentPolling.d.ts +1 -1
- package/dist/v2/react/providers/ExpressPaymentMethodsProvider.js +12 -4
- package/dist/v2/react/providers/TagadaProvider.js +5 -5
- package/dist/v2/standalone/apple-pay-service.d.ts +12 -0
- package/dist/v2/standalone/apple-pay-service.js +12 -0
- package/dist/v2/standalone/external-tracker.d.ts +1 -1
- package/dist/v2/standalone/google-pay-service.d.ts +9 -0
- package/dist/v2/standalone/google-pay-service.js +9 -0
- package/dist/v2/standalone/index.d.ts +8 -1
- package/dist/v2/standalone/index.js +7 -0
- package/dist/v2/standalone/payment-service.d.ts +18 -5
- package/dist/v2/standalone/payment-service.js +62 -9
- 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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
//
|
|
265
|
-
|
|
266
|
-
|
|
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';
|
|
@@ -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) {
|
|
@@ -164,6 +180,11 @@ export const StripeExpressButton = (props) => {
|
|
|
164
180
|
mode: 'payment',
|
|
165
181
|
amount,
|
|
166
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'),
|
|
167
188
|
};
|
|
168
189
|
return (_jsx(Elements, { stripe: stripePromise, options: elementsOptions, children: _jsx(StripeExpressButtonInner, { ...props, processorId: processorId, enabledExpressMethods: enabledExpressMethods }) }));
|
|
169
190
|
};
|
|
@@ -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,16 +1,21 @@
|
|
|
1
1
|
import { useMemo, useEffect, useState, useCallback } from 'react';
|
|
2
|
-
import { getCountries, getStatesForCountry, ensureGeoDataLoaded, importLanguage, isLanguageRegistered, getRegisteredLanguages } from '../../../data/iso3166';
|
|
2
|
+
import { getCountries, getStatesForCountry, ensureGeoDataLoaded, AVAILABLE_LANGUAGES, importLanguage, isLanguageRegistered, getRegisteredLanguages } from '../../../data/iso3166';
|
|
3
3
|
/**
|
|
4
4
|
* React hook for accessing ISO3166 countries and regions data.
|
|
5
5
|
* Fetches slim JSON from CDN on first use, then caches in memory.
|
|
6
6
|
*/
|
|
7
7
|
export function useISOData(language = 'en', autoImport = true, disputeSetting = 'UN') {
|
|
8
|
-
|
|
8
|
+
// Normalize: if the caller passed an unsupported locale (e.g. 'nl'),
|
|
9
|
+
// silently fall back to English so the country dropdown still renders.
|
|
10
|
+
// The CDN only hosts languages listed in AVAILABLE_LANGUAGES; any other
|
|
11
|
+
// value would 404 and leave `isLanguageLoaded` stuck on false.
|
|
12
|
+
const effectiveLanguage = AVAILABLE_LANGUAGES.includes(language) ? language : 'en';
|
|
13
|
+
const [isLanguageLoaded, setIsLanguageLoaded] = useState(isLanguageRegistered(effectiveLanguage));
|
|
9
14
|
const [registeredLanguages, setRegisteredLanguages] = useState(getRegisteredLanguages);
|
|
10
15
|
// Fetch geodata from CDN (countries + regions) for the requested language
|
|
11
16
|
useEffect(() => {
|
|
12
17
|
let cancelled = false;
|
|
13
|
-
ensureGeoDataLoaded(
|
|
18
|
+
ensureGeoDataLoaded(effectiveLanguage)
|
|
14
19
|
.then(() => {
|
|
15
20
|
if (!cancelled) {
|
|
16
21
|
setIsLanguageLoaded(true);
|
|
@@ -19,9 +24,22 @@ export function useISOData(language = 'en', autoImport = true, disputeSetting =
|
|
|
19
24
|
})
|
|
20
25
|
.catch((err) => {
|
|
21
26
|
console.error('[SDK] Failed to load geodata from CDN:', err);
|
|
27
|
+
if (cancelled)
|
|
28
|
+
return;
|
|
29
|
+
// English is prefetched on module load (iso3166.ts). If it
|
|
30
|
+
// landed, unblock the UI — getCountriesCached() falls back to 'en'.
|
|
31
|
+
if (isLanguageRegistered('en')) {
|
|
32
|
+
setIsLanguageLoaded(true);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
// Otherwise try one last English fetch as ultimate fallback.
|
|
36
|
+
ensureGeoDataLoaded('en')
|
|
37
|
+
.then(() => { if (!cancelled)
|
|
38
|
+
setIsLanguageLoaded(true); })
|
|
39
|
+
.catch(() => { });
|
|
22
40
|
});
|
|
23
41
|
return () => { cancelled = true; };
|
|
24
|
-
}, [
|
|
42
|
+
}, [effectiveLanguage, autoImport]);
|
|
25
43
|
const data = useMemo(() => {
|
|
26
44
|
if (!isLanguageLoaded) {
|
|
27
45
|
return {
|
|
@@ -34,7 +52,7 @@ export function useISOData(language = 'en', autoImport = true, disputeSetting =
|
|
|
34
52
|
};
|
|
35
53
|
}
|
|
36
54
|
try {
|
|
37
|
-
const countriesArray = getCountries(
|
|
55
|
+
const countriesArray = getCountries(effectiveLanguage);
|
|
38
56
|
const countries = {};
|
|
39
57
|
countriesArray.forEach((country) => {
|
|
40
58
|
countries[country.code] = {
|
|
@@ -46,7 +64,7 @@ export function useISOData(language = 'en', autoImport = true, disputeSetting =
|
|
|
46
64
|
});
|
|
47
65
|
const getRegions = (countryCode) => {
|
|
48
66
|
try {
|
|
49
|
-
const states = getStatesForCountry(countryCode,
|
|
67
|
+
const states = getStatesForCountry(countryCode, effectiveLanguage);
|
|
50
68
|
return states.map((state) => ({
|
|
51
69
|
iso: state.code,
|
|
52
70
|
name: state.name,
|
|
@@ -97,7 +115,7 @@ export function useISOData(language = 'en', autoImport = true, disputeSetting =
|
|
|
97
115
|
registeredLanguages,
|
|
98
116
|
};
|
|
99
117
|
}
|
|
100
|
-
}, [
|
|
118
|
+
}, [effectiveLanguage, disputeSetting, isLanguageLoaded, registeredLanguages]);
|
|
101
119
|
return data;
|
|
102
120
|
}
|
|
103
121
|
/**
|
|
@@ -4,7 +4,7 @@ export interface Payment {
|
|
|
4
4
|
subStatus: string;
|
|
5
5
|
requireAction: 'none' | 'redirect' | 'redirect_to_payment' | 'error' | 'radar' | 'stripe_express_checkout';
|
|
6
6
|
requireActionData?: {
|
|
7
|
-
type: 'redirect' | 'redirect_to_payment' | 'threeds_auth' | 'processor_auth' | 'error' | 'stripe_radar' | 'finix_radar' | 'radar' | 'kesspay_auth' | 'trustflow_auth' | 'mastercard_auth' | 'stripe_express_checkout';
|
|
7
|
+
type: 'redirect' | 'redirect_to_payment' | 'threeds_auth' | 'processor_auth' | 'error' | 'stripe_radar' | 'finix_radar' | 'radar' | 'kesspay_auth' | 'trustflow_auth' | 'mastercard_auth' | 'ngenius_3ds' | 'stripe_express_checkout';
|
|
8
8
|
url?: string;
|
|
9
9
|
processed: boolean;
|
|
10
10
|
processorId?: string;
|
|
@@ -42,10 +42,15 @@ function paymentMethodsFromSetupConfig(config) {
|
|
|
42
42
|
continue;
|
|
43
43
|
}
|
|
44
44
|
// Individual express methods (apple_pay, google_pay, etc.)
|
|
45
|
-
|
|
45
|
+
// Match by: explicit express flag, type, method, provider, or key name
|
|
46
|
+
const resolvedMethod = entry.method || entry.provider || key;
|
|
47
|
+
const isExpressMethod = entry.express
|
|
48
|
+
|| entry.type === 'apple_pay' || entry.type === 'google_pay'
|
|
49
|
+
|| resolvedMethod === 'apple_pay' || resolvedMethod === 'google_pay';
|
|
50
|
+
if (isExpressMethod) {
|
|
46
51
|
methods.push({
|
|
47
52
|
id: key,
|
|
48
|
-
type: entry.type || entry.
|
|
53
|
+
type: entry.type === 'apple_pay' || entry.type === 'google_pay' ? entry.type : resolvedMethod,
|
|
49
54
|
title: entry.label || key,
|
|
50
55
|
iconUrl: entry.logoUrl || '',
|
|
51
56
|
default: false,
|
|
@@ -60,8 +65,11 @@ export const ExpressPaymentMethodsProvider = ({ children, customerId, checkout,
|
|
|
60
65
|
const [availableExpressPaymentMethodIds, setAvailableExpressPaymentMethodIds] = useState([]);
|
|
61
66
|
const [error, setError] = useState(null);
|
|
62
67
|
const checkoutSessionId = checkout?.checkoutSession?.id;
|
|
63
|
-
// If paymentSetupConfig is provided
|
|
64
|
-
|
|
68
|
+
// If paymentSetupConfig is provided AND contains at least one enabled method, derive from it.
|
|
69
|
+
// Missing, empty `{}`, or all-disabled configs fall through to the API fallback.
|
|
70
|
+
const configDerivedMethods = useMemo(() => (paymentSetupConfig && Object.values(paymentSetupConfig).some((v) => v?.enabled === true))
|
|
71
|
+
? paymentMethodsFromSetupConfig(paymentSetupConfig)
|
|
72
|
+
: undefined, [paymentSetupConfig]);
|
|
65
73
|
// Create express payment methods resource client
|
|
66
74
|
const expressPaymentResource = useMemo(() => {
|
|
67
75
|
try {
|