@weldsuite/helpdesk-widget-sdk 1.0.15 → 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 +36 -41
- package/dist/angular.esm.js +483 -102
- package/dist/angular.esm.js.map +1 -1
- package/dist/angular.js +483 -102
- package/dist/angular.js.map +1 -1
- package/dist/index.d.ts +60 -56
- package/dist/index.esm.js +505 -104
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +506 -103
- package/dist/index.js.map +1 -1
- package/dist/index.umd.js +506 -103
- package/dist/index.umd.js.map +1 -1
- package/dist/react.d.ts +36 -41
- package/dist/react.esm.js +483 -102
- package/dist/react.esm.js.map +1 -1
- package/dist/react.js +483 -102
- package/dist/react.js.map +1 -1
- package/dist/vue-composables.esm.js +483 -102
- package/dist/vue-composables.esm.js.map +1 -1
- package/dist/vue-composables.js +483 -102
- package/dist/vue-composables.js.map +1 -1
- package/package.json +4 -4
package/dist/angular.esm.js
CHANGED
|
@@ -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
|
-
|
|
490
|
-
let existingContainer = document.getElementById('weld-container');
|
|
455
|
+
const existingContainer = document.getElementById('weld-container');
|
|
491
456
|
if (existingContainer) {
|
|
492
|
-
|
|
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,20 +528,27 @@ 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
|
|
570
539
|
const container = document.createElement('div');
|
|
571
540
|
container.className = 'weld-launcher-frame';
|
|
572
541
|
container.setAttribute('data-state', 'visible');
|
|
542
|
+
// Container is larger than the button to allow hover animations (scale, shadow) without clipping
|
|
543
|
+
const launcherPadding = 10;
|
|
573
544
|
container.style.cssText = `
|
|
574
545
|
position: fixed;
|
|
575
|
-
bottom: ${launcher.position.bottom};
|
|
576
|
-
right: ${launcher.position.right};
|
|
577
|
-
width: ${launcher.size};
|
|
578
|
-
height: ${launcher.size};
|
|
546
|
+
bottom: calc(${launcher.position.bottom} - ${launcherPadding}px);
|
|
547
|
+
right: calc(${launcher.position.right} - ${launcherPadding}px);
|
|
548
|
+
width: calc(${launcher.size} + ${launcherPadding * 2}px);
|
|
549
|
+
height: calc(${launcher.size} + ${launcherPadding * 2}px);
|
|
579
550
|
z-index: 2147483003;
|
|
580
|
-
pointer-events:
|
|
551
|
+
pointer-events: none;
|
|
581
552
|
display: block;
|
|
582
553
|
`;
|
|
583
554
|
// Create iframe
|
|
@@ -589,8 +560,12 @@ class IframeManager {
|
|
|
589
560
|
width: 100%;
|
|
590
561
|
height: 100%;
|
|
591
562
|
border: none;
|
|
592
|
-
background:
|
|
563
|
+
background: none;
|
|
564
|
+
color-scheme: none;
|
|
593
565
|
display: block;
|
|
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);
|
|
594
569
|
`;
|
|
595
570
|
iframe.setAttribute('allow', 'clipboard-write');
|
|
596
571
|
iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-forms allow-popups');
|
|
@@ -606,20 +581,36 @@ class IframeManager {
|
|
|
606
581
|
createdAt: Date.now(),
|
|
607
582
|
});
|
|
608
583
|
// When DOM loads, notify MessageBroker to send weld:init
|
|
584
|
+
let launcherRetried = false;
|
|
609
585
|
iframe.onload = () => {
|
|
610
586
|
const metadata = this.iframes.get(IframeType.LAUNCHER);
|
|
611
587
|
if (metadata) {
|
|
612
588
|
this.logger.debug('Launcher iframe DOM loaded');
|
|
613
|
-
// Notify MessageBroker that DOM is loaded (triggers weld:init)
|
|
614
589
|
this.messageBroker?.setIframeDomLoaded(IframeType.LAUNCHER);
|
|
615
590
|
}
|
|
616
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
|
+
};
|
|
617
603
|
this.logger.debug('Launcher iframe created');
|
|
618
604
|
}
|
|
619
605
|
/**
|
|
620
606
|
* Create widget iframe
|
|
621
607
|
*/
|
|
622
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
|
+
}
|
|
623
614
|
const { iframes } = this.config;
|
|
624
615
|
const { widget } = iframes;
|
|
625
616
|
// Create container
|
|
@@ -700,54 +691,32 @@ class IframeManager {
|
|
|
700
691
|
createdAt: Date.now(),
|
|
701
692
|
});
|
|
702
693
|
// When DOM loads, notify MessageBroker to send weld:init
|
|
694
|
+
let widgetRetried = false;
|
|
703
695
|
iframe.onload = () => {
|
|
704
696
|
const metadata = this.iframes.get(IframeType.WIDGET);
|
|
705
697
|
if (metadata) {
|
|
706
698
|
this.logger.debug('Widget iframe DOM loaded');
|
|
707
|
-
// Notify MessageBroker that DOM is loaded (triggers weld:init)
|
|
708
699
|
this.messageBroker?.setIframeDomLoaded(IframeType.WIDGET);
|
|
709
700
|
}
|
|
710
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
|
+
};
|
|
711
713
|
this.logger.debug('Widget iframe created');
|
|
712
714
|
}
|
|
713
715
|
/**
|
|
714
|
-
* Create backdrop iframe
|
|
716
|
+
* Create backdrop iframe — disabled, widget stays non-modal so users can interact with the page
|
|
715
717
|
*/
|
|
716
718
|
async createBackdropIframe() {
|
|
717
|
-
|
|
718
|
-
this.logger.debug('Backdrop disabled, skipping creation');
|
|
719
|
-
return;
|
|
720
|
-
}
|
|
721
|
-
// Create container
|
|
722
|
-
const container = document.createElement('div');
|
|
723
|
-
container.className = 'weld-backdrop-frame';
|
|
724
|
-
container.setAttribute('data-state', 'hidden');
|
|
725
|
-
container.style.cssText = `
|
|
726
|
-
position: fixed;
|
|
727
|
-
top: 0;
|
|
728
|
-
left: 0;
|
|
729
|
-
right: 0;
|
|
730
|
-
bottom: 0;
|
|
731
|
-
z-index: 2147483000;
|
|
732
|
-
background: transparent;
|
|
733
|
-
pointer-events: none;
|
|
734
|
-
opacity: 0;
|
|
735
|
-
transition: opacity 200ms ease;
|
|
736
|
-
`;
|
|
737
|
-
this.appContainer?.appendChild(container);
|
|
738
|
-
// Store metadata (backdrop doesn't have an iframe, just a div)
|
|
739
|
-
// We'll create a minimal "iframe" reference for consistency
|
|
740
|
-
const dummyIframe = document.createElement('iframe');
|
|
741
|
-
dummyIframe.style.display = 'none';
|
|
742
|
-
this.iframes.set(IframeType.BACKDROP, {
|
|
743
|
-
type: IframeType.BACKDROP,
|
|
744
|
-
element: dummyIframe,
|
|
745
|
-
container,
|
|
746
|
-
ready: true, // Backdrop is always ready
|
|
747
|
-
visible: false,
|
|
748
|
-
createdAt: Date.now(),
|
|
749
|
-
});
|
|
750
|
-
this.logger.debug('Backdrop created');
|
|
719
|
+
this.logger.debug('Backdrop disabled, skipping creation');
|
|
751
720
|
}
|
|
752
721
|
/**
|
|
753
722
|
* Build iframe URL with parameters
|
|
@@ -761,12 +730,21 @@ class IframeManager {
|
|
|
761
730
|
url.searchParams.set('device', this.deviceInfo.type);
|
|
762
731
|
url.searchParams.set('mobile', String(this.deviceInfo.isMobile));
|
|
763
732
|
url.searchParams.set('parentOrigin', window.location.origin);
|
|
733
|
+
if (this.config.testMode) {
|
|
734
|
+
url.searchParams.set('testMode', 'true');
|
|
735
|
+
}
|
|
764
736
|
return url.toString();
|
|
765
737
|
}
|
|
766
738
|
/**
|
|
767
739
|
* Setup event listeners
|
|
768
740
|
*/
|
|
769
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;
|
|
770
748
|
// Window resize - use bound handler for proper cleanup
|
|
771
749
|
window.addEventListener('resize', this.boundHandleResize);
|
|
772
750
|
// Orientation change - use bound handler for proper cleanup
|
|
@@ -869,7 +847,7 @@ class IframeManager {
|
|
|
869
847
|
iframe.container.style.transform = 'scale(1) translateY(0)';
|
|
870
848
|
}
|
|
871
849
|
// Handle mobile scroll lock
|
|
872
|
-
if (this.deviceInfo.isMobile && type === IframeType.WIDGET
|
|
850
|
+
if (this.deviceInfo.isMobile && type === IframeType.WIDGET) {
|
|
873
851
|
document.body.classList.add('weld-mobile-open');
|
|
874
852
|
}
|
|
875
853
|
// Hide launcher on mobile when widget is open (full-screen mode)
|
|
@@ -968,6 +946,8 @@ class IframeManager {
|
|
|
968
946
|
this.iframes.clear();
|
|
969
947
|
// Clear messageBroker reference
|
|
970
948
|
this.messageBroker = null;
|
|
949
|
+
// Reset guard flag
|
|
950
|
+
this.eventListenersBound = false;
|
|
971
951
|
this.logger.info('IframeManager destroyed');
|
|
972
952
|
}
|
|
973
953
|
}
|
|
@@ -1035,6 +1015,8 @@ var MessageType;
|
|
|
1035
1015
|
// Events
|
|
1036
1016
|
MessageType["EVENT_TRACK"] = "weld:event:track";
|
|
1037
1017
|
MessageType["ERROR_REPORT"] = "weld:error:report";
|
|
1018
|
+
// Page tracking
|
|
1019
|
+
MessageType["PAGE_CHANGE"] = "weld:page:change";
|
|
1038
1020
|
// API responses
|
|
1039
1021
|
MessageType["API_SUCCESS"] = "weld:api:success";
|
|
1040
1022
|
MessageType["API_ERROR"] = "weld:api:error";
|
|
@@ -1574,8 +1556,6 @@ class MessageBroker {
|
|
|
1574
1556
|
iframeType,
|
|
1575
1557
|
config: {
|
|
1576
1558
|
api: this.config.api,
|
|
1577
|
-
customization: this.config.customization,
|
|
1578
|
-
features: this.config.features,
|
|
1579
1559
|
},
|
|
1580
1560
|
};
|
|
1581
1561
|
const message = createMessage('weld:init', MessageOrigin.PARENT, initPayload);
|
|
@@ -2248,7 +2228,7 @@ class StateCoordinator {
|
|
|
2248
2228
|
}
|
|
2249
2229
|
}
|
|
2250
2230
|
|
|
2251
|
-
var version = "1.0.
|
|
2231
|
+
var version = "1.0.17";
|
|
2252
2232
|
var packageJson = {
|
|
2253
2233
|
version: version};
|
|
2254
2234
|
|
|
@@ -2256,6 +2236,16 @@ var packageJson = {
|
|
|
2256
2236
|
* Weld SDK - Main Entry Point
|
|
2257
2237
|
* Public API for the Weld helpdesk widget
|
|
2258
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
|
+
}
|
|
2259
2249
|
/**
|
|
2260
2250
|
* SDK initialization status
|
|
2261
2251
|
*/
|
|
@@ -2278,6 +2268,8 @@ class WeldSDK {
|
|
|
2278
2268
|
this.readyResolve = null;
|
|
2279
2269
|
// Subscription IDs for cleanup
|
|
2280
2270
|
this.subscriptionIds = [];
|
|
2271
|
+
// Page tracking cleanup
|
|
2272
|
+
this.pageTrackingCleanup = null;
|
|
2281
2273
|
/**
|
|
2282
2274
|
* Update user attributes (Intercom-style, with rate limiting)
|
|
2283
2275
|
* Limited to 20 calls per page load to prevent abuse
|
|
@@ -2316,6 +2308,13 @@ class WeldSDK {
|
|
|
2316
2308
|
console.log('[Weld SDK] Received message:', event.data.type);
|
|
2317
2309
|
}
|
|
2318
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
|
+
}
|
|
2319
2318
|
// Toggle behavior - if widget is open, close it; if closed, open it
|
|
2320
2319
|
const state = this.stateCoordinator.getState();
|
|
2321
2320
|
if (state.widget.isOpen) {
|
|
@@ -2328,9 +2327,260 @@ class WeldSDK {
|
|
|
2328
2327
|
}
|
|
2329
2328
|
}
|
|
2330
2329
|
if (event.data?.type === 'weld:close') {
|
|
2330
|
+
if (this.status !== SDKStatus.READY)
|
|
2331
|
+
return;
|
|
2331
2332
|
console.log('[Weld SDK] Widget close requested');
|
|
2332
2333
|
this.close();
|
|
2333
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
|
+
}
|
|
2348
|
+
if (event.data?.type === 'weld:image:open' && event.data?.url) {
|
|
2349
|
+
this.showImageLightbox(event.data.url);
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
/**
|
|
2353
|
+
* Show fullscreen image lightbox on the parent page
|
|
2354
|
+
*/
|
|
2355
|
+
showImageLightbox(url) {
|
|
2356
|
+
// Remove existing lightbox if any
|
|
2357
|
+
const existing = document.getElementById('weld-image-lightbox');
|
|
2358
|
+
if (existing)
|
|
2359
|
+
existing.remove();
|
|
2360
|
+
// Zoom / pan state
|
|
2361
|
+
let scale = 1;
|
|
2362
|
+
let translateX = 0;
|
|
2363
|
+
let translateY = 0;
|
|
2364
|
+
let isDragging = false;
|
|
2365
|
+
let dragStartX = 0;
|
|
2366
|
+
let dragStartY = 0;
|
|
2367
|
+
let lastTranslateX = 0;
|
|
2368
|
+
let lastTranslateY = 0;
|
|
2369
|
+
const applyTransform = () => {
|
|
2370
|
+
img.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;
|
|
2371
|
+
};
|
|
2372
|
+
const resetTransform = () => {
|
|
2373
|
+
scale = 1;
|
|
2374
|
+
translateX = 0;
|
|
2375
|
+
translateY = 0;
|
|
2376
|
+
applyTransform();
|
|
2377
|
+
img.style.cursor = 'zoom-in';
|
|
2378
|
+
};
|
|
2379
|
+
const overlay = document.createElement('div');
|
|
2380
|
+
overlay.id = 'weld-image-lightbox';
|
|
2381
|
+
overlay.style.cssText = `
|
|
2382
|
+
position: fixed;
|
|
2383
|
+
inset: 0;
|
|
2384
|
+
z-index: 2147483647;
|
|
2385
|
+
background: rgba(0, 0, 0, 0.92);
|
|
2386
|
+
display: flex;
|
|
2387
|
+
align-items: center;
|
|
2388
|
+
justify-content: center;
|
|
2389
|
+
padding: 16px;
|
|
2390
|
+
cursor: pointer;
|
|
2391
|
+
overflow: hidden;
|
|
2392
|
+
`;
|
|
2393
|
+
// Close button
|
|
2394
|
+
const closeBtn = document.createElement('button');
|
|
2395
|
+
closeBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>`;
|
|
2396
|
+
closeBtn.style.cssText = `
|
|
2397
|
+
position: absolute;
|
|
2398
|
+
top: 16px;
|
|
2399
|
+
right: 16px;
|
|
2400
|
+
width: 40px;
|
|
2401
|
+
height: 40px;
|
|
2402
|
+
border-radius: 50%;
|
|
2403
|
+
border: none;
|
|
2404
|
+
background: rgba(255, 255, 255, 0.1);
|
|
2405
|
+
color: white;
|
|
2406
|
+
cursor: pointer;
|
|
2407
|
+
display: flex;
|
|
2408
|
+
align-items: center;
|
|
2409
|
+
justify-content: center;
|
|
2410
|
+
transition: background 0.15s;
|
|
2411
|
+
`;
|
|
2412
|
+
closeBtn.onmouseenter = () => { closeBtn.style.background = 'rgba(255, 255, 255, 0.2)'; };
|
|
2413
|
+
closeBtn.onmouseleave = () => { closeBtn.style.background = 'rgba(255, 255, 255, 0.1)'; };
|
|
2414
|
+
// Download button
|
|
2415
|
+
const downloadBtn = document.createElement('a');
|
|
2416
|
+
downloadBtn.href = url;
|
|
2417
|
+
downloadBtn.download = '';
|
|
2418
|
+
downloadBtn.target = '_blank';
|
|
2419
|
+
downloadBtn.rel = 'noopener noreferrer';
|
|
2420
|
+
downloadBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>`;
|
|
2421
|
+
downloadBtn.style.cssText = `
|
|
2422
|
+
position: absolute;
|
|
2423
|
+
top: 16px;
|
|
2424
|
+
right: 64px;
|
|
2425
|
+
width: 40px;
|
|
2426
|
+
height: 40px;
|
|
2427
|
+
border-radius: 50%;
|
|
2428
|
+
border: none;
|
|
2429
|
+
background: rgba(255, 255, 255, 0.1);
|
|
2430
|
+
color: white;
|
|
2431
|
+
cursor: pointer;
|
|
2432
|
+
display: flex;
|
|
2433
|
+
align-items: center;
|
|
2434
|
+
justify-content: center;
|
|
2435
|
+
transition: background 0.15s;
|
|
2436
|
+
text-decoration: none;
|
|
2437
|
+
`;
|
|
2438
|
+
downloadBtn.onmouseenter = () => { downloadBtn.style.background = 'rgba(255, 255, 255, 0.2)'; };
|
|
2439
|
+
downloadBtn.onmouseleave = () => { downloadBtn.style.background = 'rgba(255, 255, 255, 0.1)'; };
|
|
2440
|
+
// Image
|
|
2441
|
+
const img = document.createElement('img');
|
|
2442
|
+
img.src = url;
|
|
2443
|
+
img.alt = 'Full size';
|
|
2444
|
+
img.draggable = false;
|
|
2445
|
+
img.style.cssText = `
|
|
2446
|
+
max-width: 100%;
|
|
2447
|
+
max-height: 100%;
|
|
2448
|
+
object-fit: contain;
|
|
2449
|
+
border-radius: 8px;
|
|
2450
|
+
cursor: zoom-in;
|
|
2451
|
+
transition: transform 0.2s ease;
|
|
2452
|
+
user-select: none;
|
|
2453
|
+
`;
|
|
2454
|
+
// Click to toggle zoom
|
|
2455
|
+
img.addEventListener('click', (e) => {
|
|
2456
|
+
e.stopPropagation();
|
|
2457
|
+
if (scale === 1) {
|
|
2458
|
+
// Zoom in to 2.5x centered on click position
|
|
2459
|
+
const rect = img.getBoundingClientRect();
|
|
2460
|
+
const clickX = e.clientX - rect.left - rect.width / 2;
|
|
2461
|
+
const clickY = e.clientY - rect.top - rect.height / 2;
|
|
2462
|
+
scale = 2.5;
|
|
2463
|
+
translateX = -clickX * 1.5;
|
|
2464
|
+
translateY = -clickY * 1.5;
|
|
2465
|
+
applyTransform();
|
|
2466
|
+
img.style.cursor = 'zoom-out';
|
|
2467
|
+
}
|
|
2468
|
+
else {
|
|
2469
|
+
// Zoom out - reset
|
|
2470
|
+
resetTransform();
|
|
2471
|
+
}
|
|
2472
|
+
});
|
|
2473
|
+
// Mouse wheel zoom
|
|
2474
|
+
overlay.addEventListener('wheel', (e) => {
|
|
2475
|
+
e.preventDefault();
|
|
2476
|
+
const delta = e.deltaY > 0 ? -0.25 : 0.25;
|
|
2477
|
+
const newScale = Math.min(Math.max(scale + delta, 1), 5);
|
|
2478
|
+
if (newScale === 1) {
|
|
2479
|
+
resetTransform();
|
|
2480
|
+
}
|
|
2481
|
+
else {
|
|
2482
|
+
scale = newScale;
|
|
2483
|
+
applyTransform();
|
|
2484
|
+
img.style.cursor = 'zoom-out';
|
|
2485
|
+
}
|
|
2486
|
+
}, { passive: false });
|
|
2487
|
+
// Drag to pan when zoomed
|
|
2488
|
+
img.addEventListener('mousedown', (e) => {
|
|
2489
|
+
if (scale <= 1)
|
|
2490
|
+
return;
|
|
2491
|
+
e.preventDefault();
|
|
2492
|
+
isDragging = true;
|
|
2493
|
+
dragStartX = e.clientX;
|
|
2494
|
+
dragStartY = e.clientY;
|
|
2495
|
+
lastTranslateX = translateX;
|
|
2496
|
+
lastTranslateY = translateY;
|
|
2497
|
+
img.style.cursor = 'grabbing';
|
|
2498
|
+
img.style.transition = 'none';
|
|
2499
|
+
});
|
|
2500
|
+
window.addEventListener('mousemove', (e) => {
|
|
2501
|
+
if (!isDragging)
|
|
2502
|
+
return;
|
|
2503
|
+
translateX = lastTranslateX + (e.clientX - dragStartX);
|
|
2504
|
+
translateY = lastTranslateY + (e.clientY - dragStartY);
|
|
2505
|
+
applyTransform();
|
|
2506
|
+
});
|
|
2507
|
+
window.addEventListener('mouseup', () => {
|
|
2508
|
+
if (!isDragging)
|
|
2509
|
+
return;
|
|
2510
|
+
isDragging = false;
|
|
2511
|
+
img.style.cursor = scale > 1 ? 'zoom-out' : 'zoom-in';
|
|
2512
|
+
img.style.transition = 'transform 0.2s ease';
|
|
2513
|
+
});
|
|
2514
|
+
// Touch: pinch to zoom + drag to pan
|
|
2515
|
+
let lastTouchDist = 0;
|
|
2516
|
+
let lastTouchScale = 1;
|
|
2517
|
+
overlay.addEventListener('touchstart', (e) => {
|
|
2518
|
+
if (e.touches.length === 2) {
|
|
2519
|
+
e.preventDefault();
|
|
2520
|
+
const dx = e.touches[0].clientX - e.touches[1].clientX;
|
|
2521
|
+
const dy = e.touches[0].clientY - e.touches[1].clientY;
|
|
2522
|
+
lastTouchDist = Math.hypot(dx, dy);
|
|
2523
|
+
lastTouchScale = scale;
|
|
2524
|
+
}
|
|
2525
|
+
else if (e.touches.length === 1 && scale > 1) {
|
|
2526
|
+
isDragging = true;
|
|
2527
|
+
dragStartX = e.touches[0].clientX;
|
|
2528
|
+
dragStartY = e.touches[0].clientY;
|
|
2529
|
+
lastTranslateX = translateX;
|
|
2530
|
+
lastTranslateY = translateY;
|
|
2531
|
+
img.style.transition = 'none';
|
|
2532
|
+
}
|
|
2533
|
+
}, { passive: false });
|
|
2534
|
+
overlay.addEventListener('touchmove', (e) => {
|
|
2535
|
+
if (e.touches.length === 2) {
|
|
2536
|
+
e.preventDefault();
|
|
2537
|
+
const dx = e.touches[0].clientX - e.touches[1].clientX;
|
|
2538
|
+
const dy = e.touches[0].clientY - e.touches[1].clientY;
|
|
2539
|
+
const dist = Math.hypot(dx, dy);
|
|
2540
|
+
scale = Math.min(Math.max(lastTouchScale * (dist / lastTouchDist), 1), 5);
|
|
2541
|
+
if (scale === 1) {
|
|
2542
|
+
translateX = 0;
|
|
2543
|
+
translateY = 0;
|
|
2544
|
+
}
|
|
2545
|
+
applyTransform();
|
|
2546
|
+
}
|
|
2547
|
+
else if (e.touches.length === 1 && isDragging) {
|
|
2548
|
+
e.preventDefault();
|
|
2549
|
+
translateX = lastTranslateX + (e.touches[0].clientX - dragStartX);
|
|
2550
|
+
translateY = lastTranslateY + (e.touches[0].clientY - dragStartY);
|
|
2551
|
+
applyTransform();
|
|
2552
|
+
}
|
|
2553
|
+
}, { passive: false });
|
|
2554
|
+
overlay.addEventListener('touchend', (e) => {
|
|
2555
|
+
if (e.touches.length < 2) {
|
|
2556
|
+
lastTouchDist = 0;
|
|
2557
|
+
}
|
|
2558
|
+
if (e.touches.length === 0) {
|
|
2559
|
+
isDragging = false;
|
|
2560
|
+
img.style.transition = 'transform 0.2s ease';
|
|
2561
|
+
}
|
|
2562
|
+
});
|
|
2563
|
+
const close = () => {
|
|
2564
|
+
document.removeEventListener('keydown', handleKeyDown);
|
|
2565
|
+
overlay.remove();
|
|
2566
|
+
};
|
|
2567
|
+
// Only close on backdrop click when not zoomed (prevent accidental close while panning)
|
|
2568
|
+
overlay.addEventListener('click', (e) => {
|
|
2569
|
+
if (e.target === overlay && scale <= 1)
|
|
2570
|
+
close();
|
|
2571
|
+
});
|
|
2572
|
+
downloadBtn.addEventListener('click', (e) => e.stopPropagation());
|
|
2573
|
+
closeBtn.addEventListener('click', (e) => { e.stopPropagation(); close(); });
|
|
2574
|
+
// Close on Escape
|
|
2575
|
+
const handleKeyDown = (e) => {
|
|
2576
|
+
if (e.key === 'Escape')
|
|
2577
|
+
close();
|
|
2578
|
+
};
|
|
2579
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
2580
|
+
overlay.appendChild(closeBtn);
|
|
2581
|
+
overlay.appendChild(downloadBtn);
|
|
2582
|
+
overlay.appendChild(img);
|
|
2583
|
+
document.body.appendChild(overlay);
|
|
2334
2584
|
}
|
|
2335
2585
|
/**
|
|
2336
2586
|
* Initialize the SDK and render widget
|
|
@@ -2356,6 +2606,13 @@ class WeldSDK {
|
|
|
2356
2606
|
this.logger.info('WeldSDK ready');
|
|
2357
2607
|
// Call onReady callback
|
|
2358
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
|
+
}
|
|
2359
2616
|
}
|
|
2360
2617
|
catch (error) {
|
|
2361
2618
|
this.status = SDKStatus.ERROR;
|
|
@@ -2434,6 +2691,58 @@ class WeldSDK {
|
|
|
2434
2691
|
isReady() {
|
|
2435
2692
|
return this.status === SDKStatus.READY;
|
|
2436
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
|
+
}
|
|
2437
2746
|
/**
|
|
2438
2747
|
* Open the widget
|
|
2439
2748
|
*/
|
|
@@ -2442,8 +2751,6 @@ class WeldSDK {
|
|
|
2442
2751
|
console.log('[Weld SDK] Opening widget...');
|
|
2443
2752
|
this.stateCoordinator.openWidget();
|
|
2444
2753
|
this.iframeManager.showIframe(IframeType.WIDGET);
|
|
2445
|
-
this.iframeManager.showIframe(IframeType.BACKDROP);
|
|
2446
|
-
// Keep launcher visible so user can click it to close the widget
|
|
2447
2754
|
// Send open message to the widget iframe
|
|
2448
2755
|
const widgetIframe = this.iframeManager.getIframe(IframeType.WIDGET);
|
|
2449
2756
|
if (widgetIframe?.element?.contentWindow) {
|
|
@@ -2454,6 +2761,7 @@ class WeldSDK {
|
|
|
2454
2761
|
if (launcherIframe?.element?.contentWindow) {
|
|
2455
2762
|
launcherIframe.element.contentWindow.postMessage({ type: 'weld:widget-opened' }, '*');
|
|
2456
2763
|
}
|
|
2764
|
+
this.persistOpenState(true);
|
|
2457
2765
|
this.config.onOpen?.();
|
|
2458
2766
|
}
|
|
2459
2767
|
/**
|
|
@@ -2464,8 +2772,6 @@ class WeldSDK {
|
|
|
2464
2772
|
console.log('[Weld SDK] Closing widget...');
|
|
2465
2773
|
this.stateCoordinator.closeWidget();
|
|
2466
2774
|
this.iframeManager.hideIframe(IframeType.WIDGET);
|
|
2467
|
-
this.iframeManager.hideIframe(IframeType.BACKDROP);
|
|
2468
|
-
// Launcher stays visible
|
|
2469
2775
|
// Send close message to the widget iframe
|
|
2470
2776
|
const widgetIframe = this.iframeManager.getIframe(IframeType.WIDGET);
|
|
2471
2777
|
if (widgetIframe?.element?.contentWindow) {
|
|
@@ -2476,6 +2782,7 @@ class WeldSDK {
|
|
|
2476
2782
|
if (launcherIframe?.element?.contentWindow) {
|
|
2477
2783
|
launcherIframe.element.contentWindow.postMessage({ type: 'weld:widget-closed' }, '*');
|
|
2478
2784
|
}
|
|
2785
|
+
this.persistOpenState(false);
|
|
2479
2786
|
this.config.onClose?.();
|
|
2480
2787
|
}
|
|
2481
2788
|
/**
|
|
@@ -2664,6 +2971,8 @@ class WeldSDK {
|
|
|
2664
2971
|
});
|
|
2665
2972
|
// Broadcast logout to iframes
|
|
2666
2973
|
this.messageBroker.broadcast('weld:auth:logout', {});
|
|
2974
|
+
// Clear persisted widget state
|
|
2975
|
+
this.clearPersistedState();
|
|
2667
2976
|
// Clear any stored session data
|
|
2668
2977
|
try {
|
|
2669
2978
|
const prefix = 'weld-';
|
|
@@ -2805,6 +3114,63 @@ class WeldSDK {
|
|
|
2805
3114
|
this.logger.setLevel('warn');
|
|
2806
3115
|
this.logger.info('Debug mode disabled');
|
|
2807
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
|
+
}
|
|
2808
3174
|
/**
|
|
2809
3175
|
* Ensure SDK is ready before operation
|
|
2810
3176
|
*/
|
|
@@ -2813,11 +3179,26 @@ class WeldSDK {
|
|
|
2813
3179
|
throw new Error('SDK not ready. Call init() first.');
|
|
2814
3180
|
}
|
|
2815
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
|
+
}
|
|
2816
3190
|
/**
|
|
2817
3191
|
* Destroy SDK and cleanup
|
|
2818
3192
|
*/
|
|
2819
3193
|
destroy() {
|
|
2820
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;
|
|
2821
3202
|
// Remove event listener using bound handler
|
|
2822
3203
|
window.removeEventListener('message', this.boundHandleLauncherClick);
|
|
2823
3204
|
// Unsubscribe from all message broker subscriptions
|