@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
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Click-ID resolver — pure, runtime-agnostic utility.
|
|
3
|
+
*
|
|
4
|
+
* Many ad-trackers (ClickFlare, Voluum, Binom, RedTrack, ClickMagick, …) all
|
|
5
|
+
* follow the same pattern: assign a `click_id` on the visitor's first ad-click
|
|
6
|
+
* on the lander, then expect that id to flow end-to-end (lander → checkout →
|
|
7
|
+
* thank-you) so they can match a server postback to the original visit.
|
|
8
|
+
*
|
|
9
|
+
* This module:
|
|
10
|
+
* - Resolves the click id from URL query params (preferred — survives the
|
|
11
|
+
* redirect chain) and first-party cookies (fallback — set by the tracker's
|
|
12
|
+
* lander script).
|
|
13
|
+
* - Publishes the resolved value to `window.TagadaPay.tracking` so merchant-
|
|
14
|
+
* authored postback snippets in `stepConfig.scripts` can read it without
|
|
15
|
+
* re-implementing URL/cookie scraping.
|
|
16
|
+
*
|
|
17
|
+
* No React, no SDK class dependencies — works from React provider, standalone
|
|
18
|
+
* SDK, the studio builder, the external-tracker bundle, or a raw <script>.
|
|
19
|
+
*
|
|
20
|
+
* The lists are intentionally explicit (not regexes) so we never accidentally
|
|
21
|
+
* promote a random param to a "click id" — they are the exact identifiers
|
|
22
|
+
* documented by each tracker / ad platform.
|
|
23
|
+
*/
|
|
24
|
+
// -----------------------------------------------------------------------------
|
|
25
|
+
// Configuration — explicit allow-lists
|
|
26
|
+
// -----------------------------------------------------------------------------
|
|
27
|
+
/** URL query-param names ad trackers / ad platforms use for click ids.
|
|
28
|
+
*
|
|
29
|
+
* Order matters — the resolver returns the FIRST matching entry, so put
|
|
30
|
+
* tracker-specific canonical names BEFORE generic fallbacks like
|
|
31
|
+
* `click_id` or `clickid`. This is why ClickFlare's `cf_click_id` comes
|
|
32
|
+
* before `click_id`, and why the per-tracker canonical token (`cid` for
|
|
33
|
+
* Voluum, `rtkclickid` for RedTrack, `cmc_tid` for ClickMagick) comes
|
|
34
|
+
* first within its own block.
|
|
35
|
+
*/
|
|
36
|
+
export const CLICK_ID_URL_PARAMS = Object.freeze([
|
|
37
|
+
// Postback ad-trackers (most specific first)
|
|
38
|
+
'cf_click_id', // ClickFlare canonical
|
|
39
|
+
'cid', // Voluum canonical (also: ClickFlare receiving-side)
|
|
40
|
+
'rtkclickid', // RedTrack canonical
|
|
41
|
+
'cmc_tid', // ClickMagick canonical (replaces legacy #S2#)
|
|
42
|
+
'cmc_id', // ClickMagick legacy
|
|
43
|
+
'cmcid', // ClickMagick alt
|
|
44
|
+
'clickid', // Generic affiliate-network token (Binom, RedTrack, Voluum, …)
|
|
45
|
+
'click_id', // Generic snake_case (ClickFlare, ClickMagick, …)
|
|
46
|
+
// Ad-platform native click ids
|
|
47
|
+
'gclid', // Google Ads
|
|
48
|
+
'gbraid', // Google Ads (iOS app)
|
|
49
|
+
'wbraid', // Google Ads (web→app)
|
|
50
|
+
'fbclid', // Meta
|
|
51
|
+
'msclkid', // Microsoft Ads
|
|
52
|
+
'ttclid', // TikTok
|
|
53
|
+
'twclid', // X / Twitter
|
|
54
|
+
'li_fat_id', // LinkedIn
|
|
55
|
+
'epik', // Pinterest
|
|
56
|
+
'dclid', // Display & Video 360
|
|
57
|
+
'yclid', // Yandex
|
|
58
|
+
'irclickid', // Impact
|
|
59
|
+
]);
|
|
60
|
+
/** Cookie names ad trackers store their click ids under.
|
|
61
|
+
*
|
|
62
|
+
* Same ordering rule as URL params: canonical first-party cookies first
|
|
63
|
+
* (e.g. `rtkclickid-store` for RedTrack), legacy fallbacks last.
|
|
64
|
+
*/
|
|
65
|
+
export const CLICK_ID_COOKIES = Object.freeze([
|
|
66
|
+
// ClickFlare
|
|
67
|
+
'cf_click_id',
|
|
68
|
+
'cfclid',
|
|
69
|
+
// RedTrack — `rtkclickid-store` is the canonical first-party cookie
|
|
70
|
+
// set by RedTrack's Universal Tracking Script.
|
|
71
|
+
'rtkclickid-store',
|
|
72
|
+
'_rtkclickid',
|
|
73
|
+
'rtkclickid',
|
|
74
|
+
// Voluum
|
|
75
|
+
'_voluum',
|
|
76
|
+
'_voluumclickid',
|
|
77
|
+
// Binom
|
|
78
|
+
'_binom',
|
|
79
|
+
'_binomclickid',
|
|
80
|
+
// ClickMagick
|
|
81
|
+
'_mck',
|
|
82
|
+
'cmcid',
|
|
83
|
+
// Other
|
|
84
|
+
'skro-click-id', // Skro
|
|
85
|
+
'click_id', // generic catch-all (last)
|
|
86
|
+
]);
|
|
87
|
+
// -----------------------------------------------------------------------------
|
|
88
|
+
// Pure resolution
|
|
89
|
+
// -----------------------------------------------------------------------------
|
|
90
|
+
function readCookie(name) {
|
|
91
|
+
if (typeof document === 'undefined' || !document.cookie)
|
|
92
|
+
return null;
|
|
93
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
94
|
+
const m = document.cookie.match(new RegExp('(?:^|; )' + escaped + '=([^;]*)'));
|
|
95
|
+
if (!m)
|
|
96
|
+
return null;
|
|
97
|
+
try {
|
|
98
|
+
return decodeURIComponent(m[1]);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return m[1];
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function readUrlParam(search, name) {
|
|
105
|
+
if (!search)
|
|
106
|
+
return null;
|
|
107
|
+
try {
|
|
108
|
+
return new URLSearchParams(search).get(name);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Resolve the visitor's click id. Pure — no side effects, safe in SSR.
|
|
116
|
+
*
|
|
117
|
+
* Resolution order:
|
|
118
|
+
* 1. URL params (in CLICK_ID_URL_PARAMS order) — most authoritative.
|
|
119
|
+
* 2. Cookies (in CLICK_ID_COOKIES order) — fallback.
|
|
120
|
+
*/
|
|
121
|
+
export function resolveClickId() {
|
|
122
|
+
const all = {};
|
|
123
|
+
let clickId = null;
|
|
124
|
+
let source = null;
|
|
125
|
+
let key = null;
|
|
126
|
+
const search = typeof window !== 'undefined' ? window.location?.search ?? '' : '';
|
|
127
|
+
for (const name of CLICK_ID_URL_PARAMS) {
|
|
128
|
+
const v = readUrlParam(search, name);
|
|
129
|
+
if (v) {
|
|
130
|
+
all[`url:${name}`] = v;
|
|
131
|
+
if (clickId === null) {
|
|
132
|
+
clickId = v;
|
|
133
|
+
source = 'url';
|
|
134
|
+
key = name;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
for (const name of CLICK_ID_COOKIES) {
|
|
139
|
+
const v = readCookie(name);
|
|
140
|
+
if (v) {
|
|
141
|
+
all[`cookie:${name}`] = v;
|
|
142
|
+
if (clickId === null) {
|
|
143
|
+
clickId = v;
|
|
144
|
+
source = 'cookie';
|
|
145
|
+
key = name;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return { clickId, source, key, all };
|
|
150
|
+
}
|
|
151
|
+
// -----------------------------------------------------------------------------
|
|
152
|
+
// Browser-side publication
|
|
153
|
+
// -----------------------------------------------------------------------------
|
|
154
|
+
/**
|
|
155
|
+
* Resolve and merge tracking metadata into `window.TagadaPay.tracking`.
|
|
156
|
+
*
|
|
157
|
+
* Idempotent and namespace-safe: we only ever touch the `tracking` key, never
|
|
158
|
+
* other namespaces (e.g. `order` set by the thank-you page).
|
|
159
|
+
*
|
|
160
|
+
* Returns the published value, or `null` when called outside a browser.
|
|
161
|
+
*/
|
|
162
|
+
export function publishTrackingGlobal() {
|
|
163
|
+
if (typeof window === 'undefined')
|
|
164
|
+
return null;
|
|
165
|
+
const resolved = resolveClickId();
|
|
166
|
+
const tracking = { ...resolved, resolvedAt: Date.now() };
|
|
167
|
+
window.TagadaPay = { ...(window.TagadaPay ?? {}), tracking };
|
|
168
|
+
return tracking;
|
|
169
|
+
}
|
|
@@ -14,3 +14,7 @@ export * from './sessionStorage';
|
|
|
14
14
|
export * from './funnelQueryKeys';
|
|
15
15
|
// Config hot reload for live preview editing
|
|
16
16
|
export * from './configHotReload';
|
|
17
|
+
// Meta CAPI / browser pixel deduplication helper.
|
|
18
|
+
export * from './metaEventId';
|
|
19
|
+
// Click-id resolver for ad-tracker integrations (ClickFlare, Voluum, Binom, …)
|
|
20
|
+
export * from './clickIdResolver';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable event_id helper for Meta CAPI / browser pixel deduplication.
|
|
3
|
+
*
|
|
4
|
+
* The browser side (`fbq('track', name, params, { eventID })`) and the server
|
|
5
|
+
* side (`ServerEvent.setEventId(...)`) must publish the same value to let Meta
|
|
6
|
+
* dedup. This helper is the single source of truth for the formula, mirrored
|
|
7
|
+
* on the server in `src/app/services/meta/meta-event-id.ts`. A parity test in
|
|
8
|
+
* the server package keeps them byte-identical.
|
|
9
|
+
*
|
|
10
|
+
* Convention: `${eventName}_${entityId}`. Different event names get different
|
|
11
|
+
* IDs even when fired for the same entity (e.g. a Purchase + Subscribe pair on
|
|
12
|
+
* the same order), so each dedups independently.
|
|
13
|
+
*/
|
|
14
|
+
export declare function makeMetaEventId(eventName: string, entityId: string): string;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable event_id helper for Meta CAPI / browser pixel deduplication.
|
|
3
|
+
*
|
|
4
|
+
* The browser side (`fbq('track', name, params, { eventID })`) and the server
|
|
5
|
+
* side (`ServerEvent.setEventId(...)`) must publish the same value to let Meta
|
|
6
|
+
* dedup. This helper is the single source of truth for the formula, mirrored
|
|
7
|
+
* on the server in `src/app/services/meta/meta-event-id.ts`. A parity test in
|
|
8
|
+
* the server package keeps them byte-identical.
|
|
9
|
+
*
|
|
10
|
+
* Convention: `${eventName}_${entityId}`. Different event names get different
|
|
11
|
+
* IDs even when fired for the same entity (e.g. a Purchase + Subscribe pair on
|
|
12
|
+
* the same order), so each dedups independently.
|
|
13
|
+
*/
|
|
14
|
+
export function makeMetaEventId(eventName, entityId) {
|
|
15
|
+
return `${eventName}_${entityId}`;
|
|
16
|
+
}
|
package/dist/v2/index.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export * from './core/googleAutocomplete';
|
|
|
9
9
|
export * from './core/isoData';
|
|
10
10
|
export * from './core/utils/configHotReload';
|
|
11
11
|
export * from './core/utils/currency';
|
|
12
|
+
export * from './core/utils/metaEventId';
|
|
12
13
|
export * from './core/utils/pluginConfig';
|
|
13
14
|
export * from './core/utils/previewMode';
|
|
14
15
|
export { injectPreviewModeIndicator, isIndicatorInjected, removePreviewModeIndicator } from './core/utils/previewModeIndicator';
|
|
@@ -16,6 +17,10 @@ export * from './core/utils/products';
|
|
|
16
17
|
export { getAssignedPaymentFlowId, getAssignedScripts, getAssignedStaticResources, getAssignedResources, getAssignedStepConfig, getAssignedOrderBumpOfferIds, getAssignedUpsellOfferIds, getLocalFunnelConfig, getEnabledMethods, getExpressMethods, getExpressMethodsByProcessor, findMethod, isMethodEnabled, loadLocalFunnelConfig } from './core/funnelClient';
|
|
17
18
|
export type { LocalFunnelConfig, PaymentMethodConfig, PaymentSetupConfig, PaymentSetupMethod, PixelsConfig, RuntimeStepConfig } from './core/funnelClient';
|
|
18
19
|
export * from './core/pathRemapping';
|
|
20
|
+
export { bootstrapPixelTrackerFromWindow, createPixelTracker, } from './core/pixelTracker';
|
|
21
|
+
export type { PixelTracker, TrackOptions } from './core/pixelTracker';
|
|
22
|
+
export type { PixelProviderKey } from './core/pixelMapping';
|
|
23
|
+
export { makeMetaEventId } from './core/utils/metaEventId';
|
|
19
24
|
export type { CheckoutData, CheckoutInitParams, CheckoutLineItem, CheckoutSession, CheckoutSessionPreview, Promotion } from './core/resources/checkout';
|
|
20
25
|
export type { Order, OrderLineItem } from './core/utils/order';
|
|
21
26
|
export type { PostPurchaseOffer, PostPurchaseOfferItem, PostPurchaseOfferSummary } from './core/resources/postPurchases';
|
|
@@ -31,6 +36,8 @@ export type { BackNavigationActionData, CartUpdatedActionData, DirectNavigationA
|
|
|
31
36
|
export { ApplePayButton, StripeExpressButton, ExpressPaymentMethodsProvider, formatMoney, getAvailableLanguages, GooglePayButton, PreviewModeIndicator, queryKeys, TagadaProvider, useApiMutation, useApiQuery, useApplePayCheckout, useAuth, useCheckout, useCheckoutToken, useClubOffers, useCountryOptions, useCredits, useCurrency, useCustomer, useCustomerInfos, useCustomerOrders, useCustomerSubscriptions, useDiscounts, useExpressPaymentMethods, useFunnel, useFunnelLegacy, useGeoLocation, useGoogleAutocomplete, useGooglePayCheckout, useInvalidateQuery, useISOData, useLanguageImport, useLogin, useOffer, useOrder, useOrderBump, usePayment, usePaymentRetrieve, usePixelTracking, usePluginConfig, usePostPurchases, usePreloadQuery, usePreviewOffer, useProducts, usePromotions, useRegionOptions, useRemappableParams, useShippingRates, useSimpleFunnel, useStepConfig, useStoreConfig, useTagadaContext, useThreeds, useThreedsModal, useTranslation, useVipOffers, useSetPaymentMethod, WhopCheckout, useWhopPaymentPolling } from './react';
|
|
32
37
|
export type { DebugScript } from './react';
|
|
33
38
|
export type { PaymentMethodName } from './react';
|
|
39
|
+
export { resolveClickId, publishTrackingGlobal, CLICK_ID_URL_PARAMS, CLICK_ID_COOKIES, } from './core/utils/clickIdResolver';
|
|
40
|
+
export type { ResolvedClickId, TagadaTrackingGlobal, ClickIdSource, } from './core/utils/clickIdResolver';
|
|
34
41
|
export type { TranslateFunction, TranslationText, UseTranslationOptions, UseTranslationResult } from './react/hooks/useTranslation';
|
|
35
42
|
export type { FunnelContextValue } from './react/hooks/useFunnel';
|
|
36
43
|
export type { UseApplePayCheckoutOptions } from './react/hooks/useApplePayCheckout';
|
package/dist/v2/index.js
CHANGED
|
@@ -10,6 +10,7 @@ export * from './core/googleAutocomplete';
|
|
|
10
10
|
export * from './core/isoData';
|
|
11
11
|
export * from './core/utils/configHotReload';
|
|
12
12
|
export * from './core/utils/currency';
|
|
13
|
+
export * from './core/utils/metaEventId';
|
|
13
14
|
export * from './core/utils/pluginConfig';
|
|
14
15
|
export * from './core/utils/previewMode';
|
|
15
16
|
export { injectPreviewModeIndicator, isIndicatorInjected, removePreviewModeIndicator } from './core/utils/previewModeIndicator';
|
|
@@ -22,6 +23,15 @@ getEnabledMethods, getExpressMethods, getExpressMethodsByProcessor, findMethod,
|
|
|
22
23
|
loadLocalFunnelConfig } from './core/funnelClient';
|
|
23
24
|
// Path remapping helpers (framework-agnostic)
|
|
24
25
|
export * from './core/pathRemapping';
|
|
26
|
+
// Framework-agnostic pixel tracker (Studio entries bootstrap from this)
|
|
27
|
+
export { bootstrapPixelTrackerFromWindow, createPixelTracker, } from './core/pixelTracker';
|
|
28
|
+
// Stable event_id helper for Meta CAPI / browser pixel deduplication.
|
|
29
|
+
// Re-exported here (already exposed via /v2/react) so framework-agnostic
|
|
30
|
+
// Studio islands can import it from the same entry as bootstrapPixelTracker.
|
|
31
|
+
export { makeMetaEventId } from './core/utils/metaEventId';
|
|
25
32
|
export { FunnelActionType } from './core/resources/funnel';
|
|
26
33
|
// React exports (hooks and components only, types are exported above)
|
|
27
34
|
export { ApplePayButton, StripeExpressButton, ExpressPaymentMethodsProvider, formatMoney, getAvailableLanguages, GooglePayButton, PreviewModeIndicator, queryKeys, TagadaProvider, useApiMutation, useApiQuery, useApplePayCheckout, useAuth, useCheckout, useCheckoutToken, useClubOffers, useCountryOptions, useCredits, useCurrency, useCustomer, useCustomerInfos, useCustomerOrders, useCustomerSubscriptions, useDiscounts, useExpressPaymentMethods, useFunnel, useFunnelLegacy, useGeoLocation, useGoogleAutocomplete, useGooglePayCheckout, useInvalidateQuery, useISOData, useLanguageImport, useLogin, useOffer, useOrder, useOrderBump, usePayment, usePaymentRetrieve, usePixelTracking, usePluginConfig, usePostPurchases, usePreloadQuery, usePreviewOffer, useProducts, usePromotions, useRegionOptions, useRemappableParams, useShippingRates, useSimpleFunnel, useStepConfig, useStoreConfig, useTagadaContext, useThreeds, useThreedsModal, useTranslation, useVipOffers, useSetPaymentMethod, WhopCheckout, useWhopPaymentPolling } from './react';
|
|
35
|
+
// Click-id resolver (ad-tracker integrations: ClickFlare, Voluum, Binom, …)
|
|
36
|
+
// Re-exported from core so it's reachable from any entry point.
|
|
37
|
+
export { resolveClickId, publishTrackingGlobal, CLICK_ID_URL_PARAMS, CLICK_ID_COOKIES, } from './core/utils/clickIdResolver';
|
|
@@ -9,6 +9,7 @@ import { getCurrencyInfo, minorUnitsToMajorUnits } from '../../../react/utils/mo
|
|
|
9
9
|
import { useExpressPaymentMethods } from '../hooks/useExpressPaymentMethods';
|
|
10
10
|
import { usePaymentQuery } from '../hooks/usePaymentQuery';
|
|
11
11
|
import { useShippingRatesQuery } from '../hooks/useShippingRatesQuery';
|
|
12
|
+
import { useStepConfig } from '../hooks/useStepConfig';
|
|
12
13
|
// Helper function to convert Apple Pay contact to Address (matches CMS)
|
|
13
14
|
const applePayContactToAddress = (contact) => {
|
|
14
15
|
return {
|
|
@@ -52,6 +53,14 @@ export const ApplePayButton = ({ checkout, onSuccess, onError, onCancel }) => {
|
|
|
52
53
|
const { applePayPaymentMethod, reComputeOrderSummary, shippingMethods, lineItems, handleAddExpressId, updateCheckoutSessionValues, updateCustomerEmail, setError: setContextError, } = useExpressPaymentMethods();
|
|
53
54
|
const [processingPayment, setProcessingPayment] = useState(false);
|
|
54
55
|
const [isApplePayAvailable, setIsApplePayAvailable] = useState(false);
|
|
56
|
+
// Per-step country allow-list (CRM-injected stepConfig). Empty/missing = all allowed.
|
|
57
|
+
const { stepConfig } = useStepConfig();
|
|
58
|
+
const countryAllowlist = useMemo(() => {
|
|
59
|
+
const list = stepConfig?.addressSettings?.countryAllowlist;
|
|
60
|
+
if (!list || list.length === 0)
|
|
61
|
+
return undefined;
|
|
62
|
+
return list.map((c) => c.toUpperCase());
|
|
63
|
+
}, [stepConfig]);
|
|
55
64
|
// Get Basis Theory API key
|
|
56
65
|
const basistheoryPublicKey = useMemo(() => getBasisTheoryApiKey(), []);
|
|
57
66
|
// Use payment hook for proper payment processing
|
|
@@ -192,6 +201,7 @@ export const ApplePayButton = ({ checkout, onSuccess, onError, onCancel }) => {
|
|
|
192
201
|
lineItems,
|
|
193
202
|
requiredShippingContactFields: ['name', 'phone', 'email', 'postalAddress'],
|
|
194
203
|
requiredBillingContactFields: ['postalAddress'],
|
|
204
|
+
...(countryAllowlist ? { supportedCountries: countryAllowlist } : {}),
|
|
195
205
|
};
|
|
196
206
|
try {
|
|
197
207
|
const session = new ApplePaySession(3, request);
|
|
@@ -216,6 +226,18 @@ export const ApplePayButton = ({ checkout, onSuccess, onError, onCancel }) => {
|
|
|
216
226
|
const billingContact = event.payment.billingContact;
|
|
217
227
|
const shippingAddress = applePayContactToAddress(shippingContact);
|
|
218
228
|
const billingAddress = applePayContactToAddress(billingContact);
|
|
229
|
+
// Defense-in-depth: reject if wallet-returned country isn't in allowlist
|
|
230
|
+
if (countryAllowlist &&
|
|
231
|
+
shippingAddress.country &&
|
|
232
|
+
!countryAllowlist.includes(shippingAddress.country.toUpperCase())) {
|
|
233
|
+
console.error('[ApplePay] Shipping country not in allowlist:', shippingAddress.country);
|
|
234
|
+
session.completePayment(ApplePaySession.STATUS_FAILURE);
|
|
235
|
+
const errorMessage = 'Shipping to this country is not supported';
|
|
236
|
+
setContextError(errorMessage);
|
|
237
|
+
if (onError)
|
|
238
|
+
onError(errorMessage);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
219
241
|
await updateCheckoutSessionValues({
|
|
220
242
|
data: {
|
|
221
243
|
shippingAddress,
|
|
@@ -283,6 +305,33 @@ export const ApplePayButton = ({ checkout, onSuccess, onError, onCancel }) => {
|
|
|
283
305
|
session.onshippingcontactselected = (event) => {
|
|
284
306
|
void (async () => {
|
|
285
307
|
const shippingContact = event.shippingContact;
|
|
308
|
+
const contactCountry = (shippingContact?.countryCode || '').toUpperCase();
|
|
309
|
+
// Defense-in-depth: surface an inline error in the Apple Pay sheet
|
|
310
|
+
// when the selected shipping country isn't in the allow-list, so the
|
|
311
|
+
// user can pick a valid address without restarting the flow.
|
|
312
|
+
if (countryAllowlist && contactCountry && !countryAllowlist.includes(contactCountry)) {
|
|
313
|
+
console.warn('[ApplePay] Selected shipping country not in allowlist:', contactCountry);
|
|
314
|
+
const ApplePayErrorCtor = window.ApplePayError;
|
|
315
|
+
const currentTotal = {
|
|
316
|
+
label: checkout.checkoutSession.store?.name || 'Store',
|
|
317
|
+
amount: minorUnitsToCurrencyString(checkout.summary?.totalAdjustedAmount ?? 0, checkout.summary?.currency),
|
|
318
|
+
type: 'final',
|
|
319
|
+
};
|
|
320
|
+
const errors = ApplePayErrorCtor
|
|
321
|
+
? [new ApplePayErrorCtor('shippingContactInvalid', 'country', 'Shipping to this country is not supported')]
|
|
322
|
+
: [{
|
|
323
|
+
code: 'shippingContactInvalid',
|
|
324
|
+
contactField: 'country',
|
|
325
|
+
message: 'Shipping to this country is not supported',
|
|
326
|
+
}];
|
|
327
|
+
session.completeShippingContactSelection({
|
|
328
|
+
newTotal: currentTotal,
|
|
329
|
+
newLineItems: lineItems,
|
|
330
|
+
newShippingMethods: [],
|
|
331
|
+
errors,
|
|
332
|
+
});
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
286
335
|
try {
|
|
287
336
|
await updateCheckoutSessionValues({
|
|
288
337
|
data: {
|
|
@@ -334,6 +383,7 @@ export const ApplePayButton = ({ checkout, onSuccess, onError, onCancel }) => {
|
|
|
334
383
|
shippingMethods,
|
|
335
384
|
lineItems,
|
|
336
385
|
applePayPaymentMethod,
|
|
386
|
+
countryAllowlist,
|
|
337
387
|
minorUnitsToCurrencyString,
|
|
338
388
|
validateMerchant,
|
|
339
389
|
tokenizeApplePay,
|
|
@@ -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,
|
|
@@ -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,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
|
8
8
|
import { useExpressPaymentMethods } from '../hooks/useExpressPaymentMethods';
|
|
9
9
|
import { usePaymentQuery } from '../hooks/usePaymentQuery';
|
|
10
10
|
import { useShippingRatesQuery } from '../hooks/useShippingRatesQuery';
|
|
11
|
+
import { useStepConfig } from '../hooks/useStepConfig';
|
|
11
12
|
import { getBasisTheoryKeys } from '../../../config/basisTheory';
|
|
12
13
|
export const GooglePayButton = ({ className = '', disabled = false, onSuccess, onError, onCancel, checkout, size = 'lg', buttonColor = 'black', buttonType = 'plain', requiresShipping: requiresShippingProp, }) => {
|
|
13
14
|
const { googlePayPaymentMethod, shippingMethods, lineItems, handleAddExpressId, updateCheckoutSessionValues, updateCustomerEmail, reComputeOrderSummary, setError: setContextError, } = useExpressPaymentMethods();
|
|
@@ -33,6 +34,14 @@ export const GooglePayButton = ({ className = '', disabled = false, onSuccess, o
|
|
|
33
34
|
const [processingPayment, setProcessingPayment] = useState(false);
|
|
34
35
|
const [pendingPaymentData, setPendingPaymentData] = useState(null);
|
|
35
36
|
const [googlePayError, setGooglePayError] = useState(null);
|
|
37
|
+
// Per-step country allow-list (CRM-injected stepConfig). Empty/missing = all allowed.
|
|
38
|
+
const { stepConfig } = useStepConfig();
|
|
39
|
+
const countryAllowlist = useMemo(() => {
|
|
40
|
+
const list = stepConfig?.addressSettings?.countryAllowlist;
|
|
41
|
+
if (!list || list.length === 0)
|
|
42
|
+
return undefined;
|
|
43
|
+
return list.map((c) => c.toUpperCase());
|
|
44
|
+
}, [stepConfig]);
|
|
36
45
|
// Don't render if no Google Pay payment method is enabled
|
|
37
46
|
if (!googlePayPaymentMethod) {
|
|
38
47
|
return null;
|
|
@@ -124,6 +133,19 @@ export const GooglePayButton = ({ className = '', disabled = false, onSuccess, o
|
|
|
124
133
|
if (paymentData.shippingAddress) {
|
|
125
134
|
shippingAddress = googlePayAddressToAddress(paymentData.shippingAddress);
|
|
126
135
|
}
|
|
136
|
+
// Defense-in-depth: reject if wallet-returned country isn't in allowlist
|
|
137
|
+
if (countryAllowlist &&
|
|
138
|
+
shippingAddress?.country &&
|
|
139
|
+
!countryAllowlist.includes(shippingAddress.country.toUpperCase())) {
|
|
140
|
+
const msg = 'Shipping to this country is not supported';
|
|
141
|
+
console.error('[GooglePay] Shipping country not in allowlist:', shippingAddress.country);
|
|
142
|
+
setProcessingPayment(false);
|
|
143
|
+
setGooglePayError(msg);
|
|
144
|
+
setContextError(msg);
|
|
145
|
+
if (onError)
|
|
146
|
+
onError(msg);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
127
149
|
// Update checkout session with addresses before processing payment
|
|
128
150
|
if (shippingAddress) {
|
|
129
151
|
await updateCheckoutSessionValues({
|
|
@@ -164,6 +186,7 @@ export const GooglePayButton = ({ className = '', disabled = false, onSuccess, o
|
|
|
164
186
|
updateCustomerEmail,
|
|
165
187
|
tokenizeGooglePayTokenWithBasisTheory,
|
|
166
188
|
handleGooglePayPayment,
|
|
189
|
+
countryAllowlist,
|
|
167
190
|
onSuccess,
|
|
168
191
|
onError,
|
|
169
192
|
setContextError,
|
|
@@ -193,6 +216,18 @@ export const GooglePayButton = ({ className = '', disabled = false, onSuccess, o
|
|
|
193
216
|
const paymentDataRequestUpdate = {};
|
|
194
217
|
if (intermediatePaymentData.callbackTrigger === 'SHIPPING_ADDRESS') {
|
|
195
218
|
const address = intermediatePaymentData.shippingAddress;
|
|
219
|
+
const addressCountry = (address?.countryCode || '').toUpperCase();
|
|
220
|
+
// Defense-in-depth: reject if selected country isn't in allowlist
|
|
221
|
+
if (countryAllowlist && addressCountry && !countryAllowlist.includes(addressCountry)) {
|
|
222
|
+
resolve({
|
|
223
|
+
error: {
|
|
224
|
+
reason: 'SHIPPING_ADDRESS_UNSERVICEABLE',
|
|
225
|
+
message: 'Shipping to this country is not supported',
|
|
226
|
+
intent: 'SHIPPING_ADDRESS',
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
196
231
|
const shippingAddress = {
|
|
197
232
|
address1: address?.addressLines?.[0] || '',
|
|
198
233
|
address2: address?.addressLines?.[1] || '',
|
|
@@ -270,7 +305,7 @@ export const GooglePayButton = ({ className = '', disabled = false, onSuccess, o
|
|
|
270
305
|
};
|
|
271
306
|
void processCallback();
|
|
272
307
|
});
|
|
273
|
-
}, [updateCheckoutSessionValues, reComputeOrderSummary, checkout, selectRate]);
|
|
308
|
+
}, [updateCheckoutSessionValues, reComputeOrderSummary, checkout, selectRate, countryAllowlist]);
|
|
274
309
|
// Handle payment authorization
|
|
275
310
|
const handleGooglePayAuthorized = useCallback((paymentData) => {
|
|
276
311
|
setPendingPaymentData(paymentData);
|
|
@@ -336,6 +371,9 @@ export const GooglePayButton = ({ className = '', disabled = false, onSuccess, o
|
|
|
336
371
|
? ['SHIPPING_OPTION', 'SHIPPING_ADDRESS', 'PAYMENT_AUTHORIZATION']
|
|
337
372
|
: ['PAYMENT_AUTHORIZATION'],
|
|
338
373
|
...(requiresShipping && {
|
|
374
|
+
shippingAddressParameters: countryAllowlist
|
|
375
|
+
? { allowedCountryCodes: countryAllowlist }
|
|
376
|
+
: undefined,
|
|
339
377
|
shippingOptionParameters: {
|
|
340
378
|
defaultSelectedOptionId: checkout.checkoutSession?.shippingRate?.id ||
|
|
341
379
|
(shippingMethods.length > 0 ? shippingMethods[0].identifier : ''),
|