@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.
Files changed (81) hide show
  1. package/dist/external-tracker.js +160 -6
  2. package/dist/external-tracker.min.js +2 -2
  3. package/dist/external-tracker.min.js.map +4 -4
  4. package/dist/react/config/payment.d.ts +2 -2
  5. package/dist/react/config/payment.js +5 -5
  6. package/dist/react/hooks/usePayment.d.ts +7 -0
  7. package/dist/react/hooks/usePayment.js +1 -0
  8. package/dist/tagada-react-sdk-minimal.min.js +2 -2
  9. package/dist/tagada-react-sdk-minimal.min.js.map +4 -4
  10. package/dist/tagada-react-sdk.js +2220 -1428
  11. package/dist/tagada-react-sdk.min.js +2 -2
  12. package/dist/tagada-react-sdk.min.js.map +4 -4
  13. package/dist/tagada-sdk.js +3784 -128
  14. package/dist/tagada-sdk.min.js +2 -2
  15. package/dist/tagada-sdk.min.js.map +4 -4
  16. package/dist/v2/core/config/environment.d.ts +3 -3
  17. package/dist/v2/core/config/environment.js +7 -7
  18. package/dist/v2/core/funnelClient.d.ts +42 -0
  19. package/dist/v2/core/funnelClient.js +30 -0
  20. package/dist/v2/core/pixelTracker.d.ts +51 -0
  21. package/dist/v2/core/pixelTracker.js +425 -0
  22. package/dist/v2/core/resources/checkout.d.ts +45 -1
  23. package/dist/v2/core/resources/checkout.js +13 -3
  24. package/dist/v2/core/resources/funnel.d.ts +1 -1
  25. package/dist/v2/core/resources/geo.d.ts +50 -0
  26. package/dist/v2/core/resources/geo.js +35 -0
  27. package/dist/v2/core/resources/offers.d.ts +1 -1
  28. package/dist/v2/core/resources/offers.js +3 -1
  29. package/dist/v2/core/resources/payments.d.ts +19 -1
  30. package/dist/v2/core/resources/payments.js +8 -0
  31. package/dist/v2/core/resources/promotionEvents.d.ts +5 -0
  32. package/dist/v2/core/resources/promotionEvents.js +2 -0
  33. package/dist/v2/core/resources/promotions.d.ts +6 -1
  34. package/dist/v2/core/resources/promotions.js +6 -1
  35. package/dist/v2/core/resources/shippingRates.d.ts +18 -0
  36. package/dist/v2/core/resources/shippingRates.js +18 -0
  37. package/dist/v2/core/utils/clickIdResolver.d.ts +79 -0
  38. package/dist/v2/core/utils/clickIdResolver.js +169 -0
  39. package/dist/v2/core/utils/index.d.ts +2 -0
  40. package/dist/v2/core/utils/index.js +4 -0
  41. package/dist/v2/core/utils/metaEventId.d.ts +14 -0
  42. package/dist/v2/core/utils/metaEventId.js +16 -0
  43. package/dist/v2/index.d.ts +7 -0
  44. package/dist/v2/index.js +10 -0
  45. package/dist/v2/react/components/ApplePayButton.js +50 -0
  46. package/dist/v2/react/components/FunnelScriptInjector.js +158 -10
  47. package/dist/v2/react/components/GooglePayButton.js +39 -1
  48. package/dist/v2/react/components/StripeExpressButton.d.ts +8 -0
  49. package/dist/v2/react/components/StripeExpressButton.js +76 -3
  50. package/dist/v2/react/hooks/payment-actions/useNgeniusThreedsAction.d.ts +15 -0
  51. package/dist/v2/react/hooks/payment-actions/useNgeniusThreedsAction.js +166 -0
  52. package/dist/v2/react/hooks/payment-actions/usePaymentActionHandler.js +12 -0
  53. package/dist/v2/react/hooks/payment-processing/usePaymentProcessors.js +1 -0
  54. package/dist/v2/react/hooks/useCheckoutQuery.js +41 -29
  55. package/dist/v2/react/hooks/useDiscountsQuery.js +4 -0
  56. package/dist/v2/react/hooks/useFunnel.d.ts +7 -0
  57. package/dist/v2/react/hooks/useFunnel.js +2 -1
  58. package/dist/v2/react/hooks/useISOData.js +25 -7
  59. package/dist/v2/react/hooks/usePaymentPolling.d.ts +1 -1
  60. package/dist/v2/react/hooks/usePixelTracking.d.ts +10 -5
  61. package/dist/v2/react/hooks/usePixelTracking.js +32 -374
  62. package/dist/v2/react/hooks/usePreviewOffer.d.ts +3 -1
  63. package/dist/v2/react/hooks/usePreviewOffer.js +8 -2
  64. package/dist/v2/react/hooks/usePromotionsQuery.js +9 -3
  65. package/dist/v2/react/hooks/useShippingRatesQuery.js +36 -21
  66. package/dist/v2/react/hooks/useStepConfig.d.ts +9 -0
  67. package/dist/v2/react/hooks/useStepConfig.js +5 -1
  68. package/dist/v2/react/index.d.ts +4 -0
  69. package/dist/v2/react/index.js +8 -0
  70. package/dist/v2/react/providers/ExpressPaymentMethodsProvider.js +12 -4
  71. package/dist/v2/react/providers/TagadaProvider.js +13 -0
  72. package/dist/v2/standalone/apple-pay-service.d.ts +12 -0
  73. package/dist/v2/standalone/apple-pay-service.js +12 -0
  74. package/dist/v2/standalone/external-tracker.d.ts +1 -1
  75. package/dist/v2/standalone/google-pay-service.d.ts +9 -0
  76. package/dist/v2/standalone/google-pay-service.js +9 -0
  77. package/dist/v2/standalone/index.d.ts +11 -1
  78. package/dist/v2/standalone/index.js +30 -0
  79. package/dist/v2/standalone/payment-service.d.ts +72 -6
  80. package/dist/v2/standalone/payment-service.js +285 -65
  81. 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
+ }
@@ -13,3 +13,5 @@ export * from './orderBump';
13
13
  export * from './sessionStorage';
14
14
  export * from './funnelQueryKeys';
15
15
  export * from './configHotReload';
16
+ export * from './metaEventId';
17
+ export * from './clickIdResolver';
@@ -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
+ }
@@ -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
- if (context?.currentStepId) {
132
- // @ts-expect-error - Updating window property
133
- window.Tagada.pageType = context.currentStepId;
193
+ prev.pageType = context?.currentStepId || null;
194
+ prev.isInitialized = isInitialized;
195
+ prev.ressources = nextResources;
196
+ prev.stepConfig = getAssignedStepConfig() || null;
197
+ prev.funnel = context
198
+ ? {
199
+ sessionId: context.sessionId,
200
+ funnelId: context.funnelId,
201
+ currentStepId: context.currentStepId,
202
+ previousStepId: context.previousStepId,
203
+ ressources: context.resources,
204
+ }
205
+ : null;
206
+ // Fire V1-compat listeners when specific resources change.
207
+ // Support both V1 ("checkout") and native V2 ("checkoutSession") keys.
208
+ const prevCheckout = prevResources?.checkoutSession || prevResources?.checkout;
209
+ const nextCheckout = nextResources?.checkoutSession || nextResources?.checkout;
210
+ if (prevCheckout !== nextCheckout) {
211
+ prev._fireCheckoutSessionUpdate?.({ checkoutSession: nextCheckout || null });
212
+ }
213
+ const prevSummary = prevResources?.orderSummary || prevResources?.summary;
214
+ const nextSummary = nextResources?.orderSummary || nextResources?.summary;
215
+ if (prevSummary !== nextSummary) {
216
+ prev._fireOrderSummaryUpdate?.({ orderSummary: nextSummary || null });
217
+ }
218
+ const prevOrder = prevResources?.order;
219
+ const nextOrder = nextResources?.order;
220
+ if (prevOrder !== nextOrder) {
221
+ prev._fireOrderUpdate?.({ order: nextOrder || null });
134
222
  }
135
- // @ts-expect-error - Updating window property
136
- window.Tagada.isInitialized = isInitialized;
137
- // @ts-expect-error - Updating window property
138
- window.Tagada.ressources = context?.resources || null;
139
223
  return;
140
224
  }
225
+ // Listener registries (captured via closures — outlive re-renders on window.Tagada)
226
+ const checkoutSessionListeners = new Set();
227
+ const orderSummaryListeners = new Set();
228
+ const orderListeners = new Set();
141
229
  // @ts-expect-error - Adding utilities to window
142
230
  window.Tagada = {
143
231
  // Wait for DOM to be ready
@@ -261,9 +349,36 @@ export function FunnelScriptInjector({ context, isInitialized }) {
261
349
  pageType: context?.currentStepId || null,
262
350
  isInitialized: isInitialized,
263
351
  ressources: context?.resources || null,
264
- // Convenience getters so scripts can use Tagada.order / Tagada.checkout
265
- get order() { return this.ressources?.order || null; },
266
- get checkout() { return this.ressources?.checkout || null; },
352
+ // Rich data caches bridged from React Query (useCheckoutQuery / useOrderQuery).
353
+ // Populated by the useEffect above. Prefer these over thin funnel-context refs.
354
+ _richCheckoutSession: null,
355
+ _richOrderSummary: null,
356
+ _richOrder: null,
357
+ // V1-compat: Tagada.order and Tagada.checkout.session return full data.
358
+ // Prefer React-Query-fetched data, fall back to funnel context resources.
359
+ get order() {
360
+ return this._richOrder || this.ressources?.order || null;
361
+ },
362
+ get checkout() {
363
+ const ressources = this.ressources;
364
+ const richSession = this._richCheckoutSession;
365
+ const richSummary = this._richOrderSummary;
366
+ const resource = richSession ||
367
+ ressources?.checkoutSession ||
368
+ ressources?.checkout ||
369
+ null;
370
+ const orderSummary = richSummary ||
371
+ ressources?.orderSummary ||
372
+ ressources?.summary ||
373
+ null;
374
+ if (!resource && !orderSummary)
375
+ return null;
376
+ return {
377
+ ...(resource || {}),
378
+ session: resource,
379
+ orderSummary,
380
+ };
381
+ },
267
382
  funnel: context
268
383
  ? {
269
384
  sessionId: context.sessionId,
@@ -274,6 +389,39 @@ export function FunnelScriptInjector({ context, isInitialized }) {
274
389
  }
275
390
  : null,
276
391
  stepConfig: getAssignedStepConfig() || null,
392
+ // V1-compat reactive listeners. Fire when the corresponding resource
393
+ // reference changes in the funnel context (see update branch above).
394
+ onCheckoutSessionUpdate: (cb) => {
395
+ checkoutSessionListeners.add(cb);
396
+ return () => checkoutSessionListeners.delete(cb);
397
+ },
398
+ onOrderSummaryUpdate: (cb) => {
399
+ orderSummaryListeners.add(cb);
400
+ return () => orderSummaryListeners.delete(cb);
401
+ },
402
+ onOrderUpdate: (cb) => {
403
+ orderListeners.add(cb);
404
+ return () => orderListeners.delete(cb);
405
+ },
406
+ // Internal fire hooks used by the update branch
407
+ _fireCheckoutSessionUpdate: (data) => checkoutSessionListeners.forEach(cb => { try {
408
+ cb(data);
409
+ }
410
+ catch (e) {
411
+ console.error('[Tagada] onCheckoutSessionUpdate listener threw:', e);
412
+ } }),
413
+ _fireOrderSummaryUpdate: (data) => orderSummaryListeners.forEach(cb => { try {
414
+ cb(data);
415
+ }
416
+ catch (e) {
417
+ console.error('[Tagada] onOrderSummaryUpdate listener threw:', e);
418
+ } }),
419
+ _fireOrderUpdate: (data) => orderListeners.forEach(cb => { try {
420
+ cb(data);
421
+ }
422
+ catch (e) {
423
+ console.error('[Tagada] onOrderUpdate listener threw:', e);
424
+ } }),
277
425
  };
278
426
  }, [context, isInitialized]);
279
427
  useEffect(() => {
@@ -8,6 +8,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 : ''),