@tagadapay/plugin-sdk 3.1.5 → 3.1.9

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 (71) hide show
  1. package/README.md +1129 -1129
  2. package/build-cdn.js +220 -113
  3. package/dist/external-tracker.js +1225 -558
  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/tagada-sdk.js +10142 -0
  13. package/dist/tagada-sdk.min.js +43 -0
  14. package/dist/tagada-sdk.min.js.map +7 -0
  15. package/dist/v2/core/client.js +34 -2
  16. package/dist/v2/core/config/environment.js +9 -2
  17. package/dist/v2/core/funnelClient.d.ts +180 -2
  18. package/dist/v2/core/funnelClient.js +289 -6
  19. package/dist/v2/core/resources/apiClient.js +1 -1
  20. package/dist/v2/core/resources/checkout.d.ts +68 -0
  21. package/dist/v2/core/resources/funnel.d.ts +25 -0
  22. package/dist/v2/core/resources/payments.d.ts +70 -3
  23. package/dist/v2/core/resources/payments.js +72 -7
  24. package/dist/v2/core/utils/index.d.ts +1 -0
  25. package/dist/v2/core/utils/index.js +2 -0
  26. package/dist/v2/core/utils/pluginConfig.d.ts +8 -0
  27. package/dist/v2/core/utils/pluginConfig.js +68 -5
  28. package/dist/v2/core/utils/previewMode.d.ts +7 -0
  29. package/dist/v2/core/utils/previewMode.js +72 -14
  30. package/dist/v2/core/utils/previewModeIndicator.d.ts +19 -0
  31. package/dist/v2/core/utils/previewModeIndicator.js +414 -0
  32. package/dist/v2/core/utils/tokenStorage.d.ts +4 -0
  33. package/dist/v2/core/utils/tokenStorage.js +15 -1
  34. package/dist/v2/index.d.ts +9 -3
  35. package/dist/v2/index.js +8 -3
  36. package/dist/v2/react/components/ApplePayButton.d.ts +22 -123
  37. package/dist/v2/react/components/ApplePayButton.js +247 -317
  38. package/dist/v2/react/components/FunnelScriptInjector.d.ts +3 -1
  39. package/dist/v2/react/components/FunnelScriptInjector.js +255 -162
  40. package/dist/v2/react/components/GooglePayButton.d.ts +2 -0
  41. package/dist/v2/react/components/GooglePayButton.js +80 -64
  42. package/dist/v2/react/components/PreviewModeIndicator.d.ts +46 -0
  43. package/dist/v2/react/components/PreviewModeIndicator.js +113 -0
  44. package/dist/v2/react/hooks/useApplePayCheckout.d.ts +16 -0
  45. package/dist/v2/react/hooks/useApplePayCheckout.js +193 -0
  46. package/dist/v2/react/hooks/useFunnel.d.ts +48 -6
  47. package/dist/v2/react/hooks/useFunnel.js +25 -5
  48. package/dist/v2/react/hooks/useGoogleAutocomplete.d.ts +10 -0
  49. package/dist/v2/react/hooks/useGoogleAutocomplete.js +48 -0
  50. package/dist/v2/react/hooks/useGooglePayCheckout.d.ts +21 -0
  51. package/dist/v2/react/hooks/useGooglePayCheckout.js +198 -0
  52. package/dist/v2/react/hooks/usePaymentPolling.d.ts +15 -3
  53. package/dist/v2/react/hooks/usePaymentPolling.js +31 -9
  54. package/dist/v2/react/hooks/usePaymentQuery.d.ts +34 -2
  55. package/dist/v2/react/hooks/usePaymentQuery.js +731 -7
  56. package/dist/v2/react/hooks/usePaymentRetrieve.d.ts +26 -0
  57. package/dist/v2/react/hooks/usePaymentRetrieve.js +175 -0
  58. package/dist/v2/react/hooks/usePixelTracking.d.ts +56 -0
  59. package/dist/v2/react/hooks/usePixelTracking.js +508 -0
  60. package/dist/v2/react/hooks/useStepConfig.d.ts +64 -0
  61. package/dist/v2/react/hooks/useStepConfig.js +53 -0
  62. package/dist/v2/react/index.d.ts +15 -5
  63. package/dist/v2/react/index.js +8 -2
  64. package/dist/v2/react/providers/ExpressPaymentMethodsProvider.d.ts +1 -0
  65. package/dist/v2/react/providers/ExpressPaymentMethodsProvider.js +41 -13
  66. package/dist/v2/react/providers/TagadaProvider.js +24 -23
  67. package/dist/v2/standalone/external-tracker.d.ts +2 -0
  68. package/dist/v2/standalone/external-tracker.js +6 -3
  69. package/package.json +112 -112
  70. package/dist/v2/react/hooks/useApplePay.d.ts +0 -16
  71. package/dist/v2/react/hooks/useApplePay.js +0 -247
@@ -34,6 +34,8 @@ const STORAGE_KEYS = {
34
34
  FORCE_RESET: 'tgd_force_reset',
35
35
  CLIENT_ENV: 'tgd_client_env',
36
36
  CLIENT_BASE_URL: 'tgd_client_base_url',
37
+ CURRENCY: 'tgd_currency',
38
+ LOCALE: 'tgd_locale',
37
39
  };
38
40
  /**
39
41
  * Check if force reset is active (simulates hard refresh)
@@ -82,10 +84,16 @@ function getFromCookie(key) {
82
84
  }
83
85
  /**
84
86
  * Set value in cookie
87
+ * ⚠️ NEVER allows tgd_draft to be set as a cookie (localStorage-only)
85
88
  */
86
89
  function setInCookie(key, value, maxAge = 86400) {
87
90
  if (typeof document === 'undefined')
88
91
  return;
92
+ // 🛡️ SAFETY: Block tgd_draft from EVER being written to cookies
93
+ if (key === 'tgd_draft' || key === 'tgd-draft') {
94
+ console.warn(`🛡️ [SDK] Blocked attempt to set ${key} as cookie - this should only be in localStorage`);
95
+ return;
96
+ }
89
97
  document.cookie = `${key}=${value}; path=/; max-age=${maxAge}`;
90
98
  }
91
99
  /**
@@ -105,14 +113,14 @@ export function getSDKParams() {
105
113
  return {};
106
114
  }
107
115
  const urlParams = new URLSearchParams(window.location.search);
108
- // Get draft mode (URL > localStorage > cookie)
116
+ // Get draft mode (URL > localStorage only - no cookie fallback to avoid resurrection)
109
117
  let draft;
110
118
  const urlDraft = urlParams.get('draft');
111
119
  if (urlDraft !== null) {
112
120
  draft = urlDraft === 'true';
113
121
  }
114
122
  else {
115
- const storageDraft = getFromStorage(STORAGE_KEYS.DRAFT) || getFromCookie(STORAGE_KEYS.DRAFT);
123
+ const storageDraft = getFromStorage(STORAGE_KEYS.DRAFT);
116
124
  if (storageDraft !== null) {
117
125
  draft = storageDraft === 'true';
118
126
  }
@@ -135,6 +143,12 @@ export function getSDKParams() {
135
143
  const funnelSessionId = urlParams.get('funnelSessionId') || null;
136
144
  // Get funnel ID (URL only)
137
145
  const funnelId = urlParams.get('funnelId') || null;
146
+ // Get funnel environment (URL only - not persisted, set on entry)
147
+ let funnelEnv;
148
+ const urlFunnelEnv = urlParams.get('funnelEnv');
149
+ if (urlFunnelEnv && (urlFunnelEnv === 'staging' || urlFunnelEnv === 'production')) {
150
+ funnelEnv = urlFunnelEnv;
151
+ }
138
152
  // Force reset is URL-only (not persisted)
139
153
  const forceReset = urlParams.get('forceReset') === 'true';
140
154
  // Get client environment override (URL > localStorage > cookie)
@@ -161,6 +175,30 @@ export function getSDKParams() {
161
175
  tagadaClientBaseUrl = storageBaseUrl;
162
176
  }
163
177
  }
178
+ // Get display currency (URL > localStorage > cookie)
179
+ let currency;
180
+ const urlCurrency = urlParams.get('currency');
181
+ if (urlCurrency) {
182
+ currency = urlCurrency;
183
+ }
184
+ else {
185
+ const storageCurrency = getFromStorage(STORAGE_KEYS.CURRENCY) || getFromCookie(STORAGE_KEYS.CURRENCY);
186
+ if (storageCurrency) {
187
+ currency = storageCurrency;
188
+ }
189
+ }
190
+ // Get display locale (URL > localStorage > cookie)
191
+ let locale;
192
+ const urlLocale = urlParams.get('locale');
193
+ if (urlLocale) {
194
+ locale = urlLocale;
195
+ }
196
+ else {
197
+ const storageLocale = getFromStorage(STORAGE_KEYS.LOCALE) || getFromCookie(STORAGE_KEYS.LOCALE);
198
+ if (storageLocale) {
199
+ locale = storageLocale;
200
+ }
201
+ }
164
202
  return {
165
203
  forceReset,
166
204
  token,
@@ -168,8 +206,11 @@ export function getSDKParams() {
168
206
  funnelId,
169
207
  draft,
170
208
  funnelTracking,
209
+ funnelEnv,
171
210
  tagadaClientEnv,
172
211
  tagadaClientBaseUrl,
212
+ currency,
213
+ locale,
173
214
  };
174
215
  }
175
216
  /**
@@ -189,17 +230,21 @@ export function isDraftMode() {
189
230
  }
190
231
  /**
191
232
  * Set draft mode in storage for persistence
233
+ * ⚠️ ONLY writes to localStorage (not cookies) to avoid resurrection issues
192
234
  */
193
235
  export function setDraftMode(draft) {
194
236
  if (typeof window === 'undefined')
195
237
  return;
196
238
  if (draft) {
197
239
  setInStorage(STORAGE_KEYS.DRAFT, 'true');
198
- setInCookie(STORAGE_KEYS.DRAFT, 'true', 86400); // 24 hours
199
240
  }
200
241
  else {
201
- setInStorage(STORAGE_KEYS.DRAFT, 'false');
202
- clearFromCookie(STORAGE_KEYS.DRAFT);
242
+ try {
243
+ localStorage.removeItem(STORAGE_KEYS.DRAFT);
244
+ }
245
+ catch {
246
+ // Storage not available
247
+ }
203
248
  }
204
249
  }
205
250
  /**
@@ -214,6 +259,13 @@ export function setDraftMode(draft) {
214
259
  * @returns True if force reset was activated and state was cleared
215
260
  */
216
261
  export function handlePreviewMode(debugMode = false) {
262
+ // 🔒 CRITICAL: Read URL token DIRECTLY first before any getSDKParams()
263
+ // This ensures we have the URL token before any clearing happens
264
+ let urlToken = null;
265
+ if (typeof window !== 'undefined') {
266
+ const urlParams = new URLSearchParams(window.location.search);
267
+ urlToken = urlParams.get('token');
268
+ }
217
269
  const params = getSDKParams();
218
270
  const shouldReset = params.forceReset || false;
219
271
  if (!shouldReset && !params.token) {
@@ -224,6 +276,7 @@ export function handlePreviewMode(debugMode = false) {
224
276
  }
225
277
  if (debugMode) {
226
278
  console.log('[SDK] Detected params:', params);
279
+ console.log('[SDK] URL token (direct read):', urlToken ? urlToken.substring(0, 20) + '...' : 'none');
227
280
  }
228
281
  // CASE 1: Force reset - clear all state (simulates hard refresh)
229
282
  if (shouldReset) {
@@ -246,23 +299,28 @@ export function handlePreviewMode(debugMode = false) {
246
299
  }
247
300
  }
248
301
  // CASE 2: Token in URL - override stored token
249
- if (params.token !== null && params.token !== undefined) {
302
+ // 🔒 CRITICAL: Use the directly-read URL token to avoid any race conditions
303
+ const tokenToSet = urlToken || params.token;
304
+ if (tokenToSet !== null && tokenToSet !== undefined) {
250
305
  if (debugMode) {
251
- console.log('[SDK] Using token from URL:', params.token.substring(0, 20) + '...');
306
+ console.log('[SDK] Setting token from URL:', tokenToSet.substring(0, 20) + '...');
252
307
  }
253
- if (params.token === '' || params.token === 'null') {
308
+ if (tokenToSet === '' || tokenToSet === 'null') {
254
309
  // Explicitly cleared token
255
310
  clearClientToken();
256
311
  }
257
312
  else {
258
- // Set token from URL
259
- setClientToken(params.token);
313
+ // Set token from URL IMMEDIATELY after clearing
314
+ setClientToken(tokenToSet);
315
+ if (debugMode) {
316
+ console.log('[SDK] ✅ Token set in localStorage immediately after clear');
317
+ }
260
318
  }
261
319
  }
262
320
  else if (shouldReset) {
263
321
  // Force reset but no token = clear stored token
264
322
  if (debugMode) {
265
- console.log('[SDK] Force reset mode (no token)');
323
+ console.log('[SDK] Force reset mode (no token in URL)');
266
324
  }
267
325
  clearClientToken();
268
326
  }
@@ -311,7 +369,7 @@ export function setFunnelTracking(enabled) {
311
369
  return;
312
370
  const value = enabled ? 'true' : 'false';
313
371
  setInStorage(STORAGE_KEYS.FUNNEL_TRACKING, value);
314
- setInCookie(STORAGE_KEYS.FUNNEL_TRACKING, value, 86400); // 24 hours
372
+ setInCookie(STORAGE_KEYS.FUNNEL_TRACKING, value, 86400);
315
373
  }
316
374
  /**
317
375
  * Set client environment override in storage for persistence
@@ -320,7 +378,7 @@ export function setClientEnvironment(env) {
320
378
  if (typeof window === 'undefined')
321
379
  return;
322
380
  setInStorage(STORAGE_KEYS.CLIENT_ENV, env);
323
- setInCookie(STORAGE_KEYS.CLIENT_ENV, env, 86400); // 24 hours
381
+ setInCookie(STORAGE_KEYS.CLIENT_ENV, env, 86400);
324
382
  }
325
383
  /**
326
384
  * Clear client environment override
@@ -343,7 +401,7 @@ export function setClientBaseUrl(baseUrl) {
343
401
  if (typeof window === 'undefined')
344
402
  return;
345
403
  setInStorage(STORAGE_KEYS.CLIENT_BASE_URL, baseUrl);
346
- setInCookie(STORAGE_KEYS.CLIENT_BASE_URL, baseUrl, 86400); // 24 hours
404
+ setInCookie(STORAGE_KEYS.CLIENT_BASE_URL, baseUrl, 86400);
347
405
  }
348
406
  /**
349
407
  * 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);