@weldsuite/helpdesk-widget-sdk 1.0.16 → 1.0.17

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.
package/dist/react.d.ts CHANGED
@@ -29,41 +29,6 @@ interface PositionConfig {
29
29
  left?: string;
30
30
  top?: string;
31
31
  }
32
- /**
33
- * Customization configuration
34
- */
35
- interface CustomizationConfig {
36
- primaryColor?: string;
37
- accentColor?: string;
38
- backgroundColor?: string;
39
- textColor?: string;
40
- fontFamily?: string;
41
- fontSize?: string;
42
- borderRadius?: string;
43
- }
44
- /**
45
- * Feature flags
46
- */
47
- interface FeatureConfig {
48
- attachments?: boolean;
49
- reactions?: boolean;
50
- typing?: boolean;
51
- readReceipts?: boolean;
52
- offlineMode?: boolean;
53
- fileUpload?: boolean;
54
- imageUpload?: boolean;
55
- voiceMessages?: boolean;
56
- videoMessages?: boolean;
57
- }
58
- /**
59
- * Mobile configuration
60
- */
61
- interface MobileConfig {
62
- fullScreen?: boolean;
63
- scrollLock?: boolean;
64
- keyboardHandling?: 'auto' | 'manual';
65
- safeAreaInsets?: boolean;
66
- }
67
32
  /**
68
33
  * Iframe configuration
69
34
  */
@@ -156,15 +121,13 @@ interface SecurityConfig {
156
121
  */
157
122
  interface WeldConfig {
158
123
  widgetId: string;
124
+ testMode?: boolean;
159
125
  api?: Partial<ApiConfig>;
160
126
  iframes?: Partial<IframeConfig>;
161
127
  position?: {
162
128
  launcher?: PositionConfig;
163
129
  widget?: PositionConfig;
164
130
  };
165
- customization?: CustomizationConfig;
166
- features?: FeatureConfig;
167
- mobile?: MobileConfig;
168
131
  auth?: AuthConfig;
169
132
  locale?: LocaleConfig;
170
133
  logging?: LogConfig;
@@ -184,11 +147,9 @@ interface WeldConfig {
184
147
  */
185
148
  interface ResolvedConfig {
186
149
  widgetId: string;
150
+ testMode?: boolean;
187
151
  api: ApiConfig;
188
152
  iframes: IframeConfig;
189
- customization: CustomizationConfig;
190
- features: FeatureConfig;
191
- mobile: MobileConfig;
192
153
  auth: AuthConfig;
193
154
  locale: LocaleConfig;
194
155
  logging: LogConfig;
@@ -458,6 +419,7 @@ declare class WeldSDK {
458
419
  private readyResolve;
459
420
  private boundHandleLauncherClick;
460
421
  private subscriptionIds;
422
+ private pageTrackingCleanup;
461
423
  constructor(config: WeldConfig);
462
424
  /**
463
425
  * Handle launcher click messages from iframe
@@ -491,6 +453,22 @@ declare class WeldSDK {
491
453
  * Check if SDK is ready
492
454
  */
493
455
  isReady(): boolean;
456
+ /**
457
+ * Update callbacks on an existing instance (used by singleton reuse)
458
+ */
459
+ updateCallbacks(config: Partial<WeldConfig>): void;
460
+ /**
461
+ * Persist open/closed state to sessionStorage
462
+ */
463
+ private persistOpenState;
464
+ /**
465
+ * Clear persisted state from sessionStorage
466
+ */
467
+ private clearPersistedState;
468
+ /**
469
+ * Check if widget was previously open (from sessionStorage)
470
+ */
471
+ private wasOpen;
494
472
  /**
495
473
  * Open the widget
496
474
  */
@@ -646,10 +624,23 @@ declare class WeldSDK {
646
624
  * Disable debug mode
647
625
  */
648
626
  disableDebug(): void;
627
+ /**
628
+ * Send a page change message to the widget iframe
629
+ */
630
+ private sendPageChange;
631
+ /**
632
+ * Start tracking page URL changes (SPA navigations + popstate)
633
+ */
634
+ private startPageTracking;
649
635
  /**
650
636
  * Ensure SDK is ready before operation
651
637
  */
652
638
  private ensureReady;
639
+ /**
640
+ * Detach from the current component lifecycle without destroying the widget.
641
+ * Use this as a React useEffect cleanup — the widget stays alive across navigations.
642
+ */
643
+ detach(): void;
653
644
  /**
654
645
  * Destroy SDK and cleanup
655
646
  */
package/dist/react.esm.js CHANGED
@@ -33,32 +33,6 @@ const DEFAULT_CONFIG = {
33
33
  closeOnClick: true,
34
34
  },
35
35
  },
36
- customization: {
37
- primaryColor: '#000000',
38
- accentColor: '#3b82f6',
39
- backgroundColor: '#ffffff',
40
- textColor: '#111827',
41
- fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
42
- fontSize: '14px',
43
- borderRadius: '12px',
44
- },
45
- features: {
46
- attachments: true,
47
- reactions: true,
48
- typing: true,
49
- readReceipts: true,
50
- offlineMode: false,
51
- fileUpload: true,
52
- imageUpload: true,
53
- voiceMessages: false,
54
- videoMessages: false,
55
- },
56
- mobile: {
57
- fullScreen: true,
58
- scrollLock: true,
59
- keyboardHandling: 'auto',
60
- safeAreaInsets: true,
61
- },
62
36
  auth: {
63
37
  enabled: true,
64
38
  mode: 'anonymous',
@@ -102,6 +76,7 @@ function resolveConfig(config) {
102
76
  validateConfig(config);
103
77
  return {
104
78
  widgetId: config.widgetId,
79
+ testMode: config.testMode,
105
80
  api: {
106
81
  ...DEFAULT_CONFIG.api,
107
82
  widgetId: config.widgetId,
@@ -131,18 +106,6 @@ function resolveConfig(config) {
131
106
  ...config.iframes?.backdrop,
132
107
  },
133
108
  },
134
- customization: {
135
- ...DEFAULT_CONFIG.customization,
136
- ...config.customization,
137
- },
138
- features: {
139
- ...DEFAULT_CONFIG.features,
140
- ...config.features,
141
- },
142
- mobile: {
143
- ...DEFAULT_CONFIG.mobile,
144
- ...config.mobile,
145
- },
146
109
  auth: {
147
110
  ...DEFAULT_CONFIG.auth,
148
111
  ...config.auth,
@@ -385,6 +348,8 @@ class IframeManager {
385
348
  this.modalContainer = null;
386
349
  this.styleElement = null;
387
350
  this.messageBroker = null;
351
+ // Guard flag to prevent double-binding event listeners
352
+ this.eventListenersBound = false;
388
353
  this.config = config;
389
354
  this.logger = new Logger(config.logging);
390
355
  this.deviceInfo = detectDevice();
@@ -424,18 +389,27 @@ class IframeManager {
424
389
  }
425
390
  /**
426
391
  * Create root container structure
392
+ * Reuses existing container if it has the same widgetId (singleton behavior)
427
393
  */
428
394
  createRootContainer() {
429
- // Check if already exists
430
- let existingContainer = document.getElementById('weld-container');
395
+ const existingContainer = document.getElementById('weld-container');
431
396
  if (existingContainer) {
432
- this.logger.warn('Weld container already exists, removing old instance');
397
+ // Reuse if same widgetId
398
+ if (existingContainer.getAttribute('data-widget-id') === this.config.widgetId) {
399
+ this.logger.debug('Reusing existing root container');
400
+ this.rootContainer = existingContainer;
401
+ this.appContainer = existingContainer.querySelector('.weld-app');
402
+ this.modalContainer = document.getElementById('weld-modal-container');
403
+ return;
404
+ }
405
+ this.logger.warn('Weld container already exists with different widgetId, removing old instance');
433
406
  existingContainer.remove();
434
407
  }
435
408
  // Create root container
436
409
  this.rootContainer = document.createElement('div');
437
410
  this.rootContainer.id = 'weld-container';
438
411
  this.rootContainer.className = 'weld-namespace';
412
+ this.rootContainer.setAttribute('data-widget-id', this.config.widgetId);
439
413
  // Create app container
440
414
  this.appContainer = document.createElement('div');
441
415
  this.appContainer.className = 'weld-app';
@@ -470,17 +444,7 @@ class IframeManager {
470
444
  * Generate CSS for containers
471
445
  */
472
446
  generateCSS() {
473
- const { customization } = this.config;
474
447
  return `
475
- /* Weld Container */
476
- #weld-container {
477
- --weld-color-primary: ${customization.primaryColor};
478
- --weld-color-accent: ${customization.accentColor};
479
- --weld-font-family: ${customization.fontFamily};
480
- --weld-font-size-base: ${customization.fontSize};
481
- --weld-radius-xl: ${customization.borderRadius};
482
- }
483
-
484
448
  /* Import main stylesheet */
485
449
  @import url('/styles/index.css');
486
450
 
@@ -504,6 +468,11 @@ class IframeManager {
504
468
  * Create launcher iframe
505
469
  */
506
470
  async createLauncherIframe() {
471
+ // Guard: skip if launcher iframe already exists
472
+ if (this.iframes.has(IframeType.LAUNCHER)) {
473
+ this.logger.debug('Launcher iframe already exists, skipping creation');
474
+ return;
475
+ }
507
476
  const { iframes } = this.config;
508
477
  const { launcher } = iframes;
509
478
  // Create container
@@ -531,9 +500,12 @@ class IframeManager {
531
500
  width: 100%;
532
501
  height: 100%;
533
502
  border: none;
534
- background: transparent;
503
+ background: none;
504
+ color-scheme: none;
535
505
  display: block;
536
506
  pointer-events: auto;
507
+ border-radius: 50%;
508
+ filter: drop-shadow(rgba(9, 14, 21, 0.54) 0px 1px 6px) drop-shadow(rgba(9, 14, 21, 0.9) 0px 2px 32px);
537
509
  `;
538
510
  iframe.setAttribute('allow', 'clipboard-write');
539
511
  iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-forms allow-popups');
@@ -549,20 +521,36 @@ class IframeManager {
549
521
  createdAt: Date.now(),
550
522
  });
551
523
  // When DOM loads, notify MessageBroker to send weld:init
524
+ let launcherRetried = false;
552
525
  iframe.onload = () => {
553
526
  const metadata = this.iframes.get(IframeType.LAUNCHER);
554
527
  if (metadata) {
555
528
  this.logger.debug('Launcher iframe DOM loaded');
556
- // Notify MessageBroker that DOM is loaded (triggers weld:init)
557
529
  this.messageBroker?.setIframeDomLoaded(IframeType.LAUNCHER);
558
530
  }
559
531
  };
532
+ iframe.onerror = () => {
533
+ this.logger.error('Launcher iframe failed to load');
534
+ if (!launcherRetried) {
535
+ launcherRetried = true;
536
+ this.logger.info('Retrying launcher iframe load...');
537
+ setTimeout(() => { iframe.src = this.buildIframeUrl(launcher.url); }, 3000);
538
+ }
539
+ else {
540
+ this.config.onError?.(new Error('Failed to load widget launcher'));
541
+ }
542
+ };
560
543
  this.logger.debug('Launcher iframe created');
561
544
  }
562
545
  /**
563
546
  * Create widget iframe
564
547
  */
565
548
  async createWidgetIframe() {
549
+ // Guard: skip if widget iframe already exists
550
+ if (this.iframes.has(IframeType.WIDGET)) {
551
+ this.logger.debug('Widget iframe already exists, skipping creation');
552
+ return;
553
+ }
566
554
  const { iframes } = this.config;
567
555
  const { widget } = iframes;
568
556
  // Create container
@@ -643,14 +631,25 @@ class IframeManager {
643
631
  createdAt: Date.now(),
644
632
  });
645
633
  // When DOM loads, notify MessageBroker to send weld:init
634
+ let widgetRetried = false;
646
635
  iframe.onload = () => {
647
636
  const metadata = this.iframes.get(IframeType.WIDGET);
648
637
  if (metadata) {
649
638
  this.logger.debug('Widget iframe DOM loaded');
650
- // Notify MessageBroker that DOM is loaded (triggers weld:init)
651
639
  this.messageBroker?.setIframeDomLoaded(IframeType.WIDGET);
652
640
  }
653
641
  };
642
+ iframe.onerror = () => {
643
+ this.logger.error('Widget iframe failed to load');
644
+ if (!widgetRetried) {
645
+ widgetRetried = true;
646
+ this.logger.info('Retrying widget iframe load...');
647
+ setTimeout(() => { iframe.src = this.buildIframeUrl(widget.url); }, 3000);
648
+ }
649
+ else {
650
+ this.config.onError?.(new Error('Failed to load widget'));
651
+ }
652
+ };
654
653
  this.logger.debug('Widget iframe created');
655
654
  }
656
655
  /**
@@ -671,12 +670,21 @@ class IframeManager {
671
670
  url.searchParams.set('device', this.deviceInfo.type);
672
671
  url.searchParams.set('mobile', String(this.deviceInfo.isMobile));
673
672
  url.searchParams.set('parentOrigin', window.location.origin);
673
+ if (this.config.testMode) {
674
+ url.searchParams.set('testMode', 'true');
675
+ }
674
676
  return url.toString();
675
677
  }
676
678
  /**
677
679
  * Setup event listeners
678
680
  */
679
681
  setupEventListeners() {
682
+ // Guard: prevent double-binding
683
+ if (this.eventListenersBound) {
684
+ this.logger.debug('Event listeners already bound, skipping');
685
+ return;
686
+ }
687
+ this.eventListenersBound = true;
680
688
  // Window resize - use bound handler for proper cleanup
681
689
  window.addEventListener('resize', this.boundHandleResize);
682
690
  // Orientation change - use bound handler for proper cleanup
@@ -779,7 +787,7 @@ class IframeManager {
779
787
  iframe.container.style.transform = 'scale(1) translateY(0)';
780
788
  }
781
789
  // Handle mobile scroll lock
782
- if (this.deviceInfo.isMobile && type === IframeType.WIDGET && this.config.mobile.scrollLock) {
790
+ if (this.deviceInfo.isMobile && type === IframeType.WIDGET) {
783
791
  document.body.classList.add('weld-mobile-open');
784
792
  }
785
793
  // Hide launcher on mobile when widget is open (full-screen mode)
@@ -878,6 +886,8 @@ class IframeManager {
878
886
  this.iframes.clear();
879
887
  // Clear messageBroker reference
880
888
  this.messageBroker = null;
889
+ // Reset guard flag
890
+ this.eventListenersBound = false;
881
891
  this.logger.info('IframeManager destroyed');
882
892
  }
883
893
  }
@@ -945,6 +955,8 @@ var MessageType;
945
955
  // Events
946
956
  MessageType["EVENT_TRACK"] = "weld:event:track";
947
957
  MessageType["ERROR_REPORT"] = "weld:error:report";
958
+ // Page tracking
959
+ MessageType["PAGE_CHANGE"] = "weld:page:change";
948
960
  // API responses
949
961
  MessageType["API_SUCCESS"] = "weld:api:success";
950
962
  MessageType["API_ERROR"] = "weld:api:error";
@@ -1484,8 +1496,6 @@ class MessageBroker {
1484
1496
  iframeType,
1485
1497
  config: {
1486
1498
  api: this.config.api,
1487
- customization: this.config.customization,
1488
- features: this.config.features,
1489
1499
  },
1490
1500
  };
1491
1501
  const message = createMessage('weld:init', MessageOrigin.PARENT, initPayload);
@@ -2158,7 +2168,7 @@ class StateCoordinator {
2158
2168
  }
2159
2169
  }
2160
2170
 
2161
- var version = "1.0.16";
2171
+ var version = "1.0.17";
2162
2172
  var packageJson = {
2163
2173
  version: version};
2164
2174
 
@@ -2166,6 +2176,16 @@ var packageJson = {
2166
2176
  * Weld SDK - Main Entry Point
2167
2177
  * Public API for the Weld helpdesk widget
2168
2178
  */
2179
+ /**
2180
+ * Module-level singleton registry keyed by widgetId
2181
+ */
2182
+ const sdkRegistry = new Map();
2183
+ /**
2184
+ * SessionStorage key helpers
2185
+ */
2186
+ function openStateKey(widgetId) {
2187
+ return `weld-widget-open-${widgetId}`;
2188
+ }
2169
2189
  /**
2170
2190
  * SDK initialization status
2171
2191
  */
@@ -2188,6 +2208,8 @@ class WeldSDK {
2188
2208
  this.readyResolve = null;
2189
2209
  // Subscription IDs for cleanup
2190
2210
  this.subscriptionIds = [];
2211
+ // Page tracking cleanup
2212
+ this.pageTrackingCleanup = null;
2191
2213
  /**
2192
2214
  * Update user attributes (Intercom-style, with rate limiting)
2193
2215
  * Limited to 20 calls per page load to prevent abuse
@@ -2226,6 +2248,13 @@ class WeldSDK {
2226
2248
  console.log('[Weld SDK] Received message:', event.data.type);
2227
2249
  }
2228
2250
  if (event.data?.type === 'launcher:clicked') {
2251
+ if (this.status !== SDKStatus.READY) {
2252
+ console.log('[Weld SDK] Launcher clicked but SDK not ready yet — waiting...');
2253
+ this.readyPromise?.then(() => {
2254
+ this.handleLauncherClickMessage(event);
2255
+ });
2256
+ return;
2257
+ }
2229
2258
  // Toggle behavior - if widget is open, close it; if closed, open it
2230
2259
  const state = this.stateCoordinator.getState();
2231
2260
  if (state.widget.isOpen) {
@@ -2238,9 +2267,24 @@ class WeldSDK {
2238
2267
  }
2239
2268
  }
2240
2269
  if (event.data?.type === 'weld:close') {
2270
+ if (this.status !== SDKStatus.READY)
2271
+ return;
2241
2272
  console.log('[Weld SDK] Widget close requested');
2242
2273
  this.close();
2243
2274
  }
2275
+ if (event.data?.type === 'weld:unread-count') {
2276
+ const count = event.data.count ?? 0;
2277
+ // Forward to launcher iframe
2278
+ const launcherIframe = this.iframeManager.getIframe(IframeType.LAUNCHER);
2279
+ if (launcherIframe?.element?.contentWindow) {
2280
+ launcherIframe.element.contentWindow.postMessage({
2281
+ type: 'weld:unread-count',
2282
+ count
2283
+ }, '*');
2284
+ }
2285
+ // Update state coordinator for external API consumers
2286
+ this.stateCoordinator.setBadgeCount(count);
2287
+ }
2244
2288
  if (event.data?.type === 'weld:image:open' && event.data?.url) {
2245
2289
  this.showImageLightbox(event.data.url);
2246
2290
  }
@@ -2502,6 +2546,13 @@ class WeldSDK {
2502
2546
  this.logger.info('WeldSDK ready');
2503
2547
  // Call onReady callback
2504
2548
  this.config.onReady?.();
2549
+ // Start tracking page URL changes
2550
+ this.startPageTracking();
2551
+ // Auto-open if widget was previously open (persisted in sessionStorage)
2552
+ if (this.wasOpen()) {
2553
+ this.logger.info('Restoring previously open widget from sessionStorage');
2554
+ this.open();
2555
+ }
2505
2556
  }
2506
2557
  catch (error) {
2507
2558
  this.status = SDKStatus.ERROR;
@@ -2580,6 +2631,58 @@ class WeldSDK {
2580
2631
  isReady() {
2581
2632
  return this.status === SDKStatus.READY;
2582
2633
  }
2634
+ /**
2635
+ * Update callbacks on an existing instance (used by singleton reuse)
2636
+ */
2637
+ updateCallbacks(config) {
2638
+ if (config.onReady !== undefined)
2639
+ this.config.onReady = config.onReady;
2640
+ if (config.onOpen !== undefined)
2641
+ this.config.onOpen = config.onOpen;
2642
+ if (config.onClose !== undefined)
2643
+ this.config.onClose = config.onClose;
2644
+ if (config.onError !== undefined)
2645
+ this.config.onError = config.onError;
2646
+ if (config.onDestroy !== undefined)
2647
+ this.config.onDestroy = config.onDestroy;
2648
+ if (config.onMinimize !== undefined)
2649
+ this.config.onMinimize = config.onMinimize;
2650
+ if (config.onMaximize !== undefined)
2651
+ this.config.onMaximize = config.onMaximize;
2652
+ }
2653
+ /**
2654
+ * Persist open/closed state to sessionStorage
2655
+ */
2656
+ persistOpenState(isOpen) {
2657
+ try {
2658
+ sessionStorage.setItem(openStateKey(this.config.widgetId), isOpen ? 'true' : 'false');
2659
+ }
2660
+ catch {
2661
+ // sessionStorage might not be available
2662
+ }
2663
+ }
2664
+ /**
2665
+ * Clear persisted state from sessionStorage
2666
+ */
2667
+ clearPersistedState() {
2668
+ try {
2669
+ sessionStorage.removeItem(openStateKey(this.config.widgetId));
2670
+ }
2671
+ catch {
2672
+ // sessionStorage might not be available
2673
+ }
2674
+ }
2675
+ /**
2676
+ * Check if widget was previously open (from sessionStorage)
2677
+ */
2678
+ wasOpen() {
2679
+ try {
2680
+ return sessionStorage.getItem(openStateKey(this.config.widgetId)) === 'true';
2681
+ }
2682
+ catch {
2683
+ return false;
2684
+ }
2685
+ }
2583
2686
  /**
2584
2687
  * Open the widget
2585
2688
  */
@@ -2598,6 +2701,7 @@ class WeldSDK {
2598
2701
  if (launcherIframe?.element?.contentWindow) {
2599
2702
  launcherIframe.element.contentWindow.postMessage({ type: 'weld:widget-opened' }, '*');
2600
2703
  }
2704
+ this.persistOpenState(true);
2601
2705
  this.config.onOpen?.();
2602
2706
  }
2603
2707
  /**
@@ -2618,6 +2722,7 @@ class WeldSDK {
2618
2722
  if (launcherIframe?.element?.contentWindow) {
2619
2723
  launcherIframe.element.contentWindow.postMessage({ type: 'weld:widget-closed' }, '*');
2620
2724
  }
2725
+ this.persistOpenState(false);
2621
2726
  this.config.onClose?.();
2622
2727
  }
2623
2728
  /**
@@ -2806,6 +2911,8 @@ class WeldSDK {
2806
2911
  });
2807
2912
  // Broadcast logout to iframes
2808
2913
  this.messageBroker.broadcast('weld:auth:logout', {});
2914
+ // Clear persisted widget state
2915
+ this.clearPersistedState();
2809
2916
  // Clear any stored session data
2810
2917
  try {
2811
2918
  const prefix = 'weld-';
@@ -2947,6 +3054,63 @@ class WeldSDK {
2947
3054
  this.logger.setLevel('warn');
2948
3055
  this.logger.info('Debug mode disabled');
2949
3056
  }
3057
+ /**
3058
+ * Send a page change message to the widget iframe
3059
+ */
3060
+ sendPageChange(url, title) {
3061
+ const widgetIframe = this.iframeManager.getIframe(IframeType.WIDGET);
3062
+ if (widgetIframe?.element?.contentWindow) {
3063
+ widgetIframe.element.contentWindow.postMessage({
3064
+ type: 'weld:page:change',
3065
+ url,
3066
+ title,
3067
+ timestamp: Date.now(),
3068
+ }, '*');
3069
+ }
3070
+ }
3071
+ /**
3072
+ * Start tracking page URL changes (SPA navigations + popstate)
3073
+ */
3074
+ startPageTracking() {
3075
+ let lastUrl = window.location.href;
3076
+ let debounceTimer = null;
3077
+ const notifyChange = () => {
3078
+ const currentUrl = window.location.href;
3079
+ if (currentUrl !== lastUrl) {
3080
+ lastUrl = currentUrl;
3081
+ this.sendPageChange(currentUrl, document.title);
3082
+ }
3083
+ };
3084
+ const debouncedNotify = () => {
3085
+ if (debounceTimer)
3086
+ clearTimeout(debounceTimer);
3087
+ debounceTimer = setTimeout(notifyChange, 300);
3088
+ };
3089
+ // Send initial page
3090
+ this.sendPageChange(window.location.href, document.title);
3091
+ // Monkey-patch history.pushState and history.replaceState
3092
+ const origPushState = history.pushState.bind(history);
3093
+ const origReplaceState = history.replaceState.bind(history);
3094
+ history.pushState = function (...args) {
3095
+ origPushState(...args);
3096
+ debouncedNotify();
3097
+ };
3098
+ history.replaceState = function (...args) {
3099
+ origReplaceState(...args);
3100
+ debouncedNotify();
3101
+ };
3102
+ // Listen for popstate (browser back/forward)
3103
+ const handlePopstate = () => debouncedNotify();
3104
+ window.addEventListener('popstate', handlePopstate);
3105
+ // Store cleanup
3106
+ this.pageTrackingCleanup = () => {
3107
+ if (debounceTimer)
3108
+ clearTimeout(debounceTimer);
3109
+ window.removeEventListener('popstate', handlePopstate);
3110
+ history.pushState = origPushState;
3111
+ history.replaceState = origReplaceState;
3112
+ };
3113
+ }
2950
3114
  /**
2951
3115
  * Ensure SDK is ready before operation
2952
3116
  */
@@ -2955,11 +3119,26 @@ class WeldSDK {
2955
3119
  throw new Error('SDK not ready. Call init() first.');
2956
3120
  }
2957
3121
  }
3122
+ /**
3123
+ * Detach from the current component lifecycle without destroying the widget.
3124
+ * Use this as a React useEffect cleanup — the widget stays alive across navigations.
3125
+ */
3126
+ detach() {
3127
+ // No-op: widget stays alive in the singleton registry
3128
+ this.logger.debug('WeldSDK detached (no-op, widget stays alive)');
3129
+ }
2958
3130
  /**
2959
3131
  * Destroy SDK and cleanup
2960
3132
  */
2961
3133
  destroy() {
2962
3134
  this.logger.info('Destroying WeldSDK');
3135
+ // Remove from singleton registry
3136
+ sdkRegistry.delete(this.config.widgetId);
3137
+ // Clear persisted state
3138
+ this.clearPersistedState();
3139
+ // Stop page tracking
3140
+ this.pageTrackingCleanup?.();
3141
+ this.pageTrackingCleanup = null;
2963
3142
  // Remove event listener using bound handler
2964
3143
  window.removeEventListener('message', this.boundHandleLauncherClick);
2965
3144
  // Unsubscribe from all message broker subscriptions