@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/angular.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
  */
@@ -93,32 +93,6 @@ const DEFAULT_CONFIG = {
93
93
  closeOnClick: true,
94
94
  },
95
95
  },
96
- customization: {
97
- primaryColor: '#000000',
98
- accentColor: '#3b82f6',
99
- backgroundColor: '#ffffff',
100
- textColor: '#111827',
101
- fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
102
- fontSize: '14px',
103
- borderRadius: '12px',
104
- },
105
- features: {
106
- attachments: true,
107
- reactions: true,
108
- typing: true,
109
- readReceipts: true,
110
- offlineMode: false,
111
- fileUpload: true,
112
- imageUpload: true,
113
- voiceMessages: false,
114
- videoMessages: false,
115
- },
116
- mobile: {
117
- fullScreen: true,
118
- scrollLock: true,
119
- keyboardHandling: 'auto',
120
- safeAreaInsets: true,
121
- },
122
96
  auth: {
123
97
  enabled: true,
124
98
  mode: 'anonymous',
@@ -162,6 +136,7 @@ function resolveConfig(config) {
162
136
  validateConfig(config);
163
137
  return {
164
138
  widgetId: config.widgetId,
139
+ testMode: config.testMode,
165
140
  api: {
166
141
  ...DEFAULT_CONFIG.api,
167
142
  widgetId: config.widgetId,
@@ -191,18 +166,6 @@ function resolveConfig(config) {
191
166
  ...config.iframes?.backdrop,
192
167
  },
193
168
  },
194
- customization: {
195
- ...DEFAULT_CONFIG.customization,
196
- ...config.customization,
197
- },
198
- features: {
199
- ...DEFAULT_CONFIG.features,
200
- ...config.features,
201
- },
202
- mobile: {
203
- ...DEFAULT_CONFIG.mobile,
204
- ...config.mobile,
205
- },
206
169
  auth: {
207
170
  ...DEFAULT_CONFIG.auth,
208
171
  ...config.auth,
@@ -445,6 +408,8 @@ class IframeManager {
445
408
  this.modalContainer = null;
446
409
  this.styleElement = null;
447
410
  this.messageBroker = null;
411
+ // Guard flag to prevent double-binding event listeners
412
+ this.eventListenersBound = false;
448
413
  this.config = config;
449
414
  this.logger = new Logger(config.logging);
450
415
  this.deviceInfo = detectDevice();
@@ -484,18 +449,27 @@ class IframeManager {
484
449
  }
485
450
  /**
486
451
  * Create root container structure
452
+ * Reuses existing container if it has the same widgetId (singleton behavior)
487
453
  */
488
454
  createRootContainer() {
489
- // Check if already exists
490
- let existingContainer = document.getElementById('weld-container');
455
+ const existingContainer = document.getElementById('weld-container');
491
456
  if (existingContainer) {
492
- this.logger.warn('Weld container already exists, removing old instance');
457
+ // Reuse if same widgetId
458
+ if (existingContainer.getAttribute('data-widget-id') === this.config.widgetId) {
459
+ this.logger.debug('Reusing existing root container');
460
+ this.rootContainer = existingContainer;
461
+ this.appContainer = existingContainer.querySelector('.weld-app');
462
+ this.modalContainer = document.getElementById('weld-modal-container');
463
+ return;
464
+ }
465
+ this.logger.warn('Weld container already exists with different widgetId, removing old instance');
493
466
  existingContainer.remove();
494
467
  }
495
468
  // Create root container
496
469
  this.rootContainer = document.createElement('div');
497
470
  this.rootContainer.id = 'weld-container';
498
471
  this.rootContainer.className = 'weld-namespace';
472
+ this.rootContainer.setAttribute('data-widget-id', this.config.widgetId);
499
473
  // Create app container
500
474
  this.appContainer = document.createElement('div');
501
475
  this.appContainer.className = 'weld-app';
@@ -530,17 +504,7 @@ class IframeManager {
530
504
  * Generate CSS for containers
531
505
  */
532
506
  generateCSS() {
533
- const { customization } = this.config;
534
507
  return `
535
- /* Weld Container */
536
- #weld-container {
537
- --weld-color-primary: ${customization.primaryColor};
538
- --weld-color-accent: ${customization.accentColor};
539
- --weld-font-family: ${customization.fontFamily};
540
- --weld-font-size-base: ${customization.fontSize};
541
- --weld-radius-xl: ${customization.borderRadius};
542
- }
543
-
544
508
  /* Import main stylesheet */
545
509
  @import url('/styles/index.css');
546
510
 
@@ -564,6 +528,11 @@ class IframeManager {
564
528
  * Create launcher iframe
565
529
  */
566
530
  async createLauncherIframe() {
531
+ // Guard: skip if launcher iframe already exists
532
+ if (this.iframes.has(IframeType.LAUNCHER)) {
533
+ this.logger.debug('Launcher iframe already exists, skipping creation');
534
+ return;
535
+ }
567
536
  const { iframes } = this.config;
568
537
  const { launcher } = iframes;
569
538
  // Create container
@@ -591,9 +560,12 @@ class IframeManager {
591
560
  width: 100%;
592
561
  height: 100%;
593
562
  border: none;
594
- background: transparent;
563
+ background: none;
564
+ color-scheme: none;
595
565
  display: block;
596
566
  pointer-events: auto;
567
+ border-radius: 50%;
568
+ filter: drop-shadow(rgba(9, 14, 21, 0.54) 0px 1px 6px) drop-shadow(rgba(9, 14, 21, 0.9) 0px 2px 32px);
597
569
  `;
598
570
  iframe.setAttribute('allow', 'clipboard-write');
599
571
  iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-forms allow-popups');
@@ -609,20 +581,36 @@ class IframeManager {
609
581
  createdAt: Date.now(),
610
582
  });
611
583
  // When DOM loads, notify MessageBroker to send weld:init
584
+ let launcherRetried = false;
612
585
  iframe.onload = () => {
613
586
  const metadata = this.iframes.get(IframeType.LAUNCHER);
614
587
  if (metadata) {
615
588
  this.logger.debug('Launcher iframe DOM loaded');
616
- // Notify MessageBroker that DOM is loaded (triggers weld:init)
617
589
  this.messageBroker?.setIframeDomLoaded(IframeType.LAUNCHER);
618
590
  }
619
591
  };
592
+ iframe.onerror = () => {
593
+ this.logger.error('Launcher iframe failed to load');
594
+ if (!launcherRetried) {
595
+ launcherRetried = true;
596
+ this.logger.info('Retrying launcher iframe load...');
597
+ setTimeout(() => { iframe.src = this.buildIframeUrl(launcher.url); }, 3000);
598
+ }
599
+ else {
600
+ this.config.onError?.(new Error('Failed to load widget launcher'));
601
+ }
602
+ };
620
603
  this.logger.debug('Launcher iframe created');
621
604
  }
622
605
  /**
623
606
  * Create widget iframe
624
607
  */
625
608
  async createWidgetIframe() {
609
+ // Guard: skip if widget iframe already exists
610
+ if (this.iframes.has(IframeType.WIDGET)) {
611
+ this.logger.debug('Widget iframe already exists, skipping creation');
612
+ return;
613
+ }
626
614
  const { iframes } = this.config;
627
615
  const { widget } = iframes;
628
616
  // Create container
@@ -703,14 +691,25 @@ class IframeManager {
703
691
  createdAt: Date.now(),
704
692
  });
705
693
  // When DOM loads, notify MessageBroker to send weld:init
694
+ let widgetRetried = false;
706
695
  iframe.onload = () => {
707
696
  const metadata = this.iframes.get(IframeType.WIDGET);
708
697
  if (metadata) {
709
698
  this.logger.debug('Widget iframe DOM loaded');
710
- // Notify MessageBroker that DOM is loaded (triggers weld:init)
711
699
  this.messageBroker?.setIframeDomLoaded(IframeType.WIDGET);
712
700
  }
713
701
  };
702
+ iframe.onerror = () => {
703
+ this.logger.error('Widget iframe failed to load');
704
+ if (!widgetRetried) {
705
+ widgetRetried = true;
706
+ this.logger.info('Retrying widget iframe load...');
707
+ setTimeout(() => { iframe.src = this.buildIframeUrl(widget.url); }, 3000);
708
+ }
709
+ else {
710
+ this.config.onError?.(new Error('Failed to load widget'));
711
+ }
712
+ };
714
713
  this.logger.debug('Widget iframe created');
715
714
  }
716
715
  /**
@@ -731,12 +730,21 @@ class IframeManager {
731
730
  url.searchParams.set('device', this.deviceInfo.type);
732
731
  url.searchParams.set('mobile', String(this.deviceInfo.isMobile));
733
732
  url.searchParams.set('parentOrigin', window.location.origin);
733
+ if (this.config.testMode) {
734
+ url.searchParams.set('testMode', 'true');
735
+ }
734
736
  return url.toString();
735
737
  }
736
738
  /**
737
739
  * Setup event listeners
738
740
  */
739
741
  setupEventListeners() {
742
+ // Guard: prevent double-binding
743
+ if (this.eventListenersBound) {
744
+ this.logger.debug('Event listeners already bound, skipping');
745
+ return;
746
+ }
747
+ this.eventListenersBound = true;
740
748
  // Window resize - use bound handler for proper cleanup
741
749
  window.addEventListener('resize', this.boundHandleResize);
742
750
  // Orientation change - use bound handler for proper cleanup
@@ -839,7 +847,7 @@ class IframeManager {
839
847
  iframe.container.style.transform = 'scale(1) translateY(0)';
840
848
  }
841
849
  // Handle mobile scroll lock
842
- if (this.deviceInfo.isMobile && type === IframeType.WIDGET && this.config.mobile.scrollLock) {
850
+ if (this.deviceInfo.isMobile && type === IframeType.WIDGET) {
843
851
  document.body.classList.add('weld-mobile-open');
844
852
  }
845
853
  // Hide launcher on mobile when widget is open (full-screen mode)
@@ -938,6 +946,8 @@ class IframeManager {
938
946
  this.iframes.clear();
939
947
  // Clear messageBroker reference
940
948
  this.messageBroker = null;
949
+ // Reset guard flag
950
+ this.eventListenersBound = false;
941
951
  this.logger.info('IframeManager destroyed');
942
952
  }
943
953
  }
@@ -1005,6 +1015,8 @@ var MessageType;
1005
1015
  // Events
1006
1016
  MessageType["EVENT_TRACK"] = "weld:event:track";
1007
1017
  MessageType["ERROR_REPORT"] = "weld:error:report";
1018
+ // Page tracking
1019
+ MessageType["PAGE_CHANGE"] = "weld:page:change";
1008
1020
  // API responses
1009
1021
  MessageType["API_SUCCESS"] = "weld:api:success";
1010
1022
  MessageType["API_ERROR"] = "weld:api:error";
@@ -1544,8 +1556,6 @@ class MessageBroker {
1544
1556
  iframeType,
1545
1557
  config: {
1546
1558
  api: this.config.api,
1547
- customization: this.config.customization,
1548
- features: this.config.features,
1549
1559
  },
1550
1560
  };
1551
1561
  const message = createMessage('weld:init', MessageOrigin.PARENT, initPayload);
@@ -2218,7 +2228,7 @@ class StateCoordinator {
2218
2228
  }
2219
2229
  }
2220
2230
 
2221
- var version = "1.0.16";
2231
+ var version = "1.0.17";
2222
2232
  var packageJson = {
2223
2233
  version: version};
2224
2234
 
@@ -2226,6 +2236,16 @@ var packageJson = {
2226
2236
  * Weld SDK - Main Entry Point
2227
2237
  * Public API for the Weld helpdesk widget
2228
2238
  */
2239
+ /**
2240
+ * Module-level singleton registry keyed by widgetId
2241
+ */
2242
+ const sdkRegistry = new Map();
2243
+ /**
2244
+ * SessionStorage key helpers
2245
+ */
2246
+ function openStateKey(widgetId) {
2247
+ return `weld-widget-open-${widgetId}`;
2248
+ }
2229
2249
  /**
2230
2250
  * SDK initialization status
2231
2251
  */
@@ -2248,6 +2268,8 @@ class WeldSDK {
2248
2268
  this.readyResolve = null;
2249
2269
  // Subscription IDs for cleanup
2250
2270
  this.subscriptionIds = [];
2271
+ // Page tracking cleanup
2272
+ this.pageTrackingCleanup = null;
2251
2273
  /**
2252
2274
  * Update user attributes (Intercom-style, with rate limiting)
2253
2275
  * Limited to 20 calls per page load to prevent abuse
@@ -2286,6 +2308,13 @@ class WeldSDK {
2286
2308
  console.log('[Weld SDK] Received message:', event.data.type);
2287
2309
  }
2288
2310
  if (event.data?.type === 'launcher:clicked') {
2311
+ if (this.status !== SDKStatus.READY) {
2312
+ console.log('[Weld SDK] Launcher clicked but SDK not ready yet — waiting...');
2313
+ this.readyPromise?.then(() => {
2314
+ this.handleLauncherClickMessage(event);
2315
+ });
2316
+ return;
2317
+ }
2289
2318
  // Toggle behavior - if widget is open, close it; if closed, open it
2290
2319
  const state = this.stateCoordinator.getState();
2291
2320
  if (state.widget.isOpen) {
@@ -2298,9 +2327,24 @@ class WeldSDK {
2298
2327
  }
2299
2328
  }
2300
2329
  if (event.data?.type === 'weld:close') {
2330
+ if (this.status !== SDKStatus.READY)
2331
+ return;
2301
2332
  console.log('[Weld SDK] Widget close requested');
2302
2333
  this.close();
2303
2334
  }
2335
+ if (event.data?.type === 'weld:unread-count') {
2336
+ const count = event.data.count ?? 0;
2337
+ // Forward to launcher iframe
2338
+ const launcherIframe = this.iframeManager.getIframe(IframeType.LAUNCHER);
2339
+ if (launcherIframe?.element?.contentWindow) {
2340
+ launcherIframe.element.contentWindow.postMessage({
2341
+ type: 'weld:unread-count',
2342
+ count
2343
+ }, '*');
2344
+ }
2345
+ // Update state coordinator for external API consumers
2346
+ this.stateCoordinator.setBadgeCount(count);
2347
+ }
2304
2348
  if (event.data?.type === 'weld:image:open' && event.data?.url) {
2305
2349
  this.showImageLightbox(event.data.url);
2306
2350
  }
@@ -2562,6 +2606,13 @@ class WeldSDK {
2562
2606
  this.logger.info('WeldSDK ready');
2563
2607
  // Call onReady callback
2564
2608
  this.config.onReady?.();
2609
+ // Start tracking page URL changes
2610
+ this.startPageTracking();
2611
+ // Auto-open if widget was previously open (persisted in sessionStorage)
2612
+ if (this.wasOpen()) {
2613
+ this.logger.info('Restoring previously open widget from sessionStorage');
2614
+ this.open();
2615
+ }
2565
2616
  }
2566
2617
  catch (error) {
2567
2618
  this.status = SDKStatus.ERROR;
@@ -2640,6 +2691,58 @@ class WeldSDK {
2640
2691
  isReady() {
2641
2692
  return this.status === SDKStatus.READY;
2642
2693
  }
2694
+ /**
2695
+ * Update callbacks on an existing instance (used by singleton reuse)
2696
+ */
2697
+ updateCallbacks(config) {
2698
+ if (config.onReady !== undefined)
2699
+ this.config.onReady = config.onReady;
2700
+ if (config.onOpen !== undefined)
2701
+ this.config.onOpen = config.onOpen;
2702
+ if (config.onClose !== undefined)
2703
+ this.config.onClose = config.onClose;
2704
+ if (config.onError !== undefined)
2705
+ this.config.onError = config.onError;
2706
+ if (config.onDestroy !== undefined)
2707
+ this.config.onDestroy = config.onDestroy;
2708
+ if (config.onMinimize !== undefined)
2709
+ this.config.onMinimize = config.onMinimize;
2710
+ if (config.onMaximize !== undefined)
2711
+ this.config.onMaximize = config.onMaximize;
2712
+ }
2713
+ /**
2714
+ * Persist open/closed state to sessionStorage
2715
+ */
2716
+ persistOpenState(isOpen) {
2717
+ try {
2718
+ sessionStorage.setItem(openStateKey(this.config.widgetId), isOpen ? 'true' : 'false');
2719
+ }
2720
+ catch {
2721
+ // sessionStorage might not be available
2722
+ }
2723
+ }
2724
+ /**
2725
+ * Clear persisted state from sessionStorage
2726
+ */
2727
+ clearPersistedState() {
2728
+ try {
2729
+ sessionStorage.removeItem(openStateKey(this.config.widgetId));
2730
+ }
2731
+ catch {
2732
+ // sessionStorage might not be available
2733
+ }
2734
+ }
2735
+ /**
2736
+ * Check if widget was previously open (from sessionStorage)
2737
+ */
2738
+ wasOpen() {
2739
+ try {
2740
+ return sessionStorage.getItem(openStateKey(this.config.widgetId)) === 'true';
2741
+ }
2742
+ catch {
2743
+ return false;
2744
+ }
2745
+ }
2643
2746
  /**
2644
2747
  * Open the widget
2645
2748
  */
@@ -2658,6 +2761,7 @@ class WeldSDK {
2658
2761
  if (launcherIframe?.element?.contentWindow) {
2659
2762
  launcherIframe.element.contentWindow.postMessage({ type: 'weld:widget-opened' }, '*');
2660
2763
  }
2764
+ this.persistOpenState(true);
2661
2765
  this.config.onOpen?.();
2662
2766
  }
2663
2767
  /**
@@ -2678,6 +2782,7 @@ class WeldSDK {
2678
2782
  if (launcherIframe?.element?.contentWindow) {
2679
2783
  launcherIframe.element.contentWindow.postMessage({ type: 'weld:widget-closed' }, '*');
2680
2784
  }
2785
+ this.persistOpenState(false);
2681
2786
  this.config.onClose?.();
2682
2787
  }
2683
2788
  /**
@@ -2866,6 +2971,8 @@ class WeldSDK {
2866
2971
  });
2867
2972
  // Broadcast logout to iframes
2868
2973
  this.messageBroker.broadcast('weld:auth:logout', {});
2974
+ // Clear persisted widget state
2975
+ this.clearPersistedState();
2869
2976
  // Clear any stored session data
2870
2977
  try {
2871
2978
  const prefix = 'weld-';
@@ -3007,6 +3114,63 @@ class WeldSDK {
3007
3114
  this.logger.setLevel('warn');
3008
3115
  this.logger.info('Debug mode disabled');
3009
3116
  }
3117
+ /**
3118
+ * Send a page change message to the widget iframe
3119
+ */
3120
+ sendPageChange(url, title) {
3121
+ const widgetIframe = this.iframeManager.getIframe(IframeType.WIDGET);
3122
+ if (widgetIframe?.element?.contentWindow) {
3123
+ widgetIframe.element.contentWindow.postMessage({
3124
+ type: 'weld:page:change',
3125
+ url,
3126
+ title,
3127
+ timestamp: Date.now(),
3128
+ }, '*');
3129
+ }
3130
+ }
3131
+ /**
3132
+ * Start tracking page URL changes (SPA navigations + popstate)
3133
+ */
3134
+ startPageTracking() {
3135
+ let lastUrl = window.location.href;
3136
+ let debounceTimer = null;
3137
+ const notifyChange = () => {
3138
+ const currentUrl = window.location.href;
3139
+ if (currentUrl !== lastUrl) {
3140
+ lastUrl = currentUrl;
3141
+ this.sendPageChange(currentUrl, document.title);
3142
+ }
3143
+ };
3144
+ const debouncedNotify = () => {
3145
+ if (debounceTimer)
3146
+ clearTimeout(debounceTimer);
3147
+ debounceTimer = setTimeout(notifyChange, 300);
3148
+ };
3149
+ // Send initial page
3150
+ this.sendPageChange(window.location.href, document.title);
3151
+ // Monkey-patch history.pushState and history.replaceState
3152
+ const origPushState = history.pushState.bind(history);
3153
+ const origReplaceState = history.replaceState.bind(history);
3154
+ history.pushState = function (...args) {
3155
+ origPushState(...args);
3156
+ debouncedNotify();
3157
+ };
3158
+ history.replaceState = function (...args) {
3159
+ origReplaceState(...args);
3160
+ debouncedNotify();
3161
+ };
3162
+ // Listen for popstate (browser back/forward)
3163
+ const handlePopstate = () => debouncedNotify();
3164
+ window.addEventListener('popstate', handlePopstate);
3165
+ // Store cleanup
3166
+ this.pageTrackingCleanup = () => {
3167
+ if (debounceTimer)
3168
+ clearTimeout(debounceTimer);
3169
+ window.removeEventListener('popstate', handlePopstate);
3170
+ history.pushState = origPushState;
3171
+ history.replaceState = origReplaceState;
3172
+ };
3173
+ }
3010
3174
  /**
3011
3175
  * Ensure SDK is ready before operation
3012
3176
  */
@@ -3015,11 +3179,26 @@ class WeldSDK {
3015
3179
  throw new Error('SDK not ready. Call init() first.');
3016
3180
  }
3017
3181
  }
3182
+ /**
3183
+ * Detach from the current component lifecycle without destroying the widget.
3184
+ * Use this as a React useEffect cleanup — the widget stays alive across navigations.
3185
+ */
3186
+ detach() {
3187
+ // No-op: widget stays alive in the singleton registry
3188
+ this.logger.debug('WeldSDK detached (no-op, widget stays alive)');
3189
+ }
3018
3190
  /**
3019
3191
  * Destroy SDK and cleanup
3020
3192
  */
3021
3193
  destroy() {
3022
3194
  this.logger.info('Destroying WeldSDK');
3195
+ // Remove from singleton registry
3196
+ sdkRegistry.delete(this.config.widgetId);
3197
+ // Clear persisted state
3198
+ this.clearPersistedState();
3199
+ // Stop page tracking
3200
+ this.pageTrackingCleanup?.();
3201
+ this.pageTrackingCleanup = null;
3023
3202
  // Remove event listener using bound handler
3024
3203
  window.removeEventListener('message', this.boundHandleLauncherClick);
3025
3204
  // Unsubscribe from all message broker subscriptions