@tagadapay/plugin-sdk 3.1.25 → 4.0.2
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/dist/external-tracker.js +160 -6
- package/dist/external-tracker.min.js +2 -2
- package/dist/external-tracker.min.js.map +4 -4
- 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/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 +2220 -1428
- package/dist/tagada-react-sdk.min.js +2 -2
- package/dist/tagada-react-sdk.min.js.map +4 -4
- package/dist/tagada-sdk.js +3784 -128
- 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 +42 -0
- package/dist/v2/core/funnelClient.js +30 -0
- package/dist/v2/core/pixelTracker.d.ts +51 -0
- package/dist/v2/core/pixelTracker.js +425 -0
- package/dist/v2/core/resources/checkout.d.ts +45 -1
- package/dist/v2/core/resources/checkout.js +13 -3
- 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/offers.d.ts +1 -1
- package/dist/v2/core/resources/offers.js +3 -1
- package/dist/v2/core/resources/payments.d.ts +19 -1
- package/dist/v2/core/resources/payments.js +8 -0
- package/dist/v2/core/resources/promotionEvents.d.ts +5 -0
- package/dist/v2/core/resources/promotionEvents.js +2 -0
- package/dist/v2/core/resources/promotions.d.ts +6 -1
- package/dist/v2/core/resources/promotions.js +6 -1
- package/dist/v2/core/resources/shippingRates.d.ts +18 -0
- package/dist/v2/core/resources/shippingRates.js +18 -0
- package/dist/v2/core/utils/clickIdResolver.d.ts +79 -0
- package/dist/v2/core/utils/clickIdResolver.js +169 -0
- package/dist/v2/core/utils/index.d.ts +2 -0
- package/dist/v2/core/utils/index.js +4 -0
- package/dist/v2/core/utils/metaEventId.d.ts +14 -0
- package/dist/v2/core/utils/metaEventId.js +16 -0
- package/dist/v2/index.d.ts +7 -0
- package/dist/v2/index.js +10 -0
- package/dist/v2/react/components/ApplePayButton.js +50 -0
- package/dist/v2/react/components/FunnelScriptInjector.js +158 -10
- package/dist/v2/react/components/GooglePayButton.js +39 -1
- package/dist/v2/react/components/StripeExpressButton.d.ts +8 -0
- package/dist/v2/react/components/StripeExpressButton.js +76 -3
- 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/useCheckoutQuery.js +41 -29
- package/dist/v2/react/hooks/useDiscountsQuery.js +4 -0
- package/dist/v2/react/hooks/useFunnel.d.ts +7 -0
- package/dist/v2/react/hooks/useFunnel.js +2 -1
- package/dist/v2/react/hooks/useISOData.js +25 -7
- package/dist/v2/react/hooks/usePaymentPolling.d.ts +1 -1
- package/dist/v2/react/hooks/usePixelTracking.d.ts +10 -5
- package/dist/v2/react/hooks/usePixelTracking.js +32 -374
- package/dist/v2/react/hooks/usePreviewOffer.d.ts +3 -1
- package/dist/v2/react/hooks/usePreviewOffer.js +8 -2
- package/dist/v2/react/hooks/usePromotionsQuery.js +9 -3
- package/dist/v2/react/hooks/useShippingRatesQuery.js +36 -21
- package/dist/v2/react/hooks/useStepConfig.d.ts +9 -0
- package/dist/v2/react/hooks/useStepConfig.js +5 -1
- package/dist/v2/react/index.d.ts +4 -0
- package/dist/v2/react/index.js +8 -0
- package/dist/v2/react/providers/ExpressPaymentMethodsProvider.js +12 -4
- package/dist/v2/react/providers/TagadaProvider.js +13 -0
- 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 +11 -1
- package/dist/v2/standalone/index.js +30 -0
- package/dist/v2/standalone/payment-service.d.ts +72 -6
- package/dist/v2/standalone/payment-service.js +285 -65
- package/package.json +2 -1
|
@@ -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,10 +1,11 @@
|
|
|
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';
|
|
8
|
+
import { useStepConfig } from '../hooks/useStepConfig';
|
|
8
9
|
import { getGlobalApiClient } from '../hooks/useApiQuery';
|
|
9
10
|
// Express method keys — drives processorGroup detection and ECE paymentMethods options.
|
|
10
11
|
// 'klarna_express' is the CRM config key for Klarna via ECE, distinct from 'klarna' (redirect flow).
|
|
@@ -18,7 +19,26 @@ function StripeExpressButtonInner({ checkout, processorId, enabledExpressMethods
|
|
|
18
19
|
const { startPolling } = usePaymentPolling();
|
|
19
20
|
const isProcessingRef = useRef(false);
|
|
20
21
|
const [visible, setVisible] = useState(false);
|
|
22
|
+
// Per-step country allow-list (CRM-injected stepConfig). Empty/missing = all allowed.
|
|
23
|
+
const { stepConfig } = useStepConfig();
|
|
24
|
+
const countryAllowlist = useMemo(() => {
|
|
25
|
+
const list = stepConfig?.addressSettings?.countryAllowlist;
|
|
26
|
+
if (!list || list.length === 0)
|
|
27
|
+
return undefined;
|
|
28
|
+
return list.map((c) => c.toUpperCase());
|
|
29
|
+
}, [stepConfig]);
|
|
21
30
|
const paymentsResource = useMemo(() => new PaymentsResource(getGlobalApiClient()), []);
|
|
31
|
+
// Push amount/currency changes (e.g. shipping cost added once address resolved,
|
|
32
|
+
// promo applied, quantity changed) to the mounted ExpressCheckoutElement.
|
|
33
|
+
// Re-rendering <Elements> with new options does not update the underlying
|
|
34
|
+
// element — Stripe requires elements.update() for live updates.
|
|
35
|
+
const liveAmount = Math.round(checkout.summary?.totalAdjustedAmount ?? 0);
|
|
36
|
+
const liveCurrency = (checkout.summary?.currency || 'usd').toLowerCase();
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (!elements)
|
|
39
|
+
return;
|
|
40
|
+
elements.update({ amount: liveAmount, currency: liveCurrency });
|
|
41
|
+
}, [elements, liveAmount, liveCurrency]);
|
|
22
42
|
const onConfirm = async (event) => {
|
|
23
43
|
if (!stripe || !elements || isProcessingRef.current)
|
|
24
44
|
return;
|
|
@@ -30,6 +50,7 @@ function StripeExpressButtonInner({ checkout, processorId, enabledExpressMethods
|
|
|
30
50
|
// This mirrors what ApplePayButton and GooglePayButton do before processing payment,
|
|
31
51
|
// and ensures customer.email is present for the backend email validation.
|
|
32
52
|
const billing = event.billingDetails;
|
|
53
|
+
const eventShipping = event.shippingAddress;
|
|
33
54
|
if (billing) {
|
|
34
55
|
const nameParts = (billing.name ?? '').trim().split(/\s+/);
|
|
35
56
|
const firstName = nameParts[0] ?? '';
|
|
@@ -46,9 +67,33 @@ function StripeExpressButtonInner({ checkout, processorId, enabledExpressMethods
|
|
|
46
67
|
phone: billing.phone ?? '',
|
|
47
68
|
email: billing.email ?? '',
|
|
48
69
|
};
|
|
70
|
+
// When shippingAddressRequired is true, Stripe provides event.shippingAddress.
|
|
71
|
+
// Otherwise the billing address is used as the shipping address (legacy behavior).
|
|
72
|
+
const shippingFromEvent = eventShipping?.address
|
|
73
|
+
? {
|
|
74
|
+
firstName: (eventShipping.name ?? '').trim().split(/\s+/)[0] ?? '',
|
|
75
|
+
lastName: (eventShipping.name ?? '').trim().split(/\s+/).slice(1).join(' '),
|
|
76
|
+
address1: eventShipping.address.line1 ?? '',
|
|
77
|
+
address2: eventShipping.address.line2 ?? '',
|
|
78
|
+
city: eventShipping.address.city ?? '',
|
|
79
|
+
state: eventShipping.address.state ?? '',
|
|
80
|
+
country: eventShipping.address.country ?? '',
|
|
81
|
+
postal: eventShipping.address.postal_code ?? '',
|
|
82
|
+
phone: billing.phone ?? '',
|
|
83
|
+
email: billing.email ?? '',
|
|
84
|
+
}
|
|
85
|
+
: billingAddress;
|
|
86
|
+
// Defense-in-depth: reject if wallet-returned country isn't in allowlist
|
|
87
|
+
const shippingCountry = shippingFromEvent.country.toUpperCase();
|
|
88
|
+
if (countryAllowlist && shippingCountry && !countryAllowlist.includes(shippingCountry)) {
|
|
89
|
+
console.error('[StripeExpress] Shipping country not in allowlist:', shippingCountry);
|
|
90
|
+
onError?.('Shipping to this country is not supported');
|
|
91
|
+
isProcessingRef.current = false;
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
49
94
|
await updateCheckoutSessionValues({
|
|
50
95
|
data: {
|
|
51
|
-
shippingAddress:
|
|
96
|
+
shippingAddress: shippingFromEvent,
|
|
52
97
|
billingAddress,
|
|
53
98
|
},
|
|
54
99
|
});
|
|
@@ -65,11 +110,16 @@ function StripeExpressButtonInner({ checkout, processorId, enabledExpressMethods
|
|
|
65
110
|
}
|
|
66
111
|
// Step 3: call backend via existing APM path
|
|
67
112
|
// paymentsResource.processPaymentDirect returns the raw PaymentResponse (no action routing)
|
|
113
|
+
// Forward the session's selected shipping rate so the order is created
|
|
114
|
+
// with shipping even if the session lookup is racing with this request.
|
|
115
|
+
const shippingRateId = checkout.checkoutSession.shippingRateId
|
|
116
|
+
|| checkout.checkoutSession.shippingRate?.id;
|
|
68
117
|
const response = await paymentsResource.processPaymentDirect(checkout.checkoutSession.id, '', // empty — backend identifies via processorId + paymentMethod
|
|
69
118
|
undefined, {
|
|
70
119
|
processorId,
|
|
71
120
|
paymentMethod,
|
|
72
121
|
isExpress: true,
|
|
122
|
+
shippingRateId,
|
|
73
123
|
});
|
|
74
124
|
const clientSecret = response?.payment?.requireActionData?.metadata?.stripeExpressCheckout?.clientSecret;
|
|
75
125
|
if (!clientSecret) {
|
|
@@ -123,7 +173,16 @@ function StripeExpressButtonInner({ checkout, processorId, enabledExpressMethods
|
|
|
123
173
|
onError?.(msg);
|
|
124
174
|
}
|
|
125
175
|
};
|
|
126
|
-
|
|
176
|
+
// When ECE has no available wallets, keep the element mounted (so onReady can still
|
|
177
|
+
// fire if the browser later exposes one) but pull it off-screen so it doesn't reserve
|
|
178
|
+
// an empty slot. Off-screen rather than zero-size so the iframe still gets layout.
|
|
179
|
+
const hiddenStyle = {
|
|
180
|
+
position: 'absolute',
|
|
181
|
+
left: '-9999px',
|
|
182
|
+
top: '-9999px',
|
|
183
|
+
pointerEvents: 'none',
|
|
184
|
+
};
|
|
185
|
+
return (_jsx("div", { style: visible ? undefined : hiddenStyle, children: _jsx(ExpressCheckoutElement, { onReady: ({ availablePaymentMethods }) => {
|
|
127
186
|
if (availablePaymentMethods) {
|
|
128
187
|
setVisible(true);
|
|
129
188
|
handleAddExpressId('stripe_express_checkout');
|
|
@@ -145,6 +204,15 @@ function StripeExpressButtonInner({ checkout, processorId, enabledExpressMethods
|
|
|
145
204
|
},
|
|
146
205
|
emailRequired: true,
|
|
147
206
|
billingAddressRequired: true,
|
|
207
|
+
// When an allow-list is configured, collect shipping via the wallet sheet so Stripe
|
|
208
|
+
// can enforce country restrictions natively. allowedShippingCountries requires
|
|
209
|
+
// shippingAddressRequired: true.
|
|
210
|
+
...(countryAllowlist
|
|
211
|
+
? {
|
|
212
|
+
shippingAddressRequired: true,
|
|
213
|
+
allowedShippingCountries: countryAllowlist,
|
|
214
|
+
}
|
|
215
|
+
: {}),
|
|
148
216
|
} }) }));
|
|
149
217
|
}
|
|
150
218
|
// Outer component — resolves Stripe instance and provides <Elements> context
|
|
@@ -164,6 +232,11 @@ export const StripeExpressButton = (props) => {
|
|
|
164
232
|
mode: 'payment',
|
|
165
233
|
amount,
|
|
166
234
|
currency,
|
|
235
|
+
// 'auto' lets Stripe Elements derive the locale from navigator.language;
|
|
236
|
+
// an explicit code (e.g. 'fr', 'pt-BR') forces a specific translation
|
|
237
|
+
// for PayPal / Klarna / Link sheets. Stripe filters unsupported codes
|
|
238
|
+
// back to English internally, so it's safe to forward any string.
|
|
239
|
+
locale: (props.locale ?? 'auto'),
|
|
167
240
|
};
|
|
168
241
|
return (_jsx(Elements, { stripe: stripePromise, options: elementsOptions, children: _jsx(StripeExpressButtonInner, { ...props, processorId: processorId, enabledExpressMethods: enabledExpressMethods }) }));
|
|
169
242
|
};
|
|
@@ -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') {
|
|
@@ -21,7 +21,7 @@ export function useCheckoutQuery(options = {}) {
|
|
|
21
21
|
const { storeId } = usePluginConfig();
|
|
22
22
|
const currency = useCurrency();
|
|
23
23
|
const queryClient = useQueryClient();
|
|
24
|
-
const { isSessionInitialized, session } = useTagadaContext();
|
|
24
|
+
const { isSessionInitialized, session, client } = useTagadaContext();
|
|
25
25
|
// Track pending session promises to avoid creating multiple promises
|
|
26
26
|
const pendingSessionPromise = useRef(null);
|
|
27
27
|
const sessionResolvers = useRef(new Set());
|
|
@@ -64,12 +64,12 @@ export function useCheckoutQuery(options = {}) {
|
|
|
64
64
|
// Create checkout resource client
|
|
65
65
|
const checkoutResource = useMemo(() => {
|
|
66
66
|
try {
|
|
67
|
-
return new CheckoutResource(getGlobalApiClient());
|
|
67
|
+
return new CheckoutResource(getGlobalApiClient(), client.bus);
|
|
68
68
|
}
|
|
69
69
|
catch (error) {
|
|
70
70
|
throw new Error(messages.initFailed + ': ' + (error instanceof Error ? error.message : 'Unknown error'));
|
|
71
71
|
}
|
|
72
|
-
}, []);
|
|
72
|
+
}, [client.bus]);
|
|
73
73
|
// Internal token state that can be updated after init
|
|
74
74
|
const [internalToken, setInternalToken] = useState(providedToken);
|
|
75
75
|
// Update internal token when provided token changes
|
|
@@ -304,6 +304,32 @@ export function useCheckoutQuery(options = {}) {
|
|
|
304
304
|
promotionIds,
|
|
305
305
|
}),
|
|
306
306
|
});
|
|
307
|
+
// Stable mutator wrappers. Returning inline arrows each render produced new
|
|
308
|
+
// refs on every checkout state change (mutation success → query invalidate
|
|
309
|
+
// → refetch → re-render), which propagated instability into consumer
|
|
310
|
+
// useCallback/useEffect deps and broke debounced auto-update (the timer in
|
|
311
|
+
// the shared RHF watch listener kept getting cleared mid-debounce, so the
|
|
312
|
+
// customer-and-session-info call never fired for email-only typing).
|
|
313
|
+
const init = useCallback(async (params) => {
|
|
314
|
+
await waitForSession();
|
|
315
|
+
const result = await initMutation.mutateAsync(params);
|
|
316
|
+
setInternalToken(result.checkoutToken);
|
|
317
|
+
return {
|
|
318
|
+
checkoutSession: checkout?.checkoutSession ?? {},
|
|
319
|
+
checkoutToken: result.checkoutToken,
|
|
320
|
+
};
|
|
321
|
+
}, [waitForSession, initMutation.mutateAsync, checkout?.checkoutSession]);
|
|
322
|
+
const replaceSessionLineItems = useCallback((lineItems) => replaceSessionLineItemsMutation.mutateAsync({ lineItems }), [replaceSessionLineItemsMutation.mutateAsync]);
|
|
323
|
+
const updateLineItems = useCallback((lineItems) => lineItemsMutation.mutateAsync({ lineItems }), [lineItemsMutation.mutateAsync]);
|
|
324
|
+
const updateLineItemsOptimistic = useCallback((lineItems) => lineItemsMutation.mutate({ lineItems }), [lineItemsMutation.mutate]);
|
|
325
|
+
const addLineItems = useCallback((lineItems) => addLineItemsMutation.mutateAsync({ lineItems }), [addLineItemsMutation.mutateAsync]);
|
|
326
|
+
const removeLineItems = useCallback((lineItems) => removeLineItemsMutation.mutateAsync({ lineItems }), [removeLineItemsMutation.mutateAsync]);
|
|
327
|
+
const setItemQuantity = useCallback((variantId, quantity, priceId) => quantityMutation.mutateAsync({ variantId, quantity, priceId }), [quantityMutation.mutateAsync]);
|
|
328
|
+
const updateCustomer = useCallback((data) => customerMutation.mutateAsync(data), [customerMutation.mutateAsync]);
|
|
329
|
+
const updateCustomerAndSessionInfo = useCallback((data) => customerAndSessionMutation.mutateAsync(data), [customerAndSessionMutation.mutateAsync]);
|
|
330
|
+
const applyPromotionCode = useCallback((code) => promotionMutation.mutateAsync({ code }), [promotionMutation.mutateAsync]);
|
|
331
|
+
const removePromotion = useCallback((promotionId) => removePromotionMutation.mutateAsync({ promotionId }), [removePromotionMutation.mutateAsync]);
|
|
332
|
+
const previewCheckoutSession = useCallback((lineItems, promotionIds) => previewCheckoutSessionMutation.mutateAsync({ lineItems, promotionIds }), [previewCheckoutSessionMutation.mutateAsync]);
|
|
307
333
|
return {
|
|
308
334
|
// Query data
|
|
309
335
|
checkout,
|
|
@@ -311,33 +337,19 @@ export function useCheckoutQuery(options = {}) {
|
|
|
311
337
|
error,
|
|
312
338
|
isSuccess,
|
|
313
339
|
// Actions
|
|
314
|
-
init
|
|
315
|
-
// Wait for session to be initialized to ensure we have customerId
|
|
316
|
-
await waitForSession();
|
|
317
|
-
const result = await initMutation.mutateAsync(params);
|
|
318
|
-
// Update internal token state so the query can fetch the checkout data
|
|
319
|
-
// The query will automatically refetch when token changes, and getCheckout()
|
|
320
|
-
// will automatically wait for async completion (via SDK skipAsyncWait=false)
|
|
321
|
-
setInternalToken(result.checkoutToken);
|
|
322
|
-
// Return immediately with token
|
|
323
|
-
// checkoutSession will be populated by the query once background processing completes
|
|
324
|
-
return {
|
|
325
|
-
checkoutSession: checkout?.checkoutSession ?? {},
|
|
326
|
-
checkoutToken: result.checkoutToken,
|
|
327
|
-
};
|
|
328
|
-
},
|
|
340
|
+
init,
|
|
329
341
|
refresh,
|
|
330
342
|
// Checkout operations
|
|
331
|
-
replaceSessionLineItems
|
|
332
|
-
updateLineItems
|
|
333
|
-
updateLineItemsOptimistic
|
|
334
|
-
addLineItems
|
|
335
|
-
removeLineItems
|
|
336
|
-
setItemQuantity
|
|
337
|
-
updateCustomer
|
|
338
|
-
updateCustomerAndSessionInfo
|
|
339
|
-
applyPromotionCode
|
|
340
|
-
removePromotion
|
|
341
|
-
previewCheckoutSession
|
|
343
|
+
replaceSessionLineItems,
|
|
344
|
+
updateLineItems,
|
|
345
|
+
updateLineItemsOptimistic,
|
|
346
|
+
addLineItems,
|
|
347
|
+
removeLineItems,
|
|
348
|
+
setItemQuantity,
|
|
349
|
+
updateCustomer,
|
|
350
|
+
updateCustomerAndSessionInfo,
|
|
351
|
+
applyPromotionCode,
|
|
352
|
+
removePromotion,
|
|
353
|
+
previewCheckoutSession,
|
|
342
354
|
};
|
|
343
355
|
}
|
|
@@ -45,6 +45,8 @@ export function useDiscountsQuery(options = {}) {
|
|
|
45
45
|
await Promise.all([
|
|
46
46
|
queryClient.invalidateQueries({ queryKey: ['discounts', sessionId] }),
|
|
47
47
|
queryClient.invalidateQueries({ queryKey: ['checkout'] }),
|
|
48
|
+
queryClient.invalidateQueries({ queryKey: ['shipping-rates', sessionId] }),
|
|
49
|
+
queryClient.invalidateQueries({ queryKey: ['shipping-rates-preview', sessionId] }),
|
|
48
50
|
]);
|
|
49
51
|
}
|
|
50
52
|
// Call onSuccess callback if provided
|
|
@@ -72,6 +74,8 @@ export function useDiscountsQuery(options = {}) {
|
|
|
72
74
|
await Promise.all([
|
|
73
75
|
queryClient.invalidateQueries({ queryKey: ['discounts', sessionId] }),
|
|
74
76
|
queryClient.invalidateQueries({ queryKey: ['checkout'] }),
|
|
77
|
+
queryClient.invalidateQueries({ queryKey: ['shipping-rates', sessionId] }),
|
|
78
|
+
queryClient.invalidateQueries({ queryKey: ['shipping-rates-preview', sessionId] }),
|
|
75
79
|
]);
|
|
76
80
|
}
|
|
77
81
|
// Call onSuccess callback if provided
|
|
@@ -65,6 +65,13 @@ export interface StepConfigValue {
|
|
|
65
65
|
* undefined = inherit all store upsells, string[] = only these IDs.
|
|
66
66
|
*/
|
|
67
67
|
upsellOfferIds: string[] | undefined;
|
|
68
|
+
/**
|
|
69
|
+
* Payment initiator override for this step's offer/upsell charge.
|
|
70
|
+
* 'merchant' = MIT (off-session, no 3DS). 'customer' = CIT (may trigger 3DS).
|
|
71
|
+
* undefined = use SDK default (merchant).
|
|
72
|
+
* usePreviewOffer auto-applies this value; consumers usually don't need to read it.
|
|
73
|
+
*/
|
|
74
|
+
paymentInitiator: 'merchant' | 'customer' | undefined;
|
|
68
75
|
}
|
|
69
76
|
export interface FunnelContextValue extends FunnelState {
|
|
70
77
|
currentStep: {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* ```
|
|
21
21
|
*/
|
|
22
22
|
import { useMemo } from 'react';
|
|
23
|
-
import { TrackingProvider, getAssignedOrderBumpOfferIds, getAssignedPaymentFlowId, getAssignedPixels, getAssignedResources, getAssignedScripts, getAssignedStepConfig, getAssignedUpsellOfferIds, } from '../../core/funnelClient';
|
|
23
|
+
import { TrackingProvider, getAssignedOrderBumpOfferIds, getAssignedPaymentFlowId, getAssignedPaymentInitiator, getAssignedPixels, getAssignedResources, getAssignedScripts, getAssignedStepConfig, getAssignedUpsellOfferIds, } from '../../core/funnelClient';
|
|
24
24
|
import { useTagadaContext } from '../providers/TagadaProvider';
|
|
25
25
|
/**
|
|
26
26
|
* Hook to access funnel state and methods
|
|
@@ -45,6 +45,7 @@ export function useFunnel() {
|
|
|
45
45
|
getScripts: (position) => getAssignedScripts(position),
|
|
46
46
|
orderBumpOfferIds: getAssignedOrderBumpOfferIds(),
|
|
47
47
|
upsellOfferIds: getAssignedUpsellOfferIds(),
|
|
48
|
+
paymentInitiator: getAssignedPaymentInitiator(),
|
|
48
49
|
};
|
|
49
50
|
}, []);
|
|
50
51
|
return {
|
|
@@ -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;
|