@tagadapay/plugin-sdk 3.1.2 → 3.1.8

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 (56) hide show
  1. package/README.md +1129 -1129
  2. package/build-cdn.js +113 -113
  3. package/dist/external-tracker.js +1104 -491
  4. package/dist/external-tracker.min.js +2 -2
  5. package/dist/external-tracker.min.js.map +4 -4
  6. package/dist/react/hooks/useApplePay.js +25 -36
  7. package/dist/react/hooks/usePaymentPolling.d.ts +9 -3
  8. package/dist/react/providers/TagadaProvider.js +5 -5
  9. package/dist/react/utils/money.d.ts +4 -3
  10. package/dist/react/utils/money.js +39 -6
  11. package/dist/react/utils/trackingUtils.js +1 -0
  12. package/dist/v2/core/client.js +34 -2
  13. package/dist/v2/core/config/environment.js +9 -2
  14. package/dist/v2/core/funnelClient.d.ts +92 -1
  15. package/dist/v2/core/funnelClient.js +247 -3
  16. package/dist/v2/core/resources/apiClient.js +1 -1
  17. package/dist/v2/core/resources/checkout.d.ts +68 -0
  18. package/dist/v2/core/resources/funnel.d.ts +15 -0
  19. package/dist/v2/core/resources/payments.d.ts +50 -3
  20. package/dist/v2/core/resources/payments.js +38 -7
  21. package/dist/v2/core/utils/pluginConfig.js +40 -5
  22. package/dist/v2/core/utils/previewMode.d.ts +3 -0
  23. package/dist/v2/core/utils/previewMode.js +44 -14
  24. package/dist/v2/core/utils/previewModeIndicator.d.ts +19 -0
  25. package/dist/v2/core/utils/previewModeIndicator.js +414 -0
  26. package/dist/v2/core/utils/tokenStorage.d.ts +4 -0
  27. package/dist/v2/core/utils/tokenStorage.js +15 -1
  28. package/dist/v2/index.d.ts +6 -1
  29. package/dist/v2/index.js +6 -1
  30. package/dist/v2/react/components/ApplePayButton.d.ts +21 -121
  31. package/dist/v2/react/components/ApplePayButton.js +221 -290
  32. package/dist/v2/react/components/FunnelScriptInjector.d.ts +3 -1
  33. package/dist/v2/react/components/FunnelScriptInjector.js +128 -24
  34. package/dist/v2/react/components/PreviewModeIndicator.d.ts +46 -0
  35. package/dist/v2/react/components/PreviewModeIndicator.js +113 -0
  36. package/dist/v2/react/hooks/useApplePayCheckout.d.ts +16 -0
  37. package/dist/v2/react/hooks/useApplePayCheckout.js +193 -0
  38. package/dist/v2/react/hooks/useFunnel.d.ts +42 -6
  39. package/dist/v2/react/hooks/useFunnel.js +25 -5
  40. package/dist/v2/react/hooks/usePaymentPolling.d.ts +9 -3
  41. package/dist/v2/react/hooks/usePaymentPolling.js +31 -9
  42. package/dist/v2/react/hooks/usePaymentQuery.d.ts +32 -2
  43. package/dist/v2/react/hooks/usePaymentQuery.js +304 -7
  44. package/dist/v2/react/hooks/usePaymentRetrieve.d.ts +26 -0
  45. package/dist/v2/react/hooks/usePaymentRetrieve.js +175 -0
  46. package/dist/v2/react/hooks/useStepConfig.d.ts +62 -0
  47. package/dist/v2/react/hooks/useStepConfig.js +52 -0
  48. package/dist/v2/react/index.d.ts +9 -3
  49. package/dist/v2/react/index.js +5 -1
  50. package/dist/v2/react/providers/ExpressPaymentMethodsProvider.js +27 -19
  51. package/dist/v2/react/providers/TagadaProvider.js +7 -7
  52. package/dist/v2/standalone/external-tracker.d.ts +2 -0
  53. package/dist/v2/standalone/external-tracker.js +6 -3
  54. package/package.json +112 -112
  55. package/dist/v2/react/hooks/useApplePay.d.ts +0 -16
  56. package/dist/v2/react/hooks/useApplePay.js +0 -247
@@ -82,10 +82,16 @@ function getFromCookie(key) {
82
82
  }
83
83
  /**
84
84
  * Set value in cookie
85
+ * ⚠️ NEVER allows tgd_draft to be set as a cookie (localStorage-only)
85
86
  */
86
87
  function setInCookie(key, value, maxAge = 86400) {
87
88
  if (typeof document === 'undefined')
88
89
  return;
90
+ // 🛡️ SAFETY: Block tgd_draft from EVER being written to cookies
91
+ if (key === 'tgd_draft' || key === 'tgd-draft') {
92
+ console.warn(`🛡️ [SDK] Blocked attempt to set ${key} as cookie - this should only be in localStorage`);
93
+ return;
94
+ }
89
95
  document.cookie = `${key}=${value}; path=/; max-age=${maxAge}`;
90
96
  }
91
97
  /**
@@ -105,14 +111,14 @@ export function getSDKParams() {
105
111
  return {};
106
112
  }
107
113
  const urlParams = new URLSearchParams(window.location.search);
108
- // Get draft mode (URL > localStorage > cookie)
114
+ // Get draft mode (URL > localStorage only - no cookie fallback to avoid resurrection)
109
115
  let draft;
110
116
  const urlDraft = urlParams.get('draft');
111
117
  if (urlDraft !== null) {
112
118
  draft = urlDraft === 'true';
113
119
  }
114
120
  else {
115
- const storageDraft = getFromStorage(STORAGE_KEYS.DRAFT) || getFromCookie(STORAGE_KEYS.DRAFT);
121
+ const storageDraft = getFromStorage(STORAGE_KEYS.DRAFT);
116
122
  if (storageDraft !== null) {
117
123
  draft = storageDraft === 'true';
118
124
  }
@@ -135,6 +141,12 @@ export function getSDKParams() {
135
141
  const funnelSessionId = urlParams.get('funnelSessionId') || null;
136
142
  // Get funnel ID (URL only)
137
143
  const funnelId = urlParams.get('funnelId') || null;
144
+ // Get funnel environment (URL only - not persisted, set on entry)
145
+ let funnelEnv;
146
+ const urlFunnelEnv = urlParams.get('funnelEnv');
147
+ if (urlFunnelEnv && (urlFunnelEnv === 'staging' || urlFunnelEnv === 'production')) {
148
+ funnelEnv = urlFunnelEnv;
149
+ }
138
150
  // Force reset is URL-only (not persisted)
139
151
  const forceReset = urlParams.get('forceReset') === 'true';
140
152
  // Get client environment override (URL > localStorage > cookie)
@@ -168,6 +180,7 @@ export function getSDKParams() {
168
180
  funnelId,
169
181
  draft,
170
182
  funnelTracking,
183
+ funnelEnv,
171
184
  tagadaClientEnv,
172
185
  tagadaClientBaseUrl,
173
186
  };
@@ -189,17 +202,21 @@ export function isDraftMode() {
189
202
  }
190
203
  /**
191
204
  * Set draft mode in storage for persistence
205
+ * ⚠️ ONLY writes to localStorage (not cookies) to avoid resurrection issues
192
206
  */
193
207
  export function setDraftMode(draft) {
194
208
  if (typeof window === 'undefined')
195
209
  return;
196
210
  if (draft) {
197
211
  setInStorage(STORAGE_KEYS.DRAFT, 'true');
198
- setInCookie(STORAGE_KEYS.DRAFT, 'true', 86400); // 24 hours
199
212
  }
200
213
  else {
201
- setInStorage(STORAGE_KEYS.DRAFT, 'false');
202
- clearFromCookie(STORAGE_KEYS.DRAFT);
214
+ try {
215
+ localStorage.removeItem(STORAGE_KEYS.DRAFT);
216
+ }
217
+ catch {
218
+ // Storage not available
219
+ }
203
220
  }
204
221
  }
205
222
  /**
@@ -214,6 +231,13 @@ export function setDraftMode(draft) {
214
231
  * @returns True if force reset was activated and state was cleared
215
232
  */
216
233
  export function handlePreviewMode(debugMode = false) {
234
+ // 🔒 CRITICAL: Read URL token DIRECTLY first before any getSDKParams()
235
+ // This ensures we have the URL token before any clearing happens
236
+ let urlToken = null;
237
+ if (typeof window !== 'undefined') {
238
+ const urlParams = new URLSearchParams(window.location.search);
239
+ urlToken = urlParams.get('token');
240
+ }
217
241
  const params = getSDKParams();
218
242
  const shouldReset = params.forceReset || false;
219
243
  if (!shouldReset && !params.token) {
@@ -224,6 +248,7 @@ export function handlePreviewMode(debugMode = false) {
224
248
  }
225
249
  if (debugMode) {
226
250
  console.log('[SDK] Detected params:', params);
251
+ console.log('[SDK] URL token (direct read):', urlToken ? urlToken.substring(0, 20) + '...' : 'none');
227
252
  }
228
253
  // CASE 1: Force reset - clear all state (simulates hard refresh)
229
254
  if (shouldReset) {
@@ -246,23 +271,28 @@ export function handlePreviewMode(debugMode = false) {
246
271
  }
247
272
  }
248
273
  // CASE 2: Token in URL - override stored token
249
- if (params.token !== null && params.token !== undefined) {
274
+ // 🔒 CRITICAL: Use the directly-read URL token to avoid any race conditions
275
+ const tokenToSet = urlToken || params.token;
276
+ if (tokenToSet !== null && tokenToSet !== undefined) {
250
277
  if (debugMode) {
251
- console.log('[SDK] Using token from URL:', params.token.substring(0, 20) + '...');
278
+ console.log('[SDK] Setting token from URL:', tokenToSet.substring(0, 20) + '...');
252
279
  }
253
- if (params.token === '' || params.token === 'null') {
280
+ if (tokenToSet === '' || tokenToSet === 'null') {
254
281
  // Explicitly cleared token
255
282
  clearClientToken();
256
283
  }
257
284
  else {
258
- // Set token from URL
259
- setClientToken(params.token);
285
+ // Set token from URL IMMEDIATELY after clearing
286
+ setClientToken(tokenToSet);
287
+ if (debugMode) {
288
+ console.log('[SDK] ✅ Token set in localStorage immediately after clear');
289
+ }
260
290
  }
261
291
  }
262
292
  else if (shouldReset) {
263
293
  // Force reset but no token = clear stored token
264
294
  if (debugMode) {
265
- console.log('[SDK] Force reset mode (no token)');
295
+ console.log('[SDK] Force reset mode (no token in URL)');
266
296
  }
267
297
  clearClientToken();
268
298
  }
@@ -311,7 +341,7 @@ export function setFunnelTracking(enabled) {
311
341
  return;
312
342
  const value = enabled ? 'true' : 'false';
313
343
  setInStorage(STORAGE_KEYS.FUNNEL_TRACKING, value);
314
- setInCookie(STORAGE_KEYS.FUNNEL_TRACKING, value, 86400); // 24 hours
344
+ setInCookie(STORAGE_KEYS.FUNNEL_TRACKING, value, 86400);
315
345
  }
316
346
  /**
317
347
  * Set client environment override in storage for persistence
@@ -320,7 +350,7 @@ export function setClientEnvironment(env) {
320
350
  if (typeof window === 'undefined')
321
351
  return;
322
352
  setInStorage(STORAGE_KEYS.CLIENT_ENV, env);
323
- setInCookie(STORAGE_KEYS.CLIENT_ENV, env, 86400); // 24 hours
353
+ setInCookie(STORAGE_KEYS.CLIENT_ENV, env, 86400);
324
354
  }
325
355
  /**
326
356
  * Clear client environment override
@@ -343,7 +373,7 @@ export function setClientBaseUrl(baseUrl) {
343
373
  if (typeof window === 'undefined')
344
374
  return;
345
375
  setInStorage(STORAGE_KEYS.CLIENT_BASE_URL, baseUrl);
346
- setInCookie(STORAGE_KEYS.CLIENT_BASE_URL, baseUrl, 86400); // 24 hours
376
+ setInCookie(STORAGE_KEYS.CLIENT_BASE_URL, baseUrl, 86400);
347
377
  }
348
378
  /**
349
379
  * Clear custom API base URL override
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Preview Mode Indicator - Auto-injected DOM element
3
+ *
4
+ * Automatically injects a visual indicator when SDK is in preview/draft mode
5
+ * Works with vanilla JS/DOM - no React required
6
+ */
7
+ /**
8
+ * Inject the preview mode indicator into the DOM
9
+ * Called automatically during SDK initialization if preview mode is active
10
+ */
11
+ export declare function injectPreviewModeIndicator(): void;
12
+ /**
13
+ * Remove the preview mode indicator from the DOM
14
+ */
15
+ export declare function removePreviewModeIndicator(): void;
16
+ /**
17
+ * Check if indicator is currently injected
18
+ */
19
+ export declare function isIndicatorInjected(): boolean;
@@ -0,0 +1,414 @@
1
+ /**
2
+ * Preview Mode Indicator - Auto-injected DOM element
3
+ *
4
+ * Automatically injects a visual indicator when SDK is in preview/draft mode
5
+ * Works with vanilla JS/DOM - no React required
6
+ */
7
+ import { isDraftMode, isFunnelTrackingEnabled, getSDKParams, clearClientEnvironment, clearClientBaseUrl } from './previewMode';
8
+ import { clearClientToken } from './tokenStorage';
9
+ import { clearFunnelSessionCookie } from './sessionStorage';
10
+ let indicatorElement = null;
11
+ let isInjected = false;
12
+ /**
13
+ * Helper to clear a specific cookie with all possible domain/path combinations
14
+ */
15
+ function clearSpecificCookie(cookieName) {
16
+ if (typeof window === 'undefined' || typeof document === 'undefined')
17
+ return;
18
+ const hostname = window.location.hostname;
19
+ const parts = hostname.split('.');
20
+ // All possible domains
21
+ const domains = [
22
+ '', // No domain (current)
23
+ hostname, // Full hostname
24
+ '.' + hostname, // Wildcard current
25
+ ];
26
+ // Add parent domains
27
+ for (let i = 1; i < parts.length; i++) {
28
+ const domain = parts.slice(i).join('.');
29
+ domains.push(domain);
30
+ domains.push('.' + domain);
31
+ }
32
+ // All possible paths
33
+ const pathParts = window.location.pathname.split('/').filter(p => p);
34
+ const paths = ['/'];
35
+ let currentPath = '';
36
+ pathParts.forEach(part => {
37
+ currentPath += '/' + part;
38
+ paths.push(currentPath);
39
+ });
40
+ // Try all combinations
41
+ domains.forEach(domain => {
42
+ paths.forEach(path => {
43
+ const baseDelete = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=${path}`;
44
+ const domainPart = domain ? `; domain=${domain}` : '';
45
+ // Try all security flag variations
46
+ document.cookie = baseDelete + domainPart;
47
+ document.cookie = baseDelete + domainPart + '; secure';
48
+ document.cookie = baseDelete + domainPart + '; SameSite=None; secure';
49
+ document.cookie = baseDelete + domainPart + '; SameSite=Lax';
50
+ document.cookie = baseDelete + domainPart + '; SameSite=Strict';
51
+ });
52
+ });
53
+ }
54
+ /**
55
+ * Leave preview mode - Clear ALL storage (including Shopify cookies, all localStorage, etc.) and reload
56
+ * ⚠️ Nuclear option - clears everything to exit preview mode completely
57
+ */
58
+ function leavePreviewMode() {
59
+ if (typeof window === 'undefined')
60
+ return;
61
+ const confirmed = confirm('🚪 Leave Preview Mode?\n\nThis will clear ALL cookies and localStorage (including Shopify session, cart, and preview settings) and reload the page.\n\nAre you sure?');
62
+ if (!confirmed)
63
+ return;
64
+ // STEP 0: Remove ALL preview params from URL immediately
65
+ const url = new URL(window.location.href);
66
+ const previewParams = [
67
+ 'draft', 'funnelEnv', 'funnelTracking', 'tagadaClientEnv',
68
+ 'tagadaClientBaseUrl', 'forceReset', 'funnelId', 'funnelSessionId', 'token',
69
+ ];
70
+ previewParams.forEach(param => url.searchParams.delete(param));
71
+ window.history.replaceState({}, '', url.href);
72
+ // STEP 0.5: Install global cookie blocker for tgd_draft
73
+ try {
74
+ const cookieDescriptor = Object.getOwnPropertyDescriptor(Document.prototype, 'cookie') ||
75
+ Object.getOwnPropertyDescriptor(HTMLDocument.prototype, 'cookie');
76
+ if (cookieDescriptor && cookieDescriptor.set) {
77
+ Object.defineProperty(document, 'cookie', {
78
+ configurable: true,
79
+ enumerable: true,
80
+ get: function () { return cookieDescriptor.get ? cookieDescriptor.get.call(this) : ''; },
81
+ set: function (val) {
82
+ if (typeof val === 'string') {
83
+ const normalizedVal = val.toLowerCase();
84
+ if (normalizedVal.includes('tgd_draft') ||
85
+ normalizedVal.includes('tgd-draft') ||
86
+ normalizedVal.includes('tgd.draft') ||
87
+ normalizedVal.includes('tgddraft')) {
88
+ // Allow deletion attempts
89
+ if (normalizedVal.includes('max-age=0') ||
90
+ normalizedVal.includes('expires=thu, 01 jan 1970')) {
91
+ if (cookieDescriptor.set) {
92
+ cookieDescriptor.set.call(this, val);
93
+ }
94
+ return;
95
+ }
96
+ // Block all creation/update attempts
97
+ console.warn('🛡️ [TagadaPay] BLOCKED: tgd_draft should never be a cookie');
98
+ return;
99
+ }
100
+ }
101
+ if (cookieDescriptor.set) {
102
+ cookieDescriptor.set.call(this, val);
103
+ }
104
+ }
105
+ });
106
+ }
107
+ }
108
+ catch (e) {
109
+ console.warn('[TagadaPay] Could not install cookie blocker:', e);
110
+ }
111
+ // STEP 1: Clear TagadaPay localStorage keys
112
+ if (window.localStorage) {
113
+ try {
114
+ localStorage.removeItem('tgd_draft');
115
+ localStorage.removeItem('tgd_funnel_tracking');
116
+ localStorage.removeItem('tgd_client_env');
117
+ localStorage.removeItem('tgd_client_base_url');
118
+ localStorage.removeItem('cms_token');
119
+ }
120
+ catch (e) {
121
+ console.warn('[TagadaPay] Failed to clear some localStorage keys:', e);
122
+ }
123
+ }
124
+ // Clear SDK utilities
125
+ clearClientToken();
126
+ clearFunnelSessionCookie();
127
+ clearClientEnvironment();
128
+ clearClientBaseUrl();
129
+ // STEP 2: Clear ALL storage
130
+ if (window.localStorage) {
131
+ localStorage.clear();
132
+ }
133
+ if (window.sessionStorage) {
134
+ sessionStorage.clear();
135
+ }
136
+ // STEP 3: Clear known TagadaPay cookies
137
+ const tagadaCookies = [
138
+ 'tgd-funnel-session-id', 'tgd_draft', 'tgd_funnel_tracking',
139
+ 'tgd_client_env', 'tgd_client_base_url', 'cms_token', 'tagadapay_session',
140
+ ];
141
+ tagadaCookies.forEach(cookieName => clearSpecificCookie(cookieName));
142
+ // STEP 3.5: Clear alternative cookie name formats
143
+ const alternativeNames = ['tgd-draft', 'tgd_draft', 'tgd.draft', 'tgddraft'];
144
+ alternativeNames.forEach(cookieName => clearSpecificCookie(cookieName));
145
+ // STEP 4: Clear ALL remaining cookies aggressively
146
+ if (document.cookie) {
147
+ const cookies = document.cookie.split(';');
148
+ cookies.forEach(cookie => {
149
+ const cookieName = cookie.split('=')[0].trim();
150
+ if (!cookieName)
151
+ return;
152
+ const hostname = window.location.hostname;
153
+ const parts = hostname.split('.');
154
+ const domains = ['', hostname, '.' + hostname];
155
+ for (let i = 1; i < parts.length; i++) {
156
+ const domain = parts.slice(i).join('.');
157
+ domains.push(domain);
158
+ domains.push('.' + domain);
159
+ }
160
+ const pathParts = window.location.pathname.split('/').filter(p => p);
161
+ const paths = ['/'];
162
+ let currentPath = '';
163
+ pathParts.forEach(part => {
164
+ currentPath += '/' + part;
165
+ paths.push(currentPath);
166
+ });
167
+ domains.forEach(domain => {
168
+ paths.forEach(path => {
169
+ const baseDelete = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=${path}`;
170
+ const domainPart = domain ? `; domain=${domain}` : '';
171
+ document.cookie = baseDelete + domainPart;
172
+ document.cookie = baseDelete + domainPart + '; secure';
173
+ document.cookie = baseDelete + domainPart + '; SameSite=None; secure';
174
+ document.cookie = baseDelete + domainPart + '; SameSite=Lax';
175
+ document.cookie = baseDelete + domainPart + '; SameSite=Strict';
176
+ document.cookie = baseDelete + domainPart + '; secure; SameSite=None';
177
+ document.cookie = baseDelete + domainPart + '; secure; SameSite=Lax';
178
+ document.cookie = baseDelete + domainPart + '; secure; SameSite=Strict';
179
+ });
180
+ });
181
+ });
182
+ }
183
+ // Final cleanup and reload
184
+ setTimeout(() => {
185
+ if (window.localStorage && localStorage.length > 0) {
186
+ localStorage.clear();
187
+ }
188
+ clearSpecificCookie('tgd_draft');
189
+ if (typeof window.stop === 'function') {
190
+ window.stop();
191
+ }
192
+ window.location.replace(url.href);
193
+ }, 10);
194
+ }
195
+ /**
196
+ * Inject the preview mode indicator into the DOM
197
+ * Called automatically during SDK initialization if preview mode is active
198
+ */
199
+ export function injectPreviewModeIndicator() {
200
+ // Skip if already injected or not in browser
201
+ if (isInjected || typeof window === 'undefined' || typeof document === 'undefined') {
202
+ return;
203
+ }
204
+ const params = getSDKParams();
205
+ const draftMode = isDraftMode();
206
+ const trackingDisabled = !isFunnelTrackingEnabled();
207
+ const hasCustomEnv = !!(params.tagadaClientEnv || params.tagadaClientBaseUrl);
208
+ // Only inject if in preview/dev mode
209
+ if (!draftMode && !trackingDisabled && !hasCustomEnv) {
210
+ return;
211
+ }
212
+ // Create container
213
+ const container = document.createElement('div');
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;
221
+ `;
222
+ // Create badge
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;
237
+ `;
238
+ badge.innerHTML = `
239
+ <span style="font-size: 16px;">🔍</span>
240
+ <span>${draftMode ? 'Preview Mode' : 'Dev Mode'}</span>
241
+ `;
242
+ // Create details popup (with padding-top to bridge gap with badge)
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;
257
+ `;
258
+ details.style.paddingTop = '20px'; // Extra padding to bridge the gap
259
+ // Add invisible bridge between badge and popup to prevent flickering
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;
268
+ `;
269
+ // Build details content
270
+ let detailsHTML = '<div style="margin-bottom: 8px; font-weight: 600; color: #1d1d1f;">Current Environment</div>';
271
+ detailsHTML += '<div style="display: flex; flex-direction: column; gap: 6px;">';
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>
278
+ `;
279
+ }
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>
286
+ `;
287
+ }
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>
296
+ `;
297
+ }
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>
306
+ `;
307
+ }
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>
316
+ `;
317
+ }
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>
326
+ `;
327
+ }
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>
333
+ `;
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>
352
+ `;
353
+ details.innerHTML = detailsHTML;
354
+ // Hover behavior - keep popup visible when hovering over badge, bridge, or popup
355
+ let isHovering = false;
356
+ const showDetails = () => {
357
+ isHovering = true;
358
+ details.style.display = 'block';
359
+ bridge.style.display = 'block';
360
+ };
361
+ const hideDetails = () => {
362
+ isHovering = false;
363
+ setTimeout(() => {
364
+ if (!isHovering) {
365
+ details.style.display = 'none';
366
+ bridge.style.display = 'none';
367
+ }
368
+ }, 100); // Small delay to allow moving between elements
369
+ };
370
+ badge.addEventListener('mouseenter', showDetails);
371
+ badge.addEventListener('mouseleave', hideDetails);
372
+ bridge.addEventListener('mouseenter', showDetails);
373
+ bridge.addEventListener('mouseleave', hideDetails);
374
+ details.addEventListener('mouseenter', showDetails);
375
+ details.addEventListener('mouseleave', hideDetails);
376
+ // Button: Leave preview mode
377
+ const leavePreviewBtn = details.querySelector('#tgd-leave-preview');
378
+ if (leavePreviewBtn) {
379
+ leavePreviewBtn.addEventListener('mouseenter', () => {
380
+ leavePreviewBtn.style.opacity = '0.8';
381
+ });
382
+ leavePreviewBtn.addEventListener('mouseleave', () => {
383
+ leavePreviewBtn.style.opacity = '1';
384
+ });
385
+ leavePreviewBtn.addEventListener('click', (e) => {
386
+ e.stopPropagation();
387
+ leavePreviewMode();
388
+ });
389
+ }
390
+ // Assemble
391
+ container.appendChild(badge);
392
+ container.appendChild(bridge); // Add invisible bridge
393
+ container.appendChild(details);
394
+ // Inject into DOM
395
+ document.body.appendChild(container);
396
+ indicatorElement = container;
397
+ isInjected = true;
398
+ }
399
+ /**
400
+ * Remove the preview mode indicator from the DOM
401
+ */
402
+ export function removePreviewModeIndicator() {
403
+ if (indicatorElement && indicatorElement.parentNode) {
404
+ indicatorElement.parentNode.removeChild(indicatorElement);
405
+ indicatorElement = null;
406
+ isInjected = false;
407
+ }
408
+ }
409
+ /**
410
+ * Check if indicator is currently injected
411
+ */
412
+ export function isIndicatorInjected() {
413
+ return isInjected;
414
+ }
@@ -3,6 +3,10 @@
3
3
  */
4
4
  /**
5
5
  * Set the CMS token in localStorage
6
+ *
7
+ * IMPORTANT: Browser's native 'storage' event ONLY fires for changes in OTHER tabs/windows.
8
+ * For same-tab updates to trigger storage listeners, we must manually dispatch the event.
9
+ * We only dispatch if the token actually changed to minimize unnecessary events.
6
10
  */
7
11
  export declare function setClientToken(token: string): void;
8
12
  /**
@@ -4,12 +4,26 @@
4
4
  const TOKEN_KEY = 'cms_token';
5
5
  /**
6
6
  * Set the CMS token in localStorage
7
+ *
8
+ * IMPORTANT: Browser's native 'storage' event ONLY fires for changes in OTHER tabs/windows.
9
+ * For same-tab updates to trigger storage listeners, we must manually dispatch the event.
10
+ * We only dispatch if the token actually changed to minimize unnecessary events.
7
11
  */
8
12
  export function setClientToken(token) {
9
13
  if (typeof window !== 'undefined') {
10
14
  try {
15
+ // Check if token actually changed to avoid unnecessary events and potential loops
16
+ const currentToken = localStorage.getItem(TOKEN_KEY);
17
+ const tokenChanged = currentToken !== token;
11
18
  localStorage.setItem(TOKEN_KEY, token);
12
- window.dispatchEvent(new Event('storage'));
19
+ // Only dispatch if token actually changed
20
+ // The storage event listeners have guards to prevent infinite loops:
21
+ // - They check if token hasn't changed (client.ts line 261)
22
+ // - They check if initialization is in progress (client.ts line 269)
23
+ // - They check retry limits (client.ts line 277)
24
+ if (tokenChanged) {
25
+ window.dispatchEvent(new Event('storage'));
26
+ }
13
27
  }
14
28
  catch (error) {
15
29
  console.error('Failed to save token to localStorage:', error);
@@ -12,6 +12,9 @@ export * from './core/utils/pluginConfig';
12
12
  export * from './core/utils/products';
13
13
  export * from './core/utils/previewMode';
14
14
  export * from './core/utils/configHotReload';
15
+ export { injectPreviewModeIndicator, removePreviewModeIndicator, isIndicatorInjected } from './core/utils/previewModeIndicator';
16
+ export { getAssignedStepConfig, getAssignedPaymentFlowId, getAssignedStaticResources, getAssignedScripts, loadLocalFunnelConfig, getLocalFunnelConfig, } from './core/funnelClient';
17
+ export type { RuntimeStepConfig, LocalFunnelConfig } from './core/funnelClient';
15
18
  export * from './core/pathRemapping';
16
19
  export type { CheckoutData, CheckoutInitParams, CheckoutLineItem, CheckoutSession, CheckoutSessionPreview, Promotion } from './core/resources/checkout';
17
20
  export type { Order, OrderLineItem } from './core/utils/order';
@@ -25,10 +28,11 @@ export type { ToggleOrderBumpResponse, VipOffer, VipPreviewResponse } from './co
25
28
  export type { StoreConfig } from './core/resources/storeConfig';
26
29
  export { FunnelActionType } from './core/resources/funnel';
27
30
  export type { BackNavigationActionData, CartUpdatedActionData, DirectNavigationActionData, FormSubmitActionData, FunnelContextUpdateRequest, FunnelContextUpdateResponse, FunnelAction as FunnelEvent, FunnelInitializeRequest, FunnelInitializeResponse, FunnelNavigateRequest, FunnelNavigateResponse, FunnelNavigationAction, FunnelNavigationResult, NextAction, OfferAcceptedActionData, OfferDeclinedActionData, PaymentFailedActionData, PaymentSuccessActionData, SimpleFunnelContext } from './core/resources/funnel';
28
- export { ApplePayButton, ExpressPaymentMethodsProvider, formatMoney, getAvailableLanguages, GooglePayButton, queryKeys, TagadaProvider, useApiMutation, useApiQuery, useAuth, useCheckout, useCheckoutToken, useClubOffers, useCountryOptions, useCredits, useCurrency, useCustomer, useCustomerInfos, useCustomerOrders, useCustomerSubscriptions, useDiscounts, useExpressPaymentMethods, useFunnel, useFunnelLegacy, useGeoLocation, useGoogleAutocomplete, useInvalidateQuery, useISOData, useLanguageImport, useLogin, useOffer, useOrder, useOrderBump, usePayment, usePluginConfig, usePostPurchases, usePreloadQuery, usePreviewOffer, useProducts, usePromotions, useRegionOptions, useRemappableParams, useShippingRates, useSimpleFunnel, useStoreConfig, useTagadaContext, useThreeds, useThreedsModal, useTranslation, useVipOffers } from './react';
31
+ export { ApplePayButton, 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, useInvalidateQuery, useISOData, useLanguageImport, useLogin, useOffer, useOrder, useOrderBump, usePayment, usePaymentRetrieve, usePluginConfig, usePostPurchases, usePreloadQuery, usePreviewOffer, useProducts, usePromotions, useRegionOptions, useRemappableParams, useShippingRates, useSimpleFunnel, useStepConfig, useStoreConfig, useTagadaContext, useThreeds, useThreedsModal, useTranslation, useVipOffers } from './react';
29
32
  export type { DebugScript } from './react';
30
33
  export type { TranslateFunction, TranslationText, UseTranslationOptions, UseTranslationResult } from './react/hooks/useTranslation';
31
34
  export type { FunnelContextValue } from './react/hooks/useFunnel';
35
+ export type { UseApplePayCheckoutOptions } from './react/hooks/useApplePayCheckout';
32
36
  export type { ClubOffer, ClubOfferItem, ClubOfferLineItem, ClubOfferSummary, UseClubOffersOptions, UseClubOffersResult } from './react/hooks/useClubOffers';
33
37
  export type { UseCreditsOptions, UseCreditsResult } from './react/hooks/useCredits';
34
38
  export type { UseLoginOptions, UseLoginResult } from './react/hooks/useLogin';
@@ -37,5 +41,6 @@ export type { UseCustomerResult } from './react/hooks/useCustomer';
37
41
  export type { UseCustomerInfosOptions, UseCustomerInfosResult } from './react/hooks/useCustomerInfos';
38
42
  export type { UseCustomerOrdersOptions, UseCustomerOrdersResult } from './react/hooks/useCustomerOrders';
39
43
  export type { UseCustomerSubscriptionsOptions, UseCustomerSubscriptionsResult } from './react/hooks/useCustomerSubscriptions';
44
+ export type { PaymentRetrieveHook as UsePaymentRetrieveResult } from './react/hooks/usePaymentRetrieve';
40
45
  export type { UseShippingRatesQueryOptions, UseShippingRatesQueryResult } from './react/hooks/useShippingRatesQuery';
41
46
  export type { PreviewOfferSummary, UsePreviewOfferOptions, UsePreviewOfferResult } from './react/hooks/usePreviewOffer';