@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.js
CHANGED
|
@@ -95,32 +95,6 @@ const DEFAULT_CONFIG = {
|
|
|
95
95
|
closeOnClick: true,
|
|
96
96
|
},
|
|
97
97
|
},
|
|
98
|
-
customization: {
|
|
99
|
-
primaryColor: '#000000',
|
|
100
|
-
accentColor: '#3b82f6',
|
|
101
|
-
backgroundColor: '#ffffff',
|
|
102
|
-
textColor: '#111827',
|
|
103
|
-
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
104
|
-
fontSize: '14px',
|
|
105
|
-
borderRadius: '12px',
|
|
106
|
-
},
|
|
107
|
-
features: {
|
|
108
|
-
attachments: true,
|
|
109
|
-
reactions: true,
|
|
110
|
-
typing: true,
|
|
111
|
-
readReceipts: true,
|
|
112
|
-
offlineMode: false,
|
|
113
|
-
fileUpload: true,
|
|
114
|
-
imageUpload: true,
|
|
115
|
-
voiceMessages: false,
|
|
116
|
-
videoMessages: false,
|
|
117
|
-
},
|
|
118
|
-
mobile: {
|
|
119
|
-
fullScreen: true,
|
|
120
|
-
scrollLock: true,
|
|
121
|
-
keyboardHandling: 'auto',
|
|
122
|
-
safeAreaInsets: true,
|
|
123
|
-
},
|
|
124
98
|
auth: {
|
|
125
99
|
enabled: true,
|
|
126
100
|
mode: 'anonymous',
|
|
@@ -164,6 +138,7 @@ function resolveConfig(config) {
|
|
|
164
138
|
validateConfig(config);
|
|
165
139
|
return {
|
|
166
140
|
widgetId: config.widgetId,
|
|
141
|
+
testMode: config.testMode,
|
|
167
142
|
api: {
|
|
168
143
|
...DEFAULT_CONFIG.api,
|
|
169
144
|
widgetId: config.widgetId,
|
|
@@ -193,18 +168,6 @@ function resolveConfig(config) {
|
|
|
193
168
|
...config.iframes?.backdrop,
|
|
194
169
|
},
|
|
195
170
|
},
|
|
196
|
-
customization: {
|
|
197
|
-
...DEFAULT_CONFIG.customization,
|
|
198
|
-
...config.customization,
|
|
199
|
-
},
|
|
200
|
-
features: {
|
|
201
|
-
...DEFAULT_CONFIG.features,
|
|
202
|
-
...config.features,
|
|
203
|
-
},
|
|
204
|
-
mobile: {
|
|
205
|
-
...DEFAULT_CONFIG.mobile,
|
|
206
|
-
...config.mobile,
|
|
207
|
-
},
|
|
208
171
|
auth: {
|
|
209
172
|
...DEFAULT_CONFIG.auth,
|
|
210
173
|
...config.auth,
|
|
@@ -447,6 +410,8 @@ class IframeManager {
|
|
|
447
410
|
this.modalContainer = null;
|
|
448
411
|
this.styleElement = null;
|
|
449
412
|
this.messageBroker = null;
|
|
413
|
+
// Guard flag to prevent double-binding event listeners
|
|
414
|
+
this.eventListenersBound = false;
|
|
450
415
|
this.config = config;
|
|
451
416
|
this.logger = new Logger(config.logging);
|
|
452
417
|
this.deviceInfo = detectDevice();
|
|
@@ -486,18 +451,27 @@ class IframeManager {
|
|
|
486
451
|
}
|
|
487
452
|
/**
|
|
488
453
|
* Create root container structure
|
|
454
|
+
* Reuses existing container if it has the same widgetId (singleton behavior)
|
|
489
455
|
*/
|
|
490
456
|
createRootContainer() {
|
|
491
|
-
|
|
492
|
-
let existingContainer = document.getElementById('weld-container');
|
|
457
|
+
const existingContainer = document.getElementById('weld-container');
|
|
493
458
|
if (existingContainer) {
|
|
494
|
-
|
|
459
|
+
// Reuse if same widgetId
|
|
460
|
+
if (existingContainer.getAttribute('data-widget-id') === this.config.widgetId) {
|
|
461
|
+
this.logger.debug('Reusing existing root container');
|
|
462
|
+
this.rootContainer = existingContainer;
|
|
463
|
+
this.appContainer = existingContainer.querySelector('.weld-app');
|
|
464
|
+
this.modalContainer = document.getElementById('weld-modal-container');
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
this.logger.warn('Weld container already exists with different widgetId, removing old instance');
|
|
495
468
|
existingContainer.remove();
|
|
496
469
|
}
|
|
497
470
|
// Create root container
|
|
498
471
|
this.rootContainer = document.createElement('div');
|
|
499
472
|
this.rootContainer.id = 'weld-container';
|
|
500
473
|
this.rootContainer.className = 'weld-namespace';
|
|
474
|
+
this.rootContainer.setAttribute('data-widget-id', this.config.widgetId);
|
|
501
475
|
// Create app container
|
|
502
476
|
this.appContainer = document.createElement('div');
|
|
503
477
|
this.appContainer.className = 'weld-app';
|
|
@@ -532,17 +506,7 @@ class IframeManager {
|
|
|
532
506
|
* Generate CSS for containers
|
|
533
507
|
*/
|
|
534
508
|
generateCSS() {
|
|
535
|
-
const { customization } = this.config;
|
|
536
509
|
return `
|
|
537
|
-
/* Weld Container */
|
|
538
|
-
#weld-container {
|
|
539
|
-
--weld-color-primary: ${customization.primaryColor};
|
|
540
|
-
--weld-color-accent: ${customization.accentColor};
|
|
541
|
-
--weld-font-family: ${customization.fontFamily};
|
|
542
|
-
--weld-font-size-base: ${customization.fontSize};
|
|
543
|
-
--weld-radius-xl: ${customization.borderRadius};
|
|
544
|
-
}
|
|
545
|
-
|
|
546
510
|
/* Import main stylesheet */
|
|
547
511
|
@import url('/styles/index.css');
|
|
548
512
|
|
|
@@ -566,20 +530,27 @@ class IframeManager {
|
|
|
566
530
|
* Create launcher iframe
|
|
567
531
|
*/
|
|
568
532
|
async createLauncherIframe() {
|
|
533
|
+
// Guard: skip if launcher iframe already exists
|
|
534
|
+
if (this.iframes.has(IframeType.LAUNCHER)) {
|
|
535
|
+
this.logger.debug('Launcher iframe already exists, skipping creation');
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
569
538
|
const { iframes } = this.config;
|
|
570
539
|
const { launcher } = iframes;
|
|
571
540
|
// Create container
|
|
572
541
|
const container = document.createElement('div');
|
|
573
542
|
container.className = 'weld-launcher-frame';
|
|
574
543
|
container.setAttribute('data-state', 'visible');
|
|
544
|
+
// Container is larger than the button to allow hover animations (scale, shadow) without clipping
|
|
545
|
+
const launcherPadding = 10;
|
|
575
546
|
container.style.cssText = `
|
|
576
547
|
position: fixed;
|
|
577
|
-
bottom: ${launcher.position.bottom};
|
|
578
|
-
right: ${launcher.position.right};
|
|
579
|
-
width: ${launcher.size};
|
|
580
|
-
height: ${launcher.size};
|
|
548
|
+
bottom: calc(${launcher.position.bottom} - ${launcherPadding}px);
|
|
549
|
+
right: calc(${launcher.position.right} - ${launcherPadding}px);
|
|
550
|
+
width: calc(${launcher.size} + ${launcherPadding * 2}px);
|
|
551
|
+
height: calc(${launcher.size} + ${launcherPadding * 2}px);
|
|
581
552
|
z-index: 2147483003;
|
|
582
|
-
pointer-events:
|
|
553
|
+
pointer-events: none;
|
|
583
554
|
display: block;
|
|
584
555
|
`;
|
|
585
556
|
// Create iframe
|
|
@@ -591,8 +562,12 @@ class IframeManager {
|
|
|
591
562
|
width: 100%;
|
|
592
563
|
height: 100%;
|
|
593
564
|
border: none;
|
|
594
|
-
background:
|
|
565
|
+
background: none;
|
|
566
|
+
color-scheme: none;
|
|
595
567
|
display: block;
|
|
568
|
+
pointer-events: auto;
|
|
569
|
+
border-radius: 50%;
|
|
570
|
+
filter: drop-shadow(rgba(9, 14, 21, 0.54) 0px 1px 6px) drop-shadow(rgba(9, 14, 21, 0.9) 0px 2px 32px);
|
|
596
571
|
`;
|
|
597
572
|
iframe.setAttribute('allow', 'clipboard-write');
|
|
598
573
|
iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-forms allow-popups');
|
|
@@ -608,20 +583,36 @@ class IframeManager {
|
|
|
608
583
|
createdAt: Date.now(),
|
|
609
584
|
});
|
|
610
585
|
// When DOM loads, notify MessageBroker to send weld:init
|
|
586
|
+
let launcherRetried = false;
|
|
611
587
|
iframe.onload = () => {
|
|
612
588
|
const metadata = this.iframes.get(IframeType.LAUNCHER);
|
|
613
589
|
if (metadata) {
|
|
614
590
|
this.logger.debug('Launcher iframe DOM loaded');
|
|
615
|
-
// Notify MessageBroker that DOM is loaded (triggers weld:init)
|
|
616
591
|
this.messageBroker?.setIframeDomLoaded(IframeType.LAUNCHER);
|
|
617
592
|
}
|
|
618
593
|
};
|
|
594
|
+
iframe.onerror = () => {
|
|
595
|
+
this.logger.error('Launcher iframe failed to load');
|
|
596
|
+
if (!launcherRetried) {
|
|
597
|
+
launcherRetried = true;
|
|
598
|
+
this.logger.info('Retrying launcher iframe load...');
|
|
599
|
+
setTimeout(() => { iframe.src = this.buildIframeUrl(launcher.url); }, 3000);
|
|
600
|
+
}
|
|
601
|
+
else {
|
|
602
|
+
this.config.onError?.(new Error('Failed to load widget launcher'));
|
|
603
|
+
}
|
|
604
|
+
};
|
|
619
605
|
this.logger.debug('Launcher iframe created');
|
|
620
606
|
}
|
|
621
607
|
/**
|
|
622
608
|
* Create widget iframe
|
|
623
609
|
*/
|
|
624
610
|
async createWidgetIframe() {
|
|
611
|
+
// Guard: skip if widget iframe already exists
|
|
612
|
+
if (this.iframes.has(IframeType.WIDGET)) {
|
|
613
|
+
this.logger.debug('Widget iframe already exists, skipping creation');
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
625
616
|
const { iframes } = this.config;
|
|
626
617
|
const { widget } = iframes;
|
|
627
618
|
// Create container
|
|
@@ -702,54 +693,32 @@ class IframeManager {
|
|
|
702
693
|
createdAt: Date.now(),
|
|
703
694
|
});
|
|
704
695
|
// When DOM loads, notify MessageBroker to send weld:init
|
|
696
|
+
let widgetRetried = false;
|
|
705
697
|
iframe.onload = () => {
|
|
706
698
|
const metadata = this.iframes.get(IframeType.WIDGET);
|
|
707
699
|
if (metadata) {
|
|
708
700
|
this.logger.debug('Widget iframe DOM loaded');
|
|
709
|
-
// Notify MessageBroker that DOM is loaded (triggers weld:init)
|
|
710
701
|
this.messageBroker?.setIframeDomLoaded(IframeType.WIDGET);
|
|
711
702
|
}
|
|
712
703
|
};
|
|
704
|
+
iframe.onerror = () => {
|
|
705
|
+
this.logger.error('Widget iframe failed to load');
|
|
706
|
+
if (!widgetRetried) {
|
|
707
|
+
widgetRetried = true;
|
|
708
|
+
this.logger.info('Retrying widget iframe load...');
|
|
709
|
+
setTimeout(() => { iframe.src = this.buildIframeUrl(widget.url); }, 3000);
|
|
710
|
+
}
|
|
711
|
+
else {
|
|
712
|
+
this.config.onError?.(new Error('Failed to load widget'));
|
|
713
|
+
}
|
|
714
|
+
};
|
|
713
715
|
this.logger.debug('Widget iframe created');
|
|
714
716
|
}
|
|
715
717
|
/**
|
|
716
|
-
* Create backdrop iframe
|
|
718
|
+
* Create backdrop iframe — disabled, widget stays non-modal so users can interact with the page
|
|
717
719
|
*/
|
|
718
720
|
async createBackdropIframe() {
|
|
719
|
-
|
|
720
|
-
this.logger.debug('Backdrop disabled, skipping creation');
|
|
721
|
-
return;
|
|
722
|
-
}
|
|
723
|
-
// Create container
|
|
724
|
-
const container = document.createElement('div');
|
|
725
|
-
container.className = 'weld-backdrop-frame';
|
|
726
|
-
container.setAttribute('data-state', 'hidden');
|
|
727
|
-
container.style.cssText = `
|
|
728
|
-
position: fixed;
|
|
729
|
-
top: 0;
|
|
730
|
-
left: 0;
|
|
731
|
-
right: 0;
|
|
732
|
-
bottom: 0;
|
|
733
|
-
z-index: 2147483000;
|
|
734
|
-
background: transparent;
|
|
735
|
-
pointer-events: none;
|
|
736
|
-
opacity: 0;
|
|
737
|
-
transition: opacity 200ms ease;
|
|
738
|
-
`;
|
|
739
|
-
this.appContainer?.appendChild(container);
|
|
740
|
-
// Store metadata (backdrop doesn't have an iframe, just a div)
|
|
741
|
-
// We'll create a minimal "iframe" reference for consistency
|
|
742
|
-
const dummyIframe = document.createElement('iframe');
|
|
743
|
-
dummyIframe.style.display = 'none';
|
|
744
|
-
this.iframes.set(IframeType.BACKDROP, {
|
|
745
|
-
type: IframeType.BACKDROP,
|
|
746
|
-
element: dummyIframe,
|
|
747
|
-
container,
|
|
748
|
-
ready: true, // Backdrop is always ready
|
|
749
|
-
visible: false,
|
|
750
|
-
createdAt: Date.now(),
|
|
751
|
-
});
|
|
752
|
-
this.logger.debug('Backdrop created');
|
|
721
|
+
this.logger.debug('Backdrop disabled, skipping creation');
|
|
753
722
|
}
|
|
754
723
|
/**
|
|
755
724
|
* Build iframe URL with parameters
|
|
@@ -763,12 +732,21 @@ class IframeManager {
|
|
|
763
732
|
url.searchParams.set('device', this.deviceInfo.type);
|
|
764
733
|
url.searchParams.set('mobile', String(this.deviceInfo.isMobile));
|
|
765
734
|
url.searchParams.set('parentOrigin', window.location.origin);
|
|
735
|
+
if (this.config.testMode) {
|
|
736
|
+
url.searchParams.set('testMode', 'true');
|
|
737
|
+
}
|
|
766
738
|
return url.toString();
|
|
767
739
|
}
|
|
768
740
|
/**
|
|
769
741
|
* Setup event listeners
|
|
770
742
|
*/
|
|
771
743
|
setupEventListeners() {
|
|
744
|
+
// Guard: prevent double-binding
|
|
745
|
+
if (this.eventListenersBound) {
|
|
746
|
+
this.logger.debug('Event listeners already bound, skipping');
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
this.eventListenersBound = true;
|
|
772
750
|
// Window resize - use bound handler for proper cleanup
|
|
773
751
|
window.addEventListener('resize', this.boundHandleResize);
|
|
774
752
|
// Orientation change - use bound handler for proper cleanup
|
|
@@ -871,7 +849,7 @@ class IframeManager {
|
|
|
871
849
|
iframe.container.style.transform = 'scale(1) translateY(0)';
|
|
872
850
|
}
|
|
873
851
|
// Handle mobile scroll lock
|
|
874
|
-
if (this.deviceInfo.isMobile && type === IframeType.WIDGET
|
|
852
|
+
if (this.deviceInfo.isMobile && type === IframeType.WIDGET) {
|
|
875
853
|
document.body.classList.add('weld-mobile-open');
|
|
876
854
|
}
|
|
877
855
|
// Hide launcher on mobile when widget is open (full-screen mode)
|
|
@@ -970,6 +948,8 @@ class IframeManager {
|
|
|
970
948
|
this.iframes.clear();
|
|
971
949
|
// Clear messageBroker reference
|
|
972
950
|
this.messageBroker = null;
|
|
951
|
+
// Reset guard flag
|
|
952
|
+
this.eventListenersBound = false;
|
|
973
953
|
this.logger.info('IframeManager destroyed');
|
|
974
954
|
}
|
|
975
955
|
}
|
|
@@ -1037,6 +1017,8 @@ var MessageType;
|
|
|
1037
1017
|
// Events
|
|
1038
1018
|
MessageType["EVENT_TRACK"] = "weld:event:track";
|
|
1039
1019
|
MessageType["ERROR_REPORT"] = "weld:error:report";
|
|
1020
|
+
// Page tracking
|
|
1021
|
+
MessageType["PAGE_CHANGE"] = "weld:page:change";
|
|
1040
1022
|
// API responses
|
|
1041
1023
|
MessageType["API_SUCCESS"] = "weld:api:success";
|
|
1042
1024
|
MessageType["API_ERROR"] = "weld:api:error";
|
|
@@ -1576,8 +1558,6 @@ class MessageBroker {
|
|
|
1576
1558
|
iframeType,
|
|
1577
1559
|
config: {
|
|
1578
1560
|
api: this.config.api,
|
|
1579
|
-
customization: this.config.customization,
|
|
1580
|
-
features: this.config.features,
|
|
1581
1561
|
},
|
|
1582
1562
|
};
|
|
1583
1563
|
const message = createMessage('weld:init', MessageOrigin.PARENT, initPayload);
|
|
@@ -2250,7 +2230,7 @@ class StateCoordinator {
|
|
|
2250
2230
|
}
|
|
2251
2231
|
}
|
|
2252
2232
|
|
|
2253
|
-
var version = "1.0.
|
|
2233
|
+
var version = "1.0.17";
|
|
2254
2234
|
var packageJson = {
|
|
2255
2235
|
version: version};
|
|
2256
2236
|
|
|
@@ -2258,6 +2238,16 @@ var packageJson = {
|
|
|
2258
2238
|
* Weld SDK - Main Entry Point
|
|
2259
2239
|
* Public API for the Weld helpdesk widget
|
|
2260
2240
|
*/
|
|
2241
|
+
/**
|
|
2242
|
+
* Module-level singleton registry keyed by widgetId
|
|
2243
|
+
*/
|
|
2244
|
+
const sdkRegistry = new Map();
|
|
2245
|
+
/**
|
|
2246
|
+
* SessionStorage key helpers
|
|
2247
|
+
*/
|
|
2248
|
+
function openStateKey(widgetId) {
|
|
2249
|
+
return `weld-widget-open-${widgetId}`;
|
|
2250
|
+
}
|
|
2261
2251
|
/**
|
|
2262
2252
|
* SDK initialization status
|
|
2263
2253
|
*/
|
|
@@ -2280,6 +2270,8 @@ class WeldSDK {
|
|
|
2280
2270
|
this.readyResolve = null;
|
|
2281
2271
|
// Subscription IDs for cleanup
|
|
2282
2272
|
this.subscriptionIds = [];
|
|
2273
|
+
// Page tracking cleanup
|
|
2274
|
+
this.pageTrackingCleanup = null;
|
|
2283
2275
|
/**
|
|
2284
2276
|
* Update user attributes (Intercom-style, with rate limiting)
|
|
2285
2277
|
* Limited to 20 calls per page load to prevent abuse
|
|
@@ -2318,6 +2310,13 @@ class WeldSDK {
|
|
|
2318
2310
|
console.log('[Weld SDK] Received message:', event.data.type);
|
|
2319
2311
|
}
|
|
2320
2312
|
if (event.data?.type === 'launcher:clicked') {
|
|
2313
|
+
if (this.status !== SDKStatus.READY) {
|
|
2314
|
+
console.log('[Weld SDK] Launcher clicked but SDK not ready yet — waiting...');
|
|
2315
|
+
this.readyPromise?.then(() => {
|
|
2316
|
+
this.handleLauncherClickMessage(event);
|
|
2317
|
+
});
|
|
2318
|
+
return;
|
|
2319
|
+
}
|
|
2321
2320
|
// Toggle behavior - if widget is open, close it; if closed, open it
|
|
2322
2321
|
const state = this.stateCoordinator.getState();
|
|
2323
2322
|
if (state.widget.isOpen) {
|
|
@@ -2330,9 +2329,260 @@ class WeldSDK {
|
|
|
2330
2329
|
}
|
|
2331
2330
|
}
|
|
2332
2331
|
if (event.data?.type === 'weld:close') {
|
|
2332
|
+
if (this.status !== SDKStatus.READY)
|
|
2333
|
+
return;
|
|
2333
2334
|
console.log('[Weld SDK] Widget close requested');
|
|
2334
2335
|
this.close();
|
|
2335
2336
|
}
|
|
2337
|
+
if (event.data?.type === 'weld:unread-count') {
|
|
2338
|
+
const count = event.data.count ?? 0;
|
|
2339
|
+
// Forward to launcher iframe
|
|
2340
|
+
const launcherIframe = this.iframeManager.getIframe(IframeType.LAUNCHER);
|
|
2341
|
+
if (launcherIframe?.element?.contentWindow) {
|
|
2342
|
+
launcherIframe.element.contentWindow.postMessage({
|
|
2343
|
+
type: 'weld:unread-count',
|
|
2344
|
+
count
|
|
2345
|
+
}, '*');
|
|
2346
|
+
}
|
|
2347
|
+
// Update state coordinator for external API consumers
|
|
2348
|
+
this.stateCoordinator.setBadgeCount(count);
|
|
2349
|
+
}
|
|
2350
|
+
if (event.data?.type === 'weld:image:open' && event.data?.url) {
|
|
2351
|
+
this.showImageLightbox(event.data.url);
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
/**
|
|
2355
|
+
* Show fullscreen image lightbox on the parent page
|
|
2356
|
+
*/
|
|
2357
|
+
showImageLightbox(url) {
|
|
2358
|
+
// Remove existing lightbox if any
|
|
2359
|
+
const existing = document.getElementById('weld-image-lightbox');
|
|
2360
|
+
if (existing)
|
|
2361
|
+
existing.remove();
|
|
2362
|
+
// Zoom / pan state
|
|
2363
|
+
let scale = 1;
|
|
2364
|
+
let translateX = 0;
|
|
2365
|
+
let translateY = 0;
|
|
2366
|
+
let isDragging = false;
|
|
2367
|
+
let dragStartX = 0;
|
|
2368
|
+
let dragStartY = 0;
|
|
2369
|
+
let lastTranslateX = 0;
|
|
2370
|
+
let lastTranslateY = 0;
|
|
2371
|
+
const applyTransform = () => {
|
|
2372
|
+
img.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;
|
|
2373
|
+
};
|
|
2374
|
+
const resetTransform = () => {
|
|
2375
|
+
scale = 1;
|
|
2376
|
+
translateX = 0;
|
|
2377
|
+
translateY = 0;
|
|
2378
|
+
applyTransform();
|
|
2379
|
+
img.style.cursor = 'zoom-in';
|
|
2380
|
+
};
|
|
2381
|
+
const overlay = document.createElement('div');
|
|
2382
|
+
overlay.id = 'weld-image-lightbox';
|
|
2383
|
+
overlay.style.cssText = `
|
|
2384
|
+
position: fixed;
|
|
2385
|
+
inset: 0;
|
|
2386
|
+
z-index: 2147483647;
|
|
2387
|
+
background: rgba(0, 0, 0, 0.92);
|
|
2388
|
+
display: flex;
|
|
2389
|
+
align-items: center;
|
|
2390
|
+
justify-content: center;
|
|
2391
|
+
padding: 16px;
|
|
2392
|
+
cursor: pointer;
|
|
2393
|
+
overflow: hidden;
|
|
2394
|
+
`;
|
|
2395
|
+
// Close button
|
|
2396
|
+
const closeBtn = document.createElement('button');
|
|
2397
|
+
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>`;
|
|
2398
|
+
closeBtn.style.cssText = `
|
|
2399
|
+
position: absolute;
|
|
2400
|
+
top: 16px;
|
|
2401
|
+
right: 16px;
|
|
2402
|
+
width: 40px;
|
|
2403
|
+
height: 40px;
|
|
2404
|
+
border-radius: 50%;
|
|
2405
|
+
border: none;
|
|
2406
|
+
background: rgba(255, 255, 255, 0.1);
|
|
2407
|
+
color: white;
|
|
2408
|
+
cursor: pointer;
|
|
2409
|
+
display: flex;
|
|
2410
|
+
align-items: center;
|
|
2411
|
+
justify-content: center;
|
|
2412
|
+
transition: background 0.15s;
|
|
2413
|
+
`;
|
|
2414
|
+
closeBtn.onmouseenter = () => { closeBtn.style.background = 'rgba(255, 255, 255, 0.2)'; };
|
|
2415
|
+
closeBtn.onmouseleave = () => { closeBtn.style.background = 'rgba(255, 255, 255, 0.1)'; };
|
|
2416
|
+
// Download button
|
|
2417
|
+
const downloadBtn = document.createElement('a');
|
|
2418
|
+
downloadBtn.href = url;
|
|
2419
|
+
downloadBtn.download = '';
|
|
2420
|
+
downloadBtn.target = '_blank';
|
|
2421
|
+
downloadBtn.rel = 'noopener noreferrer';
|
|
2422
|
+
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>`;
|
|
2423
|
+
downloadBtn.style.cssText = `
|
|
2424
|
+
position: absolute;
|
|
2425
|
+
top: 16px;
|
|
2426
|
+
right: 64px;
|
|
2427
|
+
width: 40px;
|
|
2428
|
+
height: 40px;
|
|
2429
|
+
border-radius: 50%;
|
|
2430
|
+
border: none;
|
|
2431
|
+
background: rgba(255, 255, 255, 0.1);
|
|
2432
|
+
color: white;
|
|
2433
|
+
cursor: pointer;
|
|
2434
|
+
display: flex;
|
|
2435
|
+
align-items: center;
|
|
2436
|
+
justify-content: center;
|
|
2437
|
+
transition: background 0.15s;
|
|
2438
|
+
text-decoration: none;
|
|
2439
|
+
`;
|
|
2440
|
+
downloadBtn.onmouseenter = () => { downloadBtn.style.background = 'rgba(255, 255, 255, 0.2)'; };
|
|
2441
|
+
downloadBtn.onmouseleave = () => { downloadBtn.style.background = 'rgba(255, 255, 255, 0.1)'; };
|
|
2442
|
+
// Image
|
|
2443
|
+
const img = document.createElement('img');
|
|
2444
|
+
img.src = url;
|
|
2445
|
+
img.alt = 'Full size';
|
|
2446
|
+
img.draggable = false;
|
|
2447
|
+
img.style.cssText = `
|
|
2448
|
+
max-width: 100%;
|
|
2449
|
+
max-height: 100%;
|
|
2450
|
+
object-fit: contain;
|
|
2451
|
+
border-radius: 8px;
|
|
2452
|
+
cursor: zoom-in;
|
|
2453
|
+
transition: transform 0.2s ease;
|
|
2454
|
+
user-select: none;
|
|
2455
|
+
`;
|
|
2456
|
+
// Click to toggle zoom
|
|
2457
|
+
img.addEventListener('click', (e) => {
|
|
2458
|
+
e.stopPropagation();
|
|
2459
|
+
if (scale === 1) {
|
|
2460
|
+
// Zoom in to 2.5x centered on click position
|
|
2461
|
+
const rect = img.getBoundingClientRect();
|
|
2462
|
+
const clickX = e.clientX - rect.left - rect.width / 2;
|
|
2463
|
+
const clickY = e.clientY - rect.top - rect.height / 2;
|
|
2464
|
+
scale = 2.5;
|
|
2465
|
+
translateX = -clickX * 1.5;
|
|
2466
|
+
translateY = -clickY * 1.5;
|
|
2467
|
+
applyTransform();
|
|
2468
|
+
img.style.cursor = 'zoom-out';
|
|
2469
|
+
}
|
|
2470
|
+
else {
|
|
2471
|
+
// Zoom out - reset
|
|
2472
|
+
resetTransform();
|
|
2473
|
+
}
|
|
2474
|
+
});
|
|
2475
|
+
// Mouse wheel zoom
|
|
2476
|
+
overlay.addEventListener('wheel', (e) => {
|
|
2477
|
+
e.preventDefault();
|
|
2478
|
+
const delta = e.deltaY > 0 ? -0.25 : 0.25;
|
|
2479
|
+
const newScale = Math.min(Math.max(scale + delta, 1), 5);
|
|
2480
|
+
if (newScale === 1) {
|
|
2481
|
+
resetTransform();
|
|
2482
|
+
}
|
|
2483
|
+
else {
|
|
2484
|
+
scale = newScale;
|
|
2485
|
+
applyTransform();
|
|
2486
|
+
img.style.cursor = 'zoom-out';
|
|
2487
|
+
}
|
|
2488
|
+
}, { passive: false });
|
|
2489
|
+
// Drag to pan when zoomed
|
|
2490
|
+
img.addEventListener('mousedown', (e) => {
|
|
2491
|
+
if (scale <= 1)
|
|
2492
|
+
return;
|
|
2493
|
+
e.preventDefault();
|
|
2494
|
+
isDragging = true;
|
|
2495
|
+
dragStartX = e.clientX;
|
|
2496
|
+
dragStartY = e.clientY;
|
|
2497
|
+
lastTranslateX = translateX;
|
|
2498
|
+
lastTranslateY = translateY;
|
|
2499
|
+
img.style.cursor = 'grabbing';
|
|
2500
|
+
img.style.transition = 'none';
|
|
2501
|
+
});
|
|
2502
|
+
window.addEventListener('mousemove', (e) => {
|
|
2503
|
+
if (!isDragging)
|
|
2504
|
+
return;
|
|
2505
|
+
translateX = lastTranslateX + (e.clientX - dragStartX);
|
|
2506
|
+
translateY = lastTranslateY + (e.clientY - dragStartY);
|
|
2507
|
+
applyTransform();
|
|
2508
|
+
});
|
|
2509
|
+
window.addEventListener('mouseup', () => {
|
|
2510
|
+
if (!isDragging)
|
|
2511
|
+
return;
|
|
2512
|
+
isDragging = false;
|
|
2513
|
+
img.style.cursor = scale > 1 ? 'zoom-out' : 'zoom-in';
|
|
2514
|
+
img.style.transition = 'transform 0.2s ease';
|
|
2515
|
+
});
|
|
2516
|
+
// Touch: pinch to zoom + drag to pan
|
|
2517
|
+
let lastTouchDist = 0;
|
|
2518
|
+
let lastTouchScale = 1;
|
|
2519
|
+
overlay.addEventListener('touchstart', (e) => {
|
|
2520
|
+
if (e.touches.length === 2) {
|
|
2521
|
+
e.preventDefault();
|
|
2522
|
+
const dx = e.touches[0].clientX - e.touches[1].clientX;
|
|
2523
|
+
const dy = e.touches[0].clientY - e.touches[1].clientY;
|
|
2524
|
+
lastTouchDist = Math.hypot(dx, dy);
|
|
2525
|
+
lastTouchScale = scale;
|
|
2526
|
+
}
|
|
2527
|
+
else if (e.touches.length === 1 && scale > 1) {
|
|
2528
|
+
isDragging = true;
|
|
2529
|
+
dragStartX = e.touches[0].clientX;
|
|
2530
|
+
dragStartY = e.touches[0].clientY;
|
|
2531
|
+
lastTranslateX = translateX;
|
|
2532
|
+
lastTranslateY = translateY;
|
|
2533
|
+
img.style.transition = 'none';
|
|
2534
|
+
}
|
|
2535
|
+
}, { passive: false });
|
|
2536
|
+
overlay.addEventListener('touchmove', (e) => {
|
|
2537
|
+
if (e.touches.length === 2) {
|
|
2538
|
+
e.preventDefault();
|
|
2539
|
+
const dx = e.touches[0].clientX - e.touches[1].clientX;
|
|
2540
|
+
const dy = e.touches[0].clientY - e.touches[1].clientY;
|
|
2541
|
+
const dist = Math.hypot(dx, dy);
|
|
2542
|
+
scale = Math.min(Math.max(lastTouchScale * (dist / lastTouchDist), 1), 5);
|
|
2543
|
+
if (scale === 1) {
|
|
2544
|
+
translateX = 0;
|
|
2545
|
+
translateY = 0;
|
|
2546
|
+
}
|
|
2547
|
+
applyTransform();
|
|
2548
|
+
}
|
|
2549
|
+
else if (e.touches.length === 1 && isDragging) {
|
|
2550
|
+
e.preventDefault();
|
|
2551
|
+
translateX = lastTranslateX + (e.touches[0].clientX - dragStartX);
|
|
2552
|
+
translateY = lastTranslateY + (e.touches[0].clientY - dragStartY);
|
|
2553
|
+
applyTransform();
|
|
2554
|
+
}
|
|
2555
|
+
}, { passive: false });
|
|
2556
|
+
overlay.addEventListener('touchend', (e) => {
|
|
2557
|
+
if (e.touches.length < 2) {
|
|
2558
|
+
lastTouchDist = 0;
|
|
2559
|
+
}
|
|
2560
|
+
if (e.touches.length === 0) {
|
|
2561
|
+
isDragging = false;
|
|
2562
|
+
img.style.transition = 'transform 0.2s ease';
|
|
2563
|
+
}
|
|
2564
|
+
});
|
|
2565
|
+
const close = () => {
|
|
2566
|
+
document.removeEventListener('keydown', handleKeyDown);
|
|
2567
|
+
overlay.remove();
|
|
2568
|
+
};
|
|
2569
|
+
// Only close on backdrop click when not zoomed (prevent accidental close while panning)
|
|
2570
|
+
overlay.addEventListener('click', (e) => {
|
|
2571
|
+
if (e.target === overlay && scale <= 1)
|
|
2572
|
+
close();
|
|
2573
|
+
});
|
|
2574
|
+
downloadBtn.addEventListener('click', (e) => e.stopPropagation());
|
|
2575
|
+
closeBtn.addEventListener('click', (e) => { e.stopPropagation(); close(); });
|
|
2576
|
+
// Close on Escape
|
|
2577
|
+
const handleKeyDown = (e) => {
|
|
2578
|
+
if (e.key === 'Escape')
|
|
2579
|
+
close();
|
|
2580
|
+
};
|
|
2581
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
2582
|
+
overlay.appendChild(closeBtn);
|
|
2583
|
+
overlay.appendChild(downloadBtn);
|
|
2584
|
+
overlay.appendChild(img);
|
|
2585
|
+
document.body.appendChild(overlay);
|
|
2336
2586
|
}
|
|
2337
2587
|
/**
|
|
2338
2588
|
* Initialize the SDK and render widget
|
|
@@ -2358,6 +2608,13 @@ class WeldSDK {
|
|
|
2358
2608
|
this.logger.info('WeldSDK ready');
|
|
2359
2609
|
// Call onReady callback
|
|
2360
2610
|
this.config.onReady?.();
|
|
2611
|
+
// Start tracking page URL changes
|
|
2612
|
+
this.startPageTracking();
|
|
2613
|
+
// Auto-open if widget was previously open (persisted in sessionStorage)
|
|
2614
|
+
if (this.wasOpen()) {
|
|
2615
|
+
this.logger.info('Restoring previously open widget from sessionStorage');
|
|
2616
|
+
this.open();
|
|
2617
|
+
}
|
|
2361
2618
|
}
|
|
2362
2619
|
catch (error) {
|
|
2363
2620
|
this.status = SDKStatus.ERROR;
|
|
@@ -2436,6 +2693,58 @@ class WeldSDK {
|
|
|
2436
2693
|
isReady() {
|
|
2437
2694
|
return this.status === SDKStatus.READY;
|
|
2438
2695
|
}
|
|
2696
|
+
/**
|
|
2697
|
+
* Update callbacks on an existing instance (used by singleton reuse)
|
|
2698
|
+
*/
|
|
2699
|
+
updateCallbacks(config) {
|
|
2700
|
+
if (config.onReady !== undefined)
|
|
2701
|
+
this.config.onReady = config.onReady;
|
|
2702
|
+
if (config.onOpen !== undefined)
|
|
2703
|
+
this.config.onOpen = config.onOpen;
|
|
2704
|
+
if (config.onClose !== undefined)
|
|
2705
|
+
this.config.onClose = config.onClose;
|
|
2706
|
+
if (config.onError !== undefined)
|
|
2707
|
+
this.config.onError = config.onError;
|
|
2708
|
+
if (config.onDestroy !== undefined)
|
|
2709
|
+
this.config.onDestroy = config.onDestroy;
|
|
2710
|
+
if (config.onMinimize !== undefined)
|
|
2711
|
+
this.config.onMinimize = config.onMinimize;
|
|
2712
|
+
if (config.onMaximize !== undefined)
|
|
2713
|
+
this.config.onMaximize = config.onMaximize;
|
|
2714
|
+
}
|
|
2715
|
+
/**
|
|
2716
|
+
* Persist open/closed state to sessionStorage
|
|
2717
|
+
*/
|
|
2718
|
+
persistOpenState(isOpen) {
|
|
2719
|
+
try {
|
|
2720
|
+
sessionStorage.setItem(openStateKey(this.config.widgetId), isOpen ? 'true' : 'false');
|
|
2721
|
+
}
|
|
2722
|
+
catch {
|
|
2723
|
+
// sessionStorage might not be available
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2726
|
+
/**
|
|
2727
|
+
* Clear persisted state from sessionStorage
|
|
2728
|
+
*/
|
|
2729
|
+
clearPersistedState() {
|
|
2730
|
+
try {
|
|
2731
|
+
sessionStorage.removeItem(openStateKey(this.config.widgetId));
|
|
2732
|
+
}
|
|
2733
|
+
catch {
|
|
2734
|
+
// sessionStorage might not be available
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
/**
|
|
2738
|
+
* Check if widget was previously open (from sessionStorage)
|
|
2739
|
+
*/
|
|
2740
|
+
wasOpen() {
|
|
2741
|
+
try {
|
|
2742
|
+
return sessionStorage.getItem(openStateKey(this.config.widgetId)) === 'true';
|
|
2743
|
+
}
|
|
2744
|
+
catch {
|
|
2745
|
+
return false;
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2439
2748
|
/**
|
|
2440
2749
|
* Open the widget
|
|
2441
2750
|
*/
|
|
@@ -2444,8 +2753,6 @@ class WeldSDK {
|
|
|
2444
2753
|
console.log('[Weld SDK] Opening widget...');
|
|
2445
2754
|
this.stateCoordinator.openWidget();
|
|
2446
2755
|
this.iframeManager.showIframe(IframeType.WIDGET);
|
|
2447
|
-
this.iframeManager.showIframe(IframeType.BACKDROP);
|
|
2448
|
-
// Keep launcher visible so user can click it to close the widget
|
|
2449
2756
|
// Send open message to the widget iframe
|
|
2450
2757
|
const widgetIframe = this.iframeManager.getIframe(IframeType.WIDGET);
|
|
2451
2758
|
if (widgetIframe?.element?.contentWindow) {
|
|
@@ -2456,6 +2763,7 @@ class WeldSDK {
|
|
|
2456
2763
|
if (launcherIframe?.element?.contentWindow) {
|
|
2457
2764
|
launcherIframe.element.contentWindow.postMessage({ type: 'weld:widget-opened' }, '*');
|
|
2458
2765
|
}
|
|
2766
|
+
this.persistOpenState(true);
|
|
2459
2767
|
this.config.onOpen?.();
|
|
2460
2768
|
}
|
|
2461
2769
|
/**
|
|
@@ -2466,8 +2774,6 @@ class WeldSDK {
|
|
|
2466
2774
|
console.log('[Weld SDK] Closing widget...');
|
|
2467
2775
|
this.stateCoordinator.closeWidget();
|
|
2468
2776
|
this.iframeManager.hideIframe(IframeType.WIDGET);
|
|
2469
|
-
this.iframeManager.hideIframe(IframeType.BACKDROP);
|
|
2470
|
-
// Launcher stays visible
|
|
2471
2777
|
// Send close message to the widget iframe
|
|
2472
2778
|
const widgetIframe = this.iframeManager.getIframe(IframeType.WIDGET);
|
|
2473
2779
|
if (widgetIframe?.element?.contentWindow) {
|
|
@@ -2478,6 +2784,7 @@ class WeldSDK {
|
|
|
2478
2784
|
if (launcherIframe?.element?.contentWindow) {
|
|
2479
2785
|
launcherIframe.element.contentWindow.postMessage({ type: 'weld:widget-closed' }, '*');
|
|
2480
2786
|
}
|
|
2787
|
+
this.persistOpenState(false);
|
|
2481
2788
|
this.config.onClose?.();
|
|
2482
2789
|
}
|
|
2483
2790
|
/**
|
|
@@ -2666,6 +2973,8 @@ class WeldSDK {
|
|
|
2666
2973
|
});
|
|
2667
2974
|
// Broadcast logout to iframes
|
|
2668
2975
|
this.messageBroker.broadcast('weld:auth:logout', {});
|
|
2976
|
+
// Clear persisted widget state
|
|
2977
|
+
this.clearPersistedState();
|
|
2669
2978
|
// Clear any stored session data
|
|
2670
2979
|
try {
|
|
2671
2980
|
const prefix = 'weld-';
|
|
@@ -2807,6 +3116,63 @@ class WeldSDK {
|
|
|
2807
3116
|
this.logger.setLevel('warn');
|
|
2808
3117
|
this.logger.info('Debug mode disabled');
|
|
2809
3118
|
}
|
|
3119
|
+
/**
|
|
3120
|
+
* Send a page change message to the widget iframe
|
|
3121
|
+
*/
|
|
3122
|
+
sendPageChange(url, title) {
|
|
3123
|
+
const widgetIframe = this.iframeManager.getIframe(IframeType.WIDGET);
|
|
3124
|
+
if (widgetIframe?.element?.contentWindow) {
|
|
3125
|
+
widgetIframe.element.contentWindow.postMessage({
|
|
3126
|
+
type: 'weld:page:change',
|
|
3127
|
+
url,
|
|
3128
|
+
title,
|
|
3129
|
+
timestamp: Date.now(),
|
|
3130
|
+
}, '*');
|
|
3131
|
+
}
|
|
3132
|
+
}
|
|
3133
|
+
/**
|
|
3134
|
+
* Start tracking page URL changes (SPA navigations + popstate)
|
|
3135
|
+
*/
|
|
3136
|
+
startPageTracking() {
|
|
3137
|
+
let lastUrl = window.location.href;
|
|
3138
|
+
let debounceTimer = null;
|
|
3139
|
+
const notifyChange = () => {
|
|
3140
|
+
const currentUrl = window.location.href;
|
|
3141
|
+
if (currentUrl !== lastUrl) {
|
|
3142
|
+
lastUrl = currentUrl;
|
|
3143
|
+
this.sendPageChange(currentUrl, document.title);
|
|
3144
|
+
}
|
|
3145
|
+
};
|
|
3146
|
+
const debouncedNotify = () => {
|
|
3147
|
+
if (debounceTimer)
|
|
3148
|
+
clearTimeout(debounceTimer);
|
|
3149
|
+
debounceTimer = setTimeout(notifyChange, 300);
|
|
3150
|
+
};
|
|
3151
|
+
// Send initial page
|
|
3152
|
+
this.sendPageChange(window.location.href, document.title);
|
|
3153
|
+
// Monkey-patch history.pushState and history.replaceState
|
|
3154
|
+
const origPushState = history.pushState.bind(history);
|
|
3155
|
+
const origReplaceState = history.replaceState.bind(history);
|
|
3156
|
+
history.pushState = function (...args) {
|
|
3157
|
+
origPushState(...args);
|
|
3158
|
+
debouncedNotify();
|
|
3159
|
+
};
|
|
3160
|
+
history.replaceState = function (...args) {
|
|
3161
|
+
origReplaceState(...args);
|
|
3162
|
+
debouncedNotify();
|
|
3163
|
+
};
|
|
3164
|
+
// Listen for popstate (browser back/forward)
|
|
3165
|
+
const handlePopstate = () => debouncedNotify();
|
|
3166
|
+
window.addEventListener('popstate', handlePopstate);
|
|
3167
|
+
// Store cleanup
|
|
3168
|
+
this.pageTrackingCleanup = () => {
|
|
3169
|
+
if (debounceTimer)
|
|
3170
|
+
clearTimeout(debounceTimer);
|
|
3171
|
+
window.removeEventListener('popstate', handlePopstate);
|
|
3172
|
+
history.pushState = origPushState;
|
|
3173
|
+
history.replaceState = origReplaceState;
|
|
3174
|
+
};
|
|
3175
|
+
}
|
|
2810
3176
|
/**
|
|
2811
3177
|
* Ensure SDK is ready before operation
|
|
2812
3178
|
*/
|
|
@@ -2815,11 +3181,26 @@ class WeldSDK {
|
|
|
2815
3181
|
throw new Error('SDK not ready. Call init() first.');
|
|
2816
3182
|
}
|
|
2817
3183
|
}
|
|
3184
|
+
/**
|
|
3185
|
+
* Detach from the current component lifecycle without destroying the widget.
|
|
3186
|
+
* Use this as a React useEffect cleanup — the widget stays alive across navigations.
|
|
3187
|
+
*/
|
|
3188
|
+
detach() {
|
|
3189
|
+
// No-op: widget stays alive in the singleton registry
|
|
3190
|
+
this.logger.debug('WeldSDK detached (no-op, widget stays alive)');
|
|
3191
|
+
}
|
|
2818
3192
|
/**
|
|
2819
3193
|
* Destroy SDK and cleanup
|
|
2820
3194
|
*/
|
|
2821
3195
|
destroy() {
|
|
2822
3196
|
this.logger.info('Destroying WeldSDK');
|
|
3197
|
+
// Remove from singleton registry
|
|
3198
|
+
sdkRegistry.delete(this.config.widgetId);
|
|
3199
|
+
// Clear persisted state
|
|
3200
|
+
this.clearPersistedState();
|
|
3201
|
+
// Stop page tracking
|
|
3202
|
+
this.pageTrackingCleanup?.();
|
|
3203
|
+
this.pageTrackingCleanup = null;
|
|
2823
3204
|
// Remove event listener using bound handler
|
|
2824
3205
|
window.removeEventListener('message', this.boundHandleLauncherClick);
|
|
2825
3206
|
// Unsubscribe from all message broker subscriptions
|