@tagadapay/plugin-sdk 4.0.0 → 4.0.4

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 (65) hide show
  1. package/README.md +1129 -1129
  2. package/build-cdn.js +499 -499
  3. package/dist/external-tracker.js +156 -2
  4. package/dist/external-tracker.min.js +2 -2
  5. package/dist/external-tracker.min.js.map +4 -4
  6. package/dist/react/providers/TagadaProvider.js +5 -5
  7. package/dist/tagada-react-sdk-minimal.min.js +2 -2
  8. package/dist/tagada-react-sdk-minimal.min.js.map +4 -4
  9. package/dist/tagada-react-sdk.js +707 -253
  10. package/dist/tagada-react-sdk.min.js +2 -2
  11. package/dist/tagada-react-sdk.min.js.map +4 -4
  12. package/dist/tagada-sdk.js +2922 -102
  13. package/dist/tagada-sdk.min.js +2 -2
  14. package/dist/tagada-sdk.min.js.map +4 -4
  15. package/dist/v2/core/funnelClient.d.ts +40 -0
  16. package/dist/v2/core/funnelClient.js +30 -0
  17. package/dist/v2/core/pixelTracker.d.ts +51 -0
  18. package/dist/v2/core/pixelTracker.js +425 -0
  19. package/dist/v2/core/resources/checkout.d.ts +45 -1
  20. package/dist/v2/core/resources/checkout.js +13 -3
  21. package/dist/v2/core/resources/offers.d.ts +3 -3
  22. package/dist/v2/core/resources/offers.js +11 -3
  23. package/dist/v2/core/resources/promotionEvents.d.ts +5 -0
  24. package/dist/v2/core/resources/promotionEvents.js +2 -0
  25. package/dist/v2/core/resources/promotions.d.ts +6 -1
  26. package/dist/v2/core/resources/promotions.js +6 -1
  27. package/dist/v2/core/resources/shippingRates.d.ts +18 -0
  28. package/dist/v2/core/resources/shippingRates.js +18 -0
  29. package/dist/v2/core/utils/clickIdResolver.d.ts +79 -0
  30. package/dist/v2/core/utils/clickIdResolver.js +169 -0
  31. package/dist/v2/core/utils/index.d.ts +2 -0
  32. package/dist/v2/core/utils/index.js +4 -0
  33. package/dist/v2/core/utils/metaEventId.d.ts +14 -0
  34. package/dist/v2/core/utils/metaEventId.js +16 -0
  35. package/dist/v2/core/utils/previewModeIndicator.js +101 -101
  36. package/dist/v2/index.d.ts +7 -0
  37. package/dist/v2/index.js +10 -0
  38. package/dist/v2/react/components/ApplePayButton.js +50 -0
  39. package/dist/v2/react/components/FunnelScriptInjector.js +9 -9
  40. package/dist/v2/react/components/GooglePayButton.js +39 -1
  41. package/dist/v2/react/components/StripeExpressButton.js +54 -2
  42. package/dist/v2/react/hooks/payment-actions/useNgeniusThreedsAction.js +11 -11
  43. package/dist/v2/react/hooks/useCheckoutQuery.js +41 -29
  44. package/dist/v2/react/hooks/useDiscountsQuery.js +4 -0
  45. package/dist/v2/react/hooks/useFunnel.d.ts +7 -0
  46. package/dist/v2/react/hooks/useFunnel.js +2 -1
  47. package/dist/v2/react/hooks/useOfferQuery.d.ts +11 -0
  48. package/dist/v2/react/hooks/useOfferQuery.js +11 -0
  49. package/dist/v2/react/hooks/usePixelTracking.d.ts +10 -5
  50. package/dist/v2/react/hooks/usePixelTracking.js +32 -374
  51. package/dist/v2/react/hooks/usePreviewOffer.d.ts +3 -1
  52. package/dist/v2/react/hooks/usePreviewOffer.js +4 -2
  53. package/dist/v2/react/hooks/usePromotionsQuery.js +9 -3
  54. package/dist/v2/react/hooks/useShippingRatesQuery.js +36 -21
  55. package/dist/v2/react/hooks/useStepConfig.d.ts +9 -0
  56. package/dist/v2/react/hooks/useStepConfig.js +5 -1
  57. package/dist/v2/react/index.d.ts +5 -0
  58. package/dist/v2/react/index.js +9 -0
  59. package/dist/v2/react/providers/TagadaProvider.js +18 -5
  60. package/dist/v2/standalone/apple-pay-service.d.ts +1 -1
  61. package/dist/v2/standalone/index.d.ts +3 -0
  62. package/dist/v2/standalone/index.js +23 -0
  63. package/dist/v2/standalone/payment-service.d.ts +54 -1
  64. package/dist/v2/standalone/payment-service.js +228 -61
  65. package/package.json +115 -115
@@ -212,143 +212,143 @@ export function injectPreviewModeIndicator() {
212
212
  // Create container
213
213
  const container = document.createElement('div');
214
214
  container.id = 'tgd-preview-indicator';
215
- container.style.cssText = `
216
- position: fixed;
217
- bottom: 16px;
218
- right: 16px;
219
- z-index: 999999;
220
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
215
+ container.style.cssText = `
216
+ position: fixed;
217
+ bottom: 16px;
218
+ right: 16px;
219
+ z-index: 999999;
220
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
221
221
  `;
222
222
  // Create badge
223
223
  const badge = document.createElement('div');
224
- badge.style.cssText = `
225
- background: ${draftMode ? '#ff9500' : '#007aff'};
226
- color: white;
227
- padding: 8px 12px;
228
- border-radius: 8px;
229
- font-size: 13px;
230
- font-weight: 600;
231
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
232
- cursor: pointer;
233
- transition: all 0.2s ease;
234
- display: flex;
235
- align-items: center;
236
- gap: 6px;
224
+ badge.style.cssText = `
225
+ background: ${draftMode ? '#ff9500' : '#007aff'};
226
+ color: white;
227
+ padding: 8px 12px;
228
+ border-radius: 8px;
229
+ font-size: 13px;
230
+ font-weight: 600;
231
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
232
+ cursor: pointer;
233
+ transition: all 0.2s ease;
234
+ display: flex;
235
+ align-items: center;
236
+ gap: 6px;
237
237
  `;
238
- badge.innerHTML = `
239
- <span style="font-size: 16px;">🔍</span>
240
- <span>${draftMode ? 'Preview Mode' : 'Dev Mode'}</span>
238
+ badge.innerHTML = `
239
+ <span style="font-size: 16px;">🔍</span>
240
+ <span>${draftMode ? 'Preview Mode' : 'Dev Mode'}</span>
241
241
  `;
242
242
  // Create details popup (with padding-top to bridge gap with badge)
243
243
  const details = document.createElement('div');
244
- details.style.cssText = `
245
- position: absolute;
246
- bottom: calc(100% + 8px);
247
- right: 0;
248
- background: white;
249
- border: 1px solid #e5e5e5;
250
- border-radius: 8px;
251
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
252
- padding: 12px;
253
- min-width: 250px;
254
- font-size: 12px;
255
- line-height: 1.5;
256
- display: none;
244
+ details.style.cssText = `
245
+ position: absolute;
246
+ bottom: calc(100% + 8px);
247
+ right: 0;
248
+ background: white;
249
+ border: 1px solid #e5e5e5;
250
+ border-radius: 8px;
251
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
252
+ padding: 12px;
253
+ min-width: 250px;
254
+ font-size: 12px;
255
+ line-height: 1.5;
256
+ display: none;
257
257
  `;
258
258
  details.style.paddingTop = '20px'; // Extra padding to bridge the gap
259
259
  // Add invisible bridge between badge and popup to prevent flickering
260
260
  const bridge = document.createElement('div');
261
- bridge.style.cssText = `
262
- position: absolute;
263
- bottom: 100%;
264
- left: 0;
265
- right: 0;
266
- height: 8px;
267
- display: none;
261
+ bridge.style.cssText = `
262
+ position: absolute;
263
+ bottom: 100%;
264
+ left: 0;
265
+ right: 0;
266
+ height: 8px;
267
+ display: none;
268
268
  `;
269
269
  // Build details content
270
270
  let detailsHTML = '<div style="margin-bottom: 8px; font-weight: 600; color: #1d1d1f;">Current Environment</div>';
271
271
  detailsHTML += '<div style="display: flex; flex-direction: column; gap: 6px;">';
272
272
  if (draftMode) {
273
- detailsHTML += `
274
- <div style="display: flex; justify-content: space-between; color: #86868b;">
275
- <span>Draft Mode:</span>
276
- <span style="color: #ff9500; font-weight: 600;">ON</span>
277
- </div>
273
+ detailsHTML += `
274
+ <div style="display: flex; justify-content: space-between; color: #86868b;">
275
+ <span>Draft Mode:</span>
276
+ <span style="color: #ff9500; font-weight: 600;">ON</span>
277
+ </div>
278
278
  `;
279
279
  }
280
280
  if (trackingDisabled) {
281
- detailsHTML += `
282
- <div style="display: flex; justify-content: space-between; color: #86868b;">
283
- <span>Tracking:</span>
284
- <span style="color: #ff3b30; font-weight: 600;">DISABLED</span>
285
- </div>
281
+ detailsHTML += `
282
+ <div style="display: flex; justify-content: space-between; color: #86868b;">
283
+ <span>Tracking:</span>
284
+ <span style="color: #ff3b30; font-weight: 600;">DISABLED</span>
285
+ </div>
286
286
  `;
287
287
  }
288
288
  if (params.funnelEnv) {
289
- detailsHTML += `
290
- <div style="display: flex; justify-content: space-between; color: #86868b;">
291
- <span>Funnel Env:</span>
292
- <span style="color: #1d1d1f; font-weight: 600; font-family: monospace; font-size: 11px;">
293
- ${params.funnelEnv}
294
- </span>
295
- </div>
289
+ detailsHTML += `
290
+ <div style="display: flex; justify-content: space-between; color: #86868b;">
291
+ <span>Funnel Env:</span>
292
+ <span style="color: #1d1d1f; font-weight: 600; font-family: monospace; font-size: 11px;">
293
+ ${params.funnelEnv}
294
+ </span>
295
+ </div>
296
296
  `;
297
297
  }
298
298
  if (params.tagadaClientEnv) {
299
- detailsHTML += `
300
- <div style="display: flex; justify-content: space-between; color: #86868b;">
301
- <span>API Env:</span>
302
- <span style="color: #1d1d1f; font-weight: 600; font-family: monospace; font-size: 11px;">
303
- ${params.tagadaClientEnv}
304
- </span>
305
- </div>
299
+ detailsHTML += `
300
+ <div style="display: flex; justify-content: space-between; color: #86868b;">
301
+ <span>API Env:</span>
302
+ <span style="color: #1d1d1f; font-weight: 600; font-family: monospace; font-size: 11px;">
303
+ ${params.tagadaClientEnv}
304
+ </span>
305
+ </div>
306
306
  `;
307
307
  }
308
308
  if (params.tagadaClientBaseUrl) {
309
- detailsHTML += `
310
- <div style="color: #86868b;">
311
- <div style="margin-bottom: 4px;">API URL:</div>
312
- <div style="color: #1d1d1f; font-weight: 600; font-family: monospace; font-size: 10px; word-break: break-all; background: #f5f5f7; padding: 4px 6px; border-radius: 4px;">
313
- ${params.tagadaClientBaseUrl}
314
- </div>
315
- </div>
309
+ detailsHTML += `
310
+ <div style="color: #86868b;">
311
+ <div style="margin-bottom: 4px;">API URL:</div>
312
+ <div style="color: #1d1d1f; font-weight: 600; font-family: monospace; font-size: 10px; word-break: break-all; background: #f5f5f7; padding: 4px 6px; border-radius: 4px;">
313
+ ${params.tagadaClientBaseUrl}
314
+ </div>
315
+ </div>
316
316
  `;
317
317
  }
318
318
  if (params.funnelId) {
319
- detailsHTML += `
320
- <div style="color: #86868b; margin-top: 4px; padding-top: 8px; border-top: 1px solid #e5e5e5;">
321
- <div style="margin-bottom: 4px;">Funnel ID:</div>
322
- <div style="color: #1d1d1f; font-family: monospace; font-size: 10px; word-break: break-all; background: #f5f5f7; padding: 4px 6px; border-radius: 4px;">
323
- ${params.funnelId}
324
- </div>
325
- </div>
319
+ detailsHTML += `
320
+ <div style="color: #86868b; margin-top: 4px; padding-top: 8px; border-top: 1px solid #e5e5e5;">
321
+ <div style="margin-bottom: 4px;">Funnel ID:</div>
322
+ <div style="color: #1d1d1f; font-family: monospace; font-size: 10px; word-break: break-all; background: #f5f5f7; padding: 4px 6px; border-radius: 4px;">
323
+ ${params.funnelId}
324
+ </div>
325
+ </div>
326
326
  `;
327
327
  }
328
328
  detailsHTML += '</div>';
329
- detailsHTML += `
330
- <div style="margin-top: 12px; padding-top: 8px; border-top: 1px solid #e5e5e5; font-size: 11px; color: #86868b; text-align: center;">
331
- Add <code style="background: #f5f5f7; padding: 2px 4px; border-radius: 3px;">?forceReset=true</code> to reset
332
- </div>
329
+ detailsHTML += `
330
+ <div style="margin-top: 12px; padding-top: 8px; border-top: 1px solid #e5e5e5; font-size: 11px; color: #86868b; text-align: center;">
331
+ Add <code style="background: #f5f5f7; padding: 2px 4px; border-radius: 3px;">?forceReset=true</code> to reset
332
+ </div>
333
333
  `;
334
334
  // Add action button
335
- detailsHTML += `
336
- <div style="margin-top: 12px; padding-top: 8px; border-top: 1px solid #e5e5e5;">
337
- <button id="tgd-leave-preview" style="
338
- background: #ff3b30;
339
- color: white;
340
- border: none;
341
- border-radius: 6px;
342
- padding: 10px 12px;
343
- font-size: 13px;
344
- font-weight: 600;
345
- cursor: pointer;
346
- transition: opacity 0.2s;
347
- width: 100%;
348
- ">
349
- 🚪 Leave Preview Mode
350
- </button>
351
- </div>
335
+ detailsHTML += `
336
+ <div style="margin-top: 12px; padding-top: 8px; border-top: 1px solid #e5e5e5;">
337
+ <button id="tgd-leave-preview" style="
338
+ background: #ff3b30;
339
+ color: white;
340
+ border: none;
341
+ border-radius: 6px;
342
+ padding: 10px 12px;
343
+ font-size: 13px;
344
+ font-weight: 600;
345
+ cursor: pointer;
346
+ transition: opacity 0.2s;
347
+ width: 100%;
348
+ ">
349
+ 🚪 Leave Preview Mode
350
+ </button>
351
+ </div>
352
352
  `;
353
353
  details.innerHTML = detailsHTML;
354
354
  // Hover behavior - keep popup visible when hovering over badge, bridge, or popup
@@ -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,
@@ -58,15 +58,15 @@ function parseScriptContent(content) {
58
58
  }
59
59
  /** Wrap inline JS in an error-handling IIFE */
60
60
  function wrapInErrorHandler(scriptName, code) {
61
- return `
62
- (function() {
63
- try {
64
- // Script: ${scriptName}
65
- ${code}
66
- } catch (error) {
67
- console.error('[TagadaPay] StepConfig script "${scriptName}" error:', error);
68
- }
69
- })();
61
+ return `
62
+ (function() {
63
+ try {
64
+ // Script: ${scriptName}
65
+ ${code}
66
+ } catch (error) {
67
+ console.error('[TagadaPay] StepConfig script "${scriptName}" error:', error);
68
+ }
69
+ })();
70
70
  `;
71
71
  }
72
72
  /** Inject a DOM element at the specified position */
@@ -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 : ''),
@@ -5,6 +5,7 @@ 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,6 +19,14 @@ 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()), []);
22
31
  // Push amount/currency changes (e.g. shipping cost added once address resolved,
23
32
  // promo applied, quantity changed) to the mounted ExpressCheckoutElement.
@@ -41,6 +50,7 @@ function StripeExpressButtonInner({ checkout, processorId, enabledExpressMethods
41
50
  // This mirrors what ApplePayButton and GooglePayButton do before processing payment,
42
51
  // and ensures customer.email is present for the backend email validation.
43
52
  const billing = event.billingDetails;
53
+ const eventShipping = event.shippingAddress;
44
54
  if (billing) {
45
55
  const nameParts = (billing.name ?? '').trim().split(/\s+/);
46
56
  const firstName = nameParts[0] ?? '';
@@ -57,9 +67,33 @@ function StripeExpressButtonInner({ checkout, processorId, enabledExpressMethods
57
67
  phone: billing.phone ?? '',
58
68
  email: billing.email ?? '',
59
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
+ }
60
94
  await updateCheckoutSessionValues({
61
95
  data: {
62
- shippingAddress: billingAddress,
96
+ shippingAddress: shippingFromEvent,
63
97
  billingAddress,
64
98
  },
65
99
  });
@@ -139,7 +173,16 @@ function StripeExpressButtonInner({ checkout, processorId, enabledExpressMethods
139
173
  onError?.(msg);
140
174
  }
141
175
  };
142
- return (_jsx("div", { style: { visibility: visible ? 'visible' : 'hidden' }, children: _jsx(ExpressCheckoutElement, { onReady: ({ availablePaymentMethods }) => {
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 }) => {
143
186
  if (availablePaymentMethods) {
144
187
  setVisible(true);
145
188
  handleAddExpressId('stripe_express_checkout');
@@ -161,6 +204,15 @@ function StripeExpressButtonInner({ checkout, processorId, enabledExpressMethods
161
204
  },
162
205
  emailRequired: true,
163
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
+ : {}),
164
216
  } }) }));
165
217
  }
166
218
  // Outer component — resolves Stripe instance and provides <Elements> context
@@ -16,17 +16,17 @@ function ensureNgeniusContainer() {
16
16
  if (!container) {
17
17
  container = document.createElement('div');
18
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;
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
30
  `;
31
31
  document.body.appendChild(container);
32
32
  console.log('[N-Genius 3DS] Container created and appended to body');