@tagadapay/plugin-sdk 3.1.22 → 3.1.25

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 (89) hide show
  1. package/build-cdn.js +274 -6
  2. package/dist/external-tracker.js +476 -6774
  3. package/dist/external-tracker.min.js +2 -25
  4. package/dist/external-tracker.min.js.map +4 -4
  5. package/dist/react/config/payment.d.ts +14 -4
  6. package/dist/react/config/payment.js +47 -9
  7. package/dist/react/hooks/useCheckout.d.ts +3 -0
  8. package/dist/react/hooks/useCheckout.js +11 -3
  9. package/dist/react/hooks/usePluginConfig.js +9 -10
  10. package/dist/react/providers/TagadaProvider.js +1 -1
  11. package/dist/tagada-react-sdk-minimal.min.js +36 -0
  12. package/dist/tagada-react-sdk-minimal.min.js.map +7 -0
  13. package/dist/tagada-react-sdk.js +37988 -0
  14. package/dist/tagada-react-sdk.min.js +78 -0
  15. package/dist/tagada-react-sdk.min.js.map +7 -0
  16. package/dist/tagada-sdk.js +7847 -6420
  17. package/dist/tagada-sdk.min.js +4 -22
  18. package/dist/tagada-sdk.min.js.map +4 -4
  19. package/dist/v2/cdn-react-minimal.d.ts +23 -0
  20. package/dist/v2/cdn-react-minimal.js +26 -0
  21. package/dist/v2/core/client.js +2 -1
  22. package/dist/v2/core/config/environment.js +2 -1
  23. package/dist/v2/core/funnelClient.d.ts +106 -10
  24. package/dist/v2/core/funnelClient.js +122 -28
  25. package/dist/v2/core/index.d.ts +0 -1
  26. package/dist/v2/core/index.js +0 -2
  27. package/dist/v2/core/isoData.d.ts +4 -4
  28. package/dist/v2/core/isoData.js +7 -7
  29. package/dist/v2/core/pixelMapping.js +64 -26
  30. package/dist/v2/core/resources/apiClient.d.ts +18 -14
  31. package/dist/v2/core/resources/apiClient.js +151 -109
  32. package/dist/v2/core/resources/checkout.d.ts +10 -0
  33. package/dist/v2/core/resources/checkout.js +6 -0
  34. package/dist/v2/core/resources/expressPaymentMethods.d.ts +1 -0
  35. package/dist/v2/core/resources/index.d.ts +1 -1
  36. package/dist/v2/core/resources/index.js +1 -1
  37. package/dist/v2/core/resources/offers.js +4 -4
  38. package/dist/v2/core/resources/payments.d.ts +8 -2
  39. package/dist/v2/core/resources/payments.js +1 -0
  40. package/dist/v2/core/resources/postPurchases.d.ts +17 -0
  41. package/dist/v2/core/resources/postPurchases.js +20 -0
  42. package/dist/v2/core/utils/currency.d.ts +3 -0
  43. package/dist/v2/core/utils/currency.js +40 -2
  44. package/dist/v2/core/utils/deviceInfo.d.ts +1 -10
  45. package/dist/v2/core/utils/deviceInfo.js +153 -76
  46. package/dist/v2/core/utils/order.d.ts +2 -0
  47. package/dist/v2/core/utils/pluginConfig.js +18 -22
  48. package/dist/v2/core/utils/previewMode.js +12 -0
  49. package/dist/v2/index.d.ts +4 -3
  50. package/dist/v2/index.js +4 -2
  51. package/dist/v2/react/components/ApplePayButton.js +39 -16
  52. package/dist/v2/react/components/FunnelScriptInjector.js +145 -77
  53. package/dist/v2/react/components/StripeExpressButton.d.ts +13 -0
  54. package/dist/v2/react/components/StripeExpressButton.js +170 -0
  55. package/dist/v2/react/components/WhopCheckout.js +7 -1
  56. package/dist/v2/react/hooks/payment-actions/useAirwallexRadarAction.js +1 -0
  57. package/dist/v2/react/hooks/payment-actions/useProcessorAuthAction.js +21 -3
  58. package/dist/v2/react/hooks/useApiQuery.d.ts +1 -1
  59. package/dist/v2/react/hooks/useApiQuery.js +1 -1
  60. package/dist/v2/react/hooks/useApplePayCheckout.js +8 -8
  61. package/dist/v2/react/hooks/useCheckoutQuery.d.ts +10 -0
  62. package/dist/v2/react/hooks/useCheckoutQuery.js +27 -15
  63. package/dist/v2/react/hooks/useFunnel.d.ts +15 -4
  64. package/dist/v2/react/hooks/useFunnel.js +8 -4
  65. package/dist/v2/react/hooks/useGoogleAutocomplete.d.ts +2 -0
  66. package/dist/v2/react/hooks/useGoogleAutocomplete.js +29 -15
  67. package/dist/v2/react/hooks/useISOData.d.ts +2 -5
  68. package/dist/v2/react/hooks/useISOData.js +25 -26
  69. package/dist/v2/react/hooks/usePaymentPolling.d.ts +2 -2
  70. package/dist/v2/react/hooks/usePixelTracking.js +151 -70
  71. package/dist/v2/react/hooks/usePostPurchasesQuery.js +34 -2
  72. package/dist/v2/react/hooks/usePreviewOffer.js +1 -1
  73. package/dist/v2/react/hooks/useRemappableParams.d.ts +2 -6
  74. package/dist/v2/react/hooks/useRemappableParams.js +23 -23
  75. package/dist/v2/react/hooks/useSetPaymentMethod.d.ts +16 -0
  76. package/dist/v2/react/hooks/useSetPaymentMethod.js +33 -0
  77. package/dist/v2/react/hooks/useStepConfig.d.ts +23 -6
  78. package/dist/v2/react/hooks/useStepConfig.js +14 -7
  79. package/dist/v2/react/hooks/useTranslation.js +23 -8
  80. package/dist/v2/react/index.d.ts +8 -1
  81. package/dist/v2/react/index.js +3 -0
  82. package/dist/v2/react/providers/ExpressPaymentMethodsProvider.d.ts +8 -0
  83. package/dist/v2/react/providers/ExpressPaymentMethodsProvider.js +106 -10
  84. package/dist/v2/react/providers/TagadaProvider.js +5 -5
  85. package/dist/v2/standalone/index.d.ts +21 -3
  86. package/dist/v2/standalone/index.js +25 -3
  87. package/dist/v2/standalone/payment-service.d.ts +134 -0
  88. package/dist/v2/standalone/payment-service.js +929 -0
  89. package/package.json +4 -2
@@ -44,22 +44,27 @@ export function PixelTrackingProvider({ children }) {
44
44
  return () => { isMountedRef.current = false; };
45
45
  }, []);
46
46
  // ---- Initialize pixel scripts once ----
47
+ // Wait for all external scripts to actually load before marking initialized,
48
+ // so that pixel helpers (TikTok, etc.) can intercept events properly.
47
49
  useEffect(() => {
48
50
  if (!pixels || pixelsInitialized || !isMountedRef.current)
49
51
  return;
52
+ const loadPromises = [];
50
53
  try {
51
54
  pixels.facebook?.forEach((px) => { if (px.enabled && px.pixelId)
52
- initMetaPixel(px.pixelId); });
55
+ loadPromises.push(initMetaPixel(px.pixelId)); });
53
56
  pixels.tiktok?.forEach((px) => { if (px.enabled && px.pixelId)
54
- initTikTokPixel(px.pixelId); });
57
+ loadPromises.push(initTikTokPixel(px.pixelId)); });
55
58
  pixels.snapchat?.forEach((px) => { if (px.enabled && px.pixelId)
56
- initSnapchatPixel(px.pixelId); });
59
+ loadPromises.push(initSnapchatPixel(px.pixelId)); });
57
60
  pixels.pinterest?.forEach((px) => { if (px.enabled && px.pixelId)
58
- initPinterestPixel(px.pixelId); });
61
+ loadPromises.push(initPinterestPixel(px.pixelId)); });
59
62
  pixels.gtm?.forEach((px) => { if (px.enabled && px.containerId)
60
- initGTM(px.containerId); });
61
- if (isMountedRef.current)
62
- setPixelsInitialized(true);
63
+ loadPromises.push(initGTM(px.containerId)); });
64
+ Promise.all(loadPromises).then(() => {
65
+ if (isMountedRef.current)
66
+ setPixelsInitialized(true);
67
+ });
63
68
  }
64
69
  catch (error) {
65
70
  console.error('[SDK Pixels] Initialization error:', error);
@@ -73,8 +78,21 @@ export function PixelTrackingProvider({ children }) {
73
78
  return;
74
79
  try {
75
80
  const events = resolvePixelEvents(pixels, eventName, parameters);
76
- for (const { provider, mapped } of events) {
77
- fire(provider, mapped.name, mapped.params);
81
+ // Deduplicate by provider: Meta/Snapchat/Pinterest SDKs broadcast
82
+ // to all registered pixel IDs internally, so we only need to fire once per
83
+ // provider. GTM and TikTok need per-pixel firing (GTM for Google Ads
84
+ // send_to targeting, TikTok because ttq.instance() targets a specific pixel).
85
+ const firedProviders = new Set();
86
+ for (const { provider, mapped, pixel } of events) {
87
+ if (provider === 'gtm' || provider === 'tiktok') {
88
+ fire(provider, mapped.name, mapped.params, pixel);
89
+ }
90
+ else {
91
+ if (!firedProviders.has(provider)) {
92
+ fire(provider, mapped.name, mapped.params);
93
+ firedProviders.add(provider);
94
+ }
95
+ }
78
96
  }
79
97
  }
80
98
  catch (error) {
@@ -105,7 +123,7 @@ export function usePixelTracking() {
105
123
  // Browser-specific: fire an event to the correct global pixel function
106
124
  // ---------------------------------------------------------------------------
107
125
  /* eslint-disable @typescript-eslint/no-explicit-any */
108
- function fire(provider, name, params) {
126
+ function fire(provider, name, params, pixel) {
109
127
  if (typeof window === 'undefined')
110
128
  return;
111
129
  const w = window;
@@ -113,15 +131,19 @@ function fire(provider, name, params) {
113
131
  case 'facebook':
114
132
  w.fbq?.('track', name, params);
115
133
  break;
116
- case 'tiktok':
117
- // TikTok handles page views via ttq.page(), not ttq.track('Pageview')
134
+ case 'tiktok': {
135
+ // Use ttq.instance(pixelId) to target each pixel individually,
136
+ // since the global ttq.track() only fires for the sdkid pixel.
137
+ const pixelId = pixel && 'pixelId' in pixel ? pixel.pixelId : null;
138
+ const target = pixelId && w.ttq?.instance ? w.ttq.instance(pixelId) : w.ttq;
118
139
  if (name === 'Pageview') {
119
- w.ttq?.page?.();
140
+ target?.page?.();
120
141
  }
121
142
  else {
122
- w.ttq?.track?.(name, params);
143
+ target?.track?.(name, params);
123
144
  }
124
145
  break;
146
+ }
125
147
  case 'snapchat':
126
148
  w.snaptr?.('track', name, params);
127
149
  break;
@@ -161,33 +183,54 @@ function fireGTM(event, params) {
161
183
  // ===========================================================================
162
184
  /* eslint-disable @typescript-eslint/no-explicit-any */
163
185
  const win = (typeof window !== 'undefined' ? window : undefined);
186
+ const SCRIPT_LOAD_TIMEOUT_MS = 5000;
187
+ function waitForScriptLoad(script) {
188
+ // If the script has already loaded, resolve immediately
189
+ if (script.dataset.loaded)
190
+ return Promise.resolve();
191
+ return new Promise((resolve) => {
192
+ const done = () => { script.dataset.loaded = '1'; clearTimeout(timer); resolve(); };
193
+ const timer = setTimeout(done, SCRIPT_LOAD_TIMEOUT_MS);
194
+ script.addEventListener('load', done, { once: true });
195
+ script.addEventListener('error', done, { once: true });
196
+ });
197
+ }
198
+ let _metaScriptEl = null;
164
199
  function initMetaPixel(pixelId) {
165
- if (!win || win.fbq)
166
- return;
167
- const n = function (...args) {
168
- if (n.callMethod)
169
- n.callMethod(...args);
170
- else
171
- n.queue.push(args);
172
- };
173
- n.queue = [];
174
- n.loaded = true;
175
- n.version = '2.0';
176
- win.fbq = n;
177
- if (!win._fbq)
178
- win._fbq = n;
179
- const t = document.createElement('script');
180
- t.async = true;
181
- t.src = 'https://connect.facebook.net/en_US/fbevents.js';
182
- const s = document.getElementsByTagName('script')[0];
183
- s?.parentNode?.insertBefore(t, s);
200
+ if (!win)
201
+ return Promise.resolve();
202
+ // Initialize Meta base code once
203
+ if (!win.fbq) {
204
+ const n = function (...args) {
205
+ if (n.callMethod)
206
+ n.callMethod(...args);
207
+ else
208
+ n.queue.push(args);
209
+ };
210
+ n.queue = [];
211
+ n.loaded = true;
212
+ n.version = '2.0';
213
+ win.fbq = n;
214
+ if (!win._fbq)
215
+ win._fbq = n;
216
+ const t = document.createElement('script');
217
+ t.async = true;
218
+ t.src = 'https://connect.facebook.net/en_US/fbevents.js';
219
+ const s = document.getElementsByTagName('script')[0];
220
+ s?.parentNode?.insertBefore(t, s);
221
+ _metaScriptEl = t;
222
+ }
223
+ // Register each pixel ID (fbq supports multiple pixels via multiple init calls)
184
224
  win.fbq('init', pixelId);
225
+ return _metaScriptEl ? waitForScriptLoad(_metaScriptEl) : Promise.resolve();
185
226
  }
227
+ let _tiktokBaseInitialized = false;
186
228
  function initTikTokPixel(pixelId) {
187
229
  if (!win)
188
- return;
189
- // Initialize TikTok base code once (mirrors official TikTok snippet)
190
- if (!win.ttq) {
230
+ return Promise.resolve();
231
+ // Initialize TikTok base code once
232
+ if (!_tiktokBaseInitialized) {
233
+ _tiktokBaseInitialized = true;
191
234
  win.TiktokAnalyticsObject = 'ttq';
192
235
  const ttq = (win.ttq = win.ttq || []);
193
236
  ttq.methods = [
@@ -225,57 +268,82 @@ function initTikTokPixel(pixelId) {
225
268
  p?.parentNode?.insertBefore(s, p);
226
269
  };
227
270
  }
228
- // Skip if this specific pixel ID is already loaded
229
- if (win.ttq?._i?.[pixelId])
230
- return;
271
+ // Skip if this specific pixel ID is already registered
272
+ if (win.ttq._i?.[pixelId])
273
+ return Promise.resolve();
274
+ // Register pixel and load its script
231
275
  win.ttq.load(pixelId);
232
- // Note: ttq.page() is NOT called here the Provider's auto page-view
233
- // effect fires track('PageView') which handles it. Calling both would
234
- // double-count page views in TikTok Ads Manager.
276
+ // Find the script we just created and wait for it
277
+ const scripts = document.querySelectorAll('script[src*="analytics.tiktok.com/i18n/pixel/events.js"]');
278
+ const lastScript = scripts[scripts.length - 1];
279
+ return lastScript ? waitForScriptLoad(lastScript) : Promise.resolve();
235
280
  }
281
+ let _snapchatScriptEl = null;
236
282
  function initSnapchatPixel(pixelId) {
237
- if (!win || win.snaptr)
238
- return;
239
- const a = function (...args) {
240
- if (a.handleRequest)
241
- a.handleRequest(...args);
242
- else
243
- a.queue.push(args);
244
- };
245
- a.queue = [];
246
- win.snaptr = a;
247
- const r = document.createElement('script');
248
- r.async = true;
249
- r.src = 'https://sc-static.net/scevent.min.js';
250
- const u = document.getElementsByTagName('script')[0];
251
- u?.parentNode?.insertBefore(r, u);
283
+ if (!win)
284
+ return Promise.resolve();
285
+ // Initialize Snapchat base code once
286
+ if (!win.snaptr) {
287
+ const a = function (...args) {
288
+ if (a.handleRequest)
289
+ a.handleRequest(...args);
290
+ else
291
+ a.queue.push(args);
292
+ };
293
+ a.queue = [];
294
+ win.snaptr = a;
295
+ const r = document.createElement('script');
296
+ r.async = true;
297
+ r.src = 'https://sc-static.net/scevent.min.js';
298
+ const u = document.getElementsByTagName('script')[0];
299
+ u?.parentNode?.insertBefore(r, u);
300
+ _snapchatScriptEl = r;
301
+ }
302
+ // Register each pixel ID (snaptr supports multiple pixels via multiple init calls)
252
303
  win.snaptr('init', pixelId);
304
+ return _snapchatScriptEl ? waitForScriptLoad(_snapchatScriptEl) : Promise.resolve();
253
305
  }
306
+ let _pinterestScriptEl = null;
254
307
  function initPinterestPixel(pixelId) {
255
- if (!win || win.pintrk)
256
- return;
257
- const a = function (...args) { a.queue.push(args); };
258
- a.queue = [];
259
- win.pintrk = a;
260
- const s = document.createElement('script');
261
- s.async = true;
262
- s.src = 'https://s.pinimg.com/ct/core.js';
263
- const u = document.getElementsByTagName('script')[0];
264
- u?.parentNode?.insertBefore(s, u);
308
+ if (!win)
309
+ return Promise.resolve();
310
+ // Initialize Pinterest base code once
311
+ if (!win.pintrk) {
312
+ const a = function (...args) { a.queue.push(args); };
313
+ a.queue = [];
314
+ win.pintrk = a;
315
+ const s = document.createElement('script');
316
+ s.async = true;
317
+ s.src = 'https://s.pinimg.com/ct/core.js';
318
+ const u = document.getElementsByTagName('script')[0];
319
+ u?.parentNode?.insertBefore(s, u);
320
+ _pinterestScriptEl = s;
321
+ }
322
+ // Register each pixel ID (pintrk supports multiple pixels via multiple load calls)
265
323
  win.pintrk('load', pixelId);
266
324
  // Note: pintrk('page') is NOT called here — the Provider's auto page-view
267
325
  // effect handles it via fire(). Calling both would double-count page views.
326
+ return _pinterestScriptEl ? waitForScriptLoad(_pinterestScriptEl) : Promise.resolve();
268
327
  }
269
328
  function initGTM(containerId) {
270
329
  if (!win || !containerId)
271
- return;
330
+ return Promise.resolve();
272
331
  const isGtmContainer = containerId.startsWith('GTM-');
273
332
  const scriptUrlPart = isGtmContainer
274
333
  ? `googletagmanager.com/gtm.js?id=${containerId}`
275
334
  : `googletagmanager.com/gtag/js?id=${containerId}`;
276
335
  if (document.querySelector(`script[src*="${scriptUrlPart}"]`))
277
- return;
336
+ return Promise.resolve();
278
337
  win.dataLayer = win.dataLayer || [];
338
+ // Push referrer domain context into dataLayer before GTM loads, so tags
339
+ // inside the container can use it for cross-domain tracking configuration.
340
+ try {
341
+ const ref = document.referrer && new URL(document.referrer).hostname;
342
+ if (ref && ref !== window.location.hostname) {
343
+ win.dataLayer.push({ tagada_referrer_domain: ref });
344
+ }
345
+ }
346
+ catch { /* ignore invalid referrer */ }
279
347
  if (isGtmContainer) {
280
348
  win.dataLayer.push({ 'gtm.start': new Date().getTime(), event: 'gtm.js' });
281
349
  const f = document.getElementsByTagName('script')[0];
@@ -295,13 +363,25 @@ function initGTM(containerId) {
295
363
  iframe.style.visibility = 'hidden';
296
364
  noscript.appendChild(iframe);
297
365
  (document.body || document.getElementsByTagName('body')[0])?.insertBefore(noscript, document.body?.firstChild ?? null);
366
+ return waitForScriptLoad(j);
298
367
  }
299
368
  else {
300
369
  if (!win.gtag) {
301
370
  win.gtag = function (..._args) { win.dataLayer.push(arguments); };
302
371
  }
303
372
  win.gtag('js', new Date());
304
- win.gtag('config', containerId);
373
+ // Enable cross-domain tracking: accept incoming _gl parameter from referring
374
+ // domains (e.g. Shopify store → TagadaPay checkout) so Google preserves the
375
+ // client-id/session across the redirect.
376
+ const linkerConfig = { accept_incoming: true };
377
+ try {
378
+ const ref = document.referrer && new URL(document.referrer).hostname;
379
+ if (ref && ref !== window.location.hostname) {
380
+ linkerConfig.domains = [ref, window.location.hostname];
381
+ }
382
+ }
383
+ catch { /* ignore invalid referrer */ }
384
+ win.gtag('config', containerId, { linker: linkerConfig });
305
385
  const script = document.createElement('script');
306
386
  script.async = true;
307
387
  script.src = 'https://www.googletagmanager.com/gtag/js?id=' + containerId;
@@ -310,5 +390,6 @@ function initGTM(containerId) {
310
390
  firstScript.parentNode.insertBefore(script, firstScript);
311
391
  else
312
392
  (document.head || document.getElementsByTagName('head')[0])?.appendChild(script);
393
+ return waitForScriptLoad(script);
313
394
  }
314
395
  }
@@ -150,6 +150,24 @@ export function usePostPurchasesQuery(options) {
150
150
  });
151
151
  // Fetch order summary with variant options
152
152
  await fetchOrderSummary(offerId, sessionId);
153
+ // Detect Whop payment method for this checkout session
154
+ try {
155
+ const paymentMethods = await postPurchasesResource.getPaymentMethods(sessionId);
156
+ const isWhop = paymentMethods.some((m) => m.type === 'whop');
157
+ if (isWhop) {
158
+ setCheckoutSessions(prev => ({
159
+ ...prev,
160
+ [offerId]: {
161
+ ...prev[offerId],
162
+ isWhop: true,
163
+ }
164
+ }));
165
+ }
166
+ }
167
+ catch (err) {
168
+ // Non-critical: if payment methods fetch fails, fall back to standard flow
169
+ console.warn(`[SDK] Failed to detect payment methods for offer ${offerId}:`, err);
170
+ }
153
171
  }
154
172
  finally {
155
173
  // Remove from initializing set
@@ -421,8 +439,22 @@ export function usePostPurchasesQuery(options) {
421
439
  if (!sessionState?.checkoutSessionId) {
422
440
  throw new Error('Checkout session not initialized for this offer');
423
441
  }
424
- // Use the enhanced payWithCheckoutSession with proper metadata
425
- await postPurchasesResource.payWithCheckoutSession(sessionState.checkoutSessionId, orderId);
442
+ if (sessionState.isWhop) {
443
+ // Whop post-purchase: charge using the stored payment method from the original order
444
+ const storeId = session?.storeId;
445
+ if (!storeId) {
446
+ throw new Error('Store ID not available for Whop payment');
447
+ }
448
+ await postPurchasesResource.chargeWhopPostPurchase({
449
+ checkoutSessionId: sessionState.checkoutSessionId,
450
+ orderId,
451
+ storeId,
452
+ });
453
+ }
454
+ else {
455
+ // Standard post-purchase: use the generic pay endpoint
456
+ await postPurchasesResource.payWithCheckoutSession(sessionState.checkoutSessionId, orderId);
457
+ }
426
458
  },
427
459
  };
428
460
  }
@@ -12,7 +12,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
12
12
  import { OffersResource } from '../../core/resources/offers';
13
13
  import { getGlobalApiClient } from './useApiQuery';
14
14
  export function usePreviewOffer(options) {
15
- const { offerId, currency: requestedCurrency = 'USD', initialSelections = {} } = options;
15
+ const { offerId, currency: requestedCurrency = '', initialSelections = {} } = options;
16
16
  const queryParamsCurrency = typeof window !== 'undefined'
17
17
  ? new URLSearchParams(window.location.search).get('currency') || undefined
18
18
  : undefined;
@@ -1,9 +1,6 @@
1
1
  /**
2
- * Hook to extract URL parameters that works with both remapped and non-remapped paths
3
- *
4
- * This hook automatically detects if the current path is remapped and extracts
5
- * parameters correctly in both cases, so your components don't need to know
6
- * about path remapping at all.
2
+ * Hook to extract URL parameters that works with both remapped and non-remapped paths.
3
+ * Router-agnostic: uses window.location + path-to-regexp directly (no react-router-dom needed).
7
4
  *
8
5
  * @param internalPath - The internal path pattern (e.g., "/hello-with-param/:myparam")
9
6
  * @returns Parameters object with extracted values
@@ -13,7 +10,6 @@
13
10
  * function HelloWithParamPage() {
14
11
  * // Works for both /hello-with-param/test AND /myremap/test
15
12
  * const { myparam } = useRemappableParams<{ myparam: string }>('/hello-with-param/:myparam');
16
- *
17
13
  * return <div>Parameter: {myparam}</div>;
18
14
  * }
19
15
  * ```
@@ -1,12 +1,22 @@
1
- import { useParams } from 'react-router-dom';
2
1
  import { getPathInfo } from '../../core/pathRemapping';
3
2
  import { match } from 'path-to-regexp';
4
3
  /**
5
- * Hook to extract URL parameters that works with both remapped and non-remapped paths
6
- *
7
- * This hook automatically detects if the current path is remapped and extracts
8
- * parameters correctly in both cases, so your components don't need to know
9
- * about path remapping at all.
4
+ * Extract params from a URL path using a pattern via path-to-regexp
5
+ */
6
+ function extractMatchParams(pattern, path) {
7
+ try {
8
+ const matchFn = match(pattern, { decode: decodeURIComponent });
9
+ const result = matchFn(path);
10
+ if (result && typeof result !== 'boolean') {
11
+ return result.params;
12
+ }
13
+ }
14
+ catch { /* ignore */ }
15
+ return null;
16
+ }
17
+ /**
18
+ * Hook to extract URL parameters that works with both remapped and non-remapped paths.
19
+ * Router-agnostic: uses window.location + path-to-regexp directly (no react-router-dom needed).
10
20
  *
11
21
  * @param internalPath - The internal path pattern (e.g., "/hello-with-param/:myparam")
12
22
  * @returns Parameters object with extracted values
@@ -16,21 +26,19 @@ import { match } from 'path-to-regexp';
16
26
  * function HelloWithParamPage() {
17
27
  * // Works for both /hello-with-param/test AND /myremap/test
18
28
  * const { myparam } = useRemappableParams<{ myparam: string }>('/hello-with-param/:myparam');
19
- *
20
29
  * return <div>Parameter: {myparam}</div>;
21
30
  * }
22
31
  * ```
23
32
  */
24
33
  export function useRemappableParams(internalPath) {
25
- const routerParams = useParams();
26
34
  const pathInfo = getPathInfo();
27
- // If not remapped, just return router params
35
+ const currentPath = typeof window !== 'undefined' ? window.location.pathname : '/';
36
+ // If not remapped, extract params from URL using the internal pattern directly
28
37
  if (!pathInfo.isRemapped) {
29
- return routerParams;
38
+ return extractMatchParams(internalPath, currentPath) ?? {};
30
39
  }
31
40
  // If remapped, extract params from the external URL
32
41
  try {
33
- // Get the external pattern from localStorage (for testing) or from meta tag
34
42
  let externalPattern = null;
35
43
  // Check localStorage for explicit remapping (development/testing)
36
44
  if (typeof localStorage !== 'undefined') {
@@ -54,17 +62,13 @@ export function useRemappableParams(internalPath) {
54
62
  externalPattern = metaTag.getAttribute('content');
55
63
  }
56
64
  }
57
- // If we have an external pattern, extract params from it
65
+ // If we have an external pattern, extract params and map to internal names
58
66
  if (externalPattern) {
59
67
  const matchFn = match(externalPattern, { decode: decodeURIComponent });
60
68
  const result = matchFn(pathInfo.externalPath);
61
69
  if (result && typeof result !== 'boolean') {
62
- // We have extracted params from external URL
63
- // Now we need to map them to internal param names
64
- // Extract param names from both patterns
65
70
  const externalParamNames = extractParamNames(externalPattern);
66
71
  const internalParamNames = extractParamNames(internalPath);
67
- // Map external params to internal params (by position)
68
72
  const mappedParams = {};
69
73
  externalParamNames.forEach((externalName, index) => {
70
74
  const internalName = internalParamNames[index];
@@ -76,17 +80,13 @@ export function useRemappableParams(internalPath) {
76
80
  }
77
81
  }
78
82
  // Fallback: try to extract from internal path directly
79
- const matchFn = match(internalPath, { decode: decodeURIComponent });
80
- const result = matchFn(pathInfo.externalPath);
81
- if (result && typeof result !== 'boolean') {
82
- return result.params;
83
- }
83
+ return extractMatchParams(internalPath, pathInfo.externalPath) ?? {};
84
84
  }
85
85
  catch (error) {
86
86
  console.error('[useRemappableParams] Failed to extract params:', error);
87
87
  }
88
- // Fallback to router params
89
- return routerParams;
88
+ // Final fallback: try matching current URL against internal pattern
89
+ return extractMatchParams(internalPath, currentPath) ?? {};
90
90
  }
91
91
  /**
92
92
  * Extract parameter names from a path pattern
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Hook for setting the payment method on a checkout session
3
+ */
4
+ import { CheckoutData } from '../../core/resources/checkout';
5
+ import type { PaymentMethodName } from '../../core/resources/checkout';
6
+ export interface UseSetPaymentMethodOptions {
7
+ sessionId?: string;
8
+ checkout?: CheckoutData;
9
+ onSuccess?: (paymentMethodName: PaymentMethodName) => void;
10
+ }
11
+ export interface UseSetPaymentMethodResult {
12
+ setPaymentMethod: (paymentMethodName: PaymentMethodName) => Promise<void>;
13
+ isPending: boolean;
14
+ error: Error | null;
15
+ }
16
+ export declare function useSetPaymentMethod(options?: UseSetPaymentMethodOptions): UseSetPaymentMethodResult;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Hook for setting the payment method on a checkout session
3
+ */
4
+ import { useMutation, useQueryClient } from '@tanstack/react-query';
5
+ import { useMemo } from 'react';
6
+ import { CheckoutResource } from '../../core/resources/checkout';
7
+ import { getGlobalApiClient } from './useApiQuery';
8
+ export function useSetPaymentMethod(options = {}) {
9
+ const { sessionId, checkout, onSuccess } = options;
10
+ const queryClient = useQueryClient();
11
+ const checkoutResource = useMemo(() => new CheckoutResource(getGlobalApiClient()), []);
12
+ const effectiveSessionId = sessionId ?? checkout?.checkoutSession?.id;
13
+ const mutation = useMutation({
14
+ mutationFn: (paymentMethodName) => {
15
+ if (!effectiveSessionId) {
16
+ throw new Error('No session ID available');
17
+ }
18
+ return checkoutResource.setPaymentMethod(effectiveSessionId, paymentMethodName);
19
+ },
20
+ onSuccess: async (_, paymentMethodName) => {
21
+ await queryClient.invalidateQueries({ queryKey: ['checkout'] });
22
+ onSuccess?.(paymentMethodName);
23
+ },
24
+ });
25
+ const setPaymentMethod = async (paymentMethodName) => {
26
+ await mutation.mutateAsync(paymentMethodName);
27
+ };
28
+ return {
29
+ setPaymentMethod,
30
+ isPending: mutation.isPending,
31
+ error: mutation.error,
32
+ };
33
+ }
@@ -10,18 +10,18 @@
10
10
  *
11
11
  * Usage:
12
12
  * ```tsx
13
- * const { stepConfig, paymentFlowId, staticResources } = useStepConfig();
13
+ * const { stepConfig, paymentFlowId, resources } = useStepConfig();
14
14
  *
15
15
  * // Access payment flow override
16
16
  * if (paymentFlowId) {
17
17
  * console.log('Using custom payment flow:', paymentFlowId);
18
18
  * }
19
19
  *
20
- * // Access static resources (e.g., offer ID for current A/B variant)
21
- * const offerId = staticResources?.offer;
20
+ * // Access resource bindings (e.g., offer ID for current A/B variant)
21
+ * const offerId = resources?.offer;
22
22
  * ```
23
23
  */
24
- import { PixelsConfig, RuntimeStepConfig } from '../../core/funnelClient';
24
+ import { type PaymentSetupConfig, type PixelsConfig, type RuntimeStepConfig } from '../../core/funnelClient';
25
25
  export interface UseStepConfigResult {
26
26
  /**
27
27
  * Full step configuration object
@@ -34,10 +34,11 @@ export interface UseStepConfigResult {
34
34
  */
35
35
  paymentFlowId: string | undefined;
36
36
  /**
37
- * Static resources assigned to this step/variant
38
- * For A/B tests, this contains the resources for the specific variant the user landed on
37
+ * Resource bindings for this step/variant.
39
38
  * e.g., { offer: 'offer_xxx', product: 'product_xxx' }
40
39
  */
40
+ resources: Record<string, string> | undefined;
41
+ /** @deprecated Use `resources` instead */
41
42
  staticResources: Record<string, string> | undefined;
42
43
  /**
43
44
  * Get scripts for a specific injection position
@@ -46,6 +47,22 @@ export interface UseStepConfigResult {
46
47
  */
47
48
  getScripts: (position?: 'head-start' | 'head-end' | 'body-start' | 'body-end') => RuntimeStepConfig['scripts'];
48
49
  pixels: PixelsConfig | undefined;
50
+ /**
51
+ * Payment setup configuration for this step.
52
+ * Keys: "{method}" or "{method}:{provider}" (e.g. "card", "apple_pay:stripe")
53
+ * Use helpers: getEnabledMethods(), getExpressMethods(), findMethod()
54
+ */
55
+ paymentSetupConfig: PaymentSetupConfig | undefined;
56
+ /**
57
+ * Enabled order bump offer IDs for this step.
58
+ * undefined = inherit all store bumps, string[] = only these IDs.
59
+ */
60
+ orderBumpOfferIds: string[] | undefined;
61
+ /**
62
+ * Enabled upsell offer IDs for this step.
63
+ * undefined = inherit all store upsells, string[] = only these IDs.
64
+ */
65
+ upsellOfferIds: string[] | undefined;
49
66
  }
50
67
  /**
51
68
  * Hook to access runtime step configuration injected via HTML
@@ -10,19 +10,19 @@
10
10
  *
11
11
  * Usage:
12
12
  * ```tsx
13
- * const { stepConfig, paymentFlowId, staticResources } = useStepConfig();
13
+ * const { stepConfig, paymentFlowId, resources } = useStepConfig();
14
14
  *
15
15
  * // Access payment flow override
16
16
  * if (paymentFlowId) {
17
17
  * console.log('Using custom payment flow:', paymentFlowId);
18
18
  * }
19
19
  *
20
- * // Access static resources (e.g., offer ID for current A/B variant)
21
- * const offerId = staticResources?.offer;
20
+ * // Access resource bindings (e.g., offer ID for current A/B variant)
21
+ * const offerId = resources?.offer;
22
22
  * ```
23
23
  */
24
24
  import { useMemo } from 'react';
25
- import { getAssignedPaymentFlowId, getAssignedPixels, getAssignedScripts, getAssignedStaticResources, getAssignedStepConfig, } from '../../core/funnelClient';
25
+ import { getAssignedOrderBumpOfferIds, getAssignedPaymentFlowId, getAssignedPixels, getAssignedResources, getAssignedScripts, getAssignedStepConfig, getAssignedUpsellOfferIds, } from '../../core/funnelClient';
26
26
  /**
27
27
  * Hook to access runtime step configuration injected via HTML
28
28
  *
@@ -35,7 +35,7 @@ export function useStepConfig() {
35
35
  // Read once on mount - these values don't change during the page lifecycle
36
36
  const stepConfig = useMemo(() => getAssignedStepConfig(), []);
37
37
  const paymentFlowId = useMemo(() => getAssignedPaymentFlowId(), []);
38
- const staticResources = useMemo(() => getAssignedStaticResources(), []);
38
+ const resources = useMemo(() => getAssignedResources(), []);
39
39
  const pixels = useMemo(() => getAssignedPixels(), []);
40
40
  // Create a stable reference for getScripts
41
41
  const getScripts = useMemo(() => {
@@ -43,11 +43,18 @@ export function useStepConfig() {
43
43
  return getAssignedScripts(position);
44
44
  };
45
45
  }, []);
46
+ const paymentSetupConfig = useMemo(() => stepConfig?.paymentSetupConfig, [stepConfig]);
47
+ const orderBumpOfferIds = useMemo(() => getAssignedOrderBumpOfferIds(), []);
48
+ const upsellOfferIds = useMemo(() => getAssignedUpsellOfferIds(), []);
46
49
  return {
47
50
  stepConfig,
48
51
  paymentFlowId,
49
- staticResources,
52
+ resources,
53
+ staticResources: resources,
50
54
  getScripts,
51
- pixels
55
+ pixels,
56
+ paymentSetupConfig,
57
+ orderBumpOfferIds,
58
+ upsellOfferIds,
52
59
  };
53
60
  }