@journyio/messaging-sdk 1.0.0 → 1.0.1

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.
@@ -102,6 +102,28 @@ class MessageQueue {
102
102
  }
103
103
  }
104
104
 
105
+ const DEFAULT_API_ENDPOINT = 'https://analyze.journy.io';
106
+ const DEFAULT_POLLING_INTERVAL = 30000;
107
+ const ROOT_ELEMENT_ID = 'journy-messages-root';
108
+ const DEFAULT_STYLE_ID = 'journy-messages-default';
109
+ const REACT_CHECK_INTERVAL = 100;
110
+ const REACT_WAIT_TIMEOUT = 10000;
111
+ const MESSAGE_CLOSE_DELAY = 300;
112
+ const DEFAULT_WIDGET_SETTINGS = {
113
+ pollingInterval: DEFAULT_POLLING_INTERVAL,
114
+ showReadMessages: true,
115
+ autoExpandOnNew: true,
116
+ displayMode: 'widget',
117
+ apiEndpoint: DEFAULT_API_ENDPOINT,
118
+ styles: 'default',
119
+ };
120
+ const STORAGE_KEYS = {
121
+ WIDGET_SETTINGS: 'widget_settings',
122
+ WIDGET_VISIBLE: 'widget_visible',
123
+ WIDGET_COLLAPSED: 'widget_collapsed',
124
+ CURRENT_MESSAGE_ID: 'current_message_id',
125
+ };
126
+
105
127
  const API_PATHS = {
106
128
  IN_APP_MESSAGES: '/sdk/in-app-messages',
107
129
  };
@@ -120,7 +142,7 @@ class ApiClient {
120
142
  return headers;
121
143
  }
122
144
  buildUrl(endpoint) {
123
- const baseUrl = (this.config.apiEndpoint || 'https://jtm.journy.io').replace(/\/$/, '');
145
+ const baseUrl = (this.config.apiEndpoint || DEFAULT_API_ENDPOINT).replace(/\/$/, '');
124
146
  const entityId = this.config.entityType === 'user'
125
147
  ? this.config.userId
126
148
  : this.config.accountId;
@@ -181,14 +203,6 @@ var SDKEventType;
181
203
  SDKEventType["MessageClosed"] = "In-App Message Closed";
182
204
  SDKEventType["MessageLinkClicked"] = "In-App Message Link Clicked";
183
205
  })(SDKEventType || (SDKEventType = {}));
184
- const DEFAULT_WIDGET_SETTINGS = {
185
- pollingInterval: 30000,
186
- showReadMessages: true,
187
- autoExpandOnNew: true,
188
- displayMode: 'widget',
189
- apiEndpoint: 'https://jtm.journy.io',
190
- styles: 'default',
191
- };
192
206
 
193
207
  const STORAGE_PREFIX = 'journy_messages_';
194
208
  function getStorageKey(key) {
@@ -326,17 +340,33 @@ class MessagingStore {
326
340
  }
327
341
  }
328
342
 
329
- function createRootElement(id) {
330
- let element = document.getElementById(id);
343
+ function getDebugFlags() {
344
+ if (typeof window === 'undefined')
345
+ return {};
346
+ const flag = window.__JOURNY_DEBUG__;
347
+ if (flag === true)
348
+ return { mockMessages: true, settings: true };
349
+ if (flag && typeof flag === 'object')
350
+ return flag;
351
+ return {};
352
+ }
353
+ function isDebugMockMessages() {
354
+ return getDebugFlags().mockMessages === true;
355
+ }
356
+ function isDebugSettings() {
357
+ return getDebugFlags().settings === true;
358
+ }
359
+ function createRootElement(id, doc = document) {
360
+ let element = doc.getElementById(id);
331
361
  if (!element) {
332
- element = document.createElement('div');
362
+ element = doc.createElement('div');
333
363
  element.id = id;
334
- document.body.appendChild(element);
364
+ doc.body.appendChild(element);
335
365
  }
336
366
  return element;
337
367
  }
338
- function removeRootElement(id) {
339
- const element = document.getElementById(id);
368
+ function removeRootElement(id, doc = document) {
369
+ const element = doc.getElementById(id);
340
370
  if (element) {
341
371
  element.remove();
342
372
  }
@@ -344,47 +374,78 @@ function removeRootElement(id) {
344
374
  const STYLES_LINK_ID = 'journy-messages-styles-link';
345
375
  const STYLES_TAG_ID = 'journy-messages-custom';
346
376
  const DEFAULT_STYLES_TAG_ID = 'journy-messages-default';
347
- function injectStyleLink(href) {
348
- if (typeof document === 'undefined' || !document.head)
377
+ function injectStyleLink(href, doc = document) {
378
+ if (!doc || !doc.head)
349
379
  return;
350
- const existing = document.getElementById(STYLES_LINK_ID);
380
+ const existing = doc.getElementById(STYLES_LINK_ID);
351
381
  if (existing && existing instanceof HTMLLinkElement && existing.href === href)
352
382
  return;
353
383
  if (existing)
354
384
  existing.remove();
355
- const link = document.createElement('link');
385
+ const link = doc.createElement('link');
356
386
  link.id = STYLES_LINK_ID;
357
387
  link.rel = 'stylesheet';
358
388
  link.href = href;
359
- document.head.appendChild(link);
389
+ doc.head.appendChild(link);
360
390
  }
361
- function injectStyleTag(css, id = STYLES_TAG_ID) {
362
- if (typeof document === 'undefined' || !document.head)
391
+ function injectStyleTag(css, id = STYLES_TAG_ID, doc = document) {
392
+ if (!doc || !doc.head)
363
393
  return;
364
- let style = document.getElementById(id);
394
+ let style = doc.getElementById(id);
365
395
  if (style) {
366
396
  style.textContent = css;
367
397
  return;
368
398
  }
369
- style = document.createElement('style');
399
+ style = doc.createElement('style');
370
400
  style.id = id;
371
401
  style.textContent = css;
372
- document.head.appendChild(style);
402
+ doc.head.appendChild(style);
373
403
  }
374
- function removeInjectedStyles() {
375
- if (typeof document === 'undefined')
404
+ function removeInjectedStyles(doc = document) {
405
+ if (!doc)
376
406
  return;
377
- const link = document.getElementById(STYLES_LINK_ID);
407
+ const link = doc.getElementById(STYLES_LINK_ID);
378
408
  if (link)
379
409
  link.remove();
380
- const style = document.getElementById(STYLES_TAG_ID);
410
+ const style = doc.getElementById(STYLES_TAG_ID);
381
411
  if (style)
382
412
  style.remove();
383
- const defaultStyle = document.getElementById(DEFAULT_STYLES_TAG_ID);
413
+ const defaultStyle = doc.getElementById(DEFAULT_STYLES_TAG_ID);
384
414
  if (defaultStyle)
385
415
  defaultStyle.remove();
386
416
  }
387
417
 
418
+ function canAccessWindow(win) {
419
+ try {
420
+ // Accessing .document on a cross-origin window throws DOMException
421
+ return !!win.document;
422
+ }
423
+ catch {
424
+ return false;
425
+ }
426
+ }
427
+ function resolveRenderContext(target) {
428
+ const sourceWindow = window;
429
+ if (!target || target === 'self') {
430
+ return { targetWindow: window, targetDocument: document, isRemote: false, sourceWindow };
431
+ }
432
+ const candidateWindow = target === 'top' ? window.top : window.parent;
433
+ if (!candidateWindow || candidateWindow === window) {
434
+ // Not inside an iframe — 'parent'/'top' is the same as 'self'
435
+ return { targetWindow: window, targetDocument: document, isRemote: false, sourceWindow };
436
+ }
437
+ if (canAccessWindow(candidateWindow)) {
438
+ return {
439
+ targetWindow: candidateWindow,
440
+ targetDocument: candidateWindow.document,
441
+ isRemote: true,
442
+ sourceWindow,
443
+ };
444
+ }
445
+ console.warn(`[JournyMessaging] Cannot access ${target} window (cross-origin). Falling back to rendering inside the current iframe.`);
446
+ return { targetWindow: window, targetDocument: document, isRemote: false, sourceWindow };
447
+ }
448
+
388
449
  var defaultStyles = "/* Widget Container */\n.journy-message-widget {\n position: fixed;\n z-index: 10000;\n background: white;\n border-radius: 12px;\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);\n overflow: hidden;\n user-select: none;\n transform-origin: bottom right;\n}\n\n.journy-message-widget.journy-message-widget-dragging {\n cursor: grabbing;\n transition: none;\n z-index: 10001;\n}\n\n.journy-message-widget.journy-message-widget-expanded {\n display: flex;\n flex-direction: column;\n min-height: 0;\n}\n\n.journy-message-widget.journy-message-widget-resizing {\n transition: none;\n}\n\n/* Expand/collapse: height and top are animated via inline transition so bottom-right stays fixed */\n\n/* Widget Header */\n.journy-message-widget-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 12px 16px;\n background: #f9fafb;\n border-bottom: 1px solid #e5e7eb;\n user-select: none;\n}\n\n.journy-message-widget-drag-handle {\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 4px 8px;\n margin-right: 8px;\n cursor: grab;\n color: #9ca3af;\n transition: color 0.2s;\n flex-shrink: 0;\n}\n\n.journy-message-widget-drag-handle:active {\n cursor: grabbing;\n color: #6b7280;\n}\n\n.journy-message-widget-drag-handle:hover {\n color: #6b7280;\n}\n\n.journy-message-widget-drag-handle svg {\n display: block;\n}\n\n.journy-message-widget-header-content {\n display: flex;\n align-items: center;\n gap: 10px;\n flex: 1;\n flex-wrap: wrap;\n cursor: pointer;\n}\n\n.journy-message-widget-badge {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-width: 24px;\n height: 24px;\n padding: 0 8px;\n background: #3b82f6;\n color: white;\n border-radius: 12px;\n font-size: 12px;\n font-weight: 600;\n}\n\n.journy-message-widget-title {\n font-size: 14px;\n font-weight: 600;\n color: #111827;\n}\n\n.journy-message-widget-read-count {\n font-size: 12px;\n color: #6b7280;\n font-weight: 400;\n}\n\n.journy-message-widget-controls {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.journy-message-widget-toggle,\n.journy-message-widget-close {\n background: none;\n border: none;\n font-size: 18px;\n line-height: 1;\n cursor: pointer;\n color: #6b7280;\n padding: 4px 8px;\n border-radius: 4px;\n transition: background-color 0.2s, color 0.2s;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n}\n\n.journy-message-widget-toggle:hover,\n.journy-message-widget-close:hover {\n background-color: #e5e7eb;\n color: #374151;\n}\n\n/* Widget Content */\n.journy-message-widget-content {\n padding: 16px;\n flex: 1;\n overflow-y: auto;\n overflow-x: hidden;\n min-height: 0;\n display: flex;\n flex-direction: column;\n}\n\n.journy-message-widget-content--single .journy-message-widget-message {\n flex: 1;\n min-height: 0;\n display: flex;\n flex-direction: column;\n}\n\n.journy-message-widget-content--single .journy-message-widget-message .journy-message-content {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n}\n\n/* Resize Handle */\n.journy-message-widget-resize-handle {\n position: absolute;\n bottom: 0;\n right: 0;\n width: 20px;\n height: 20px;\n cursor: nwse-resize;\n display: flex;\n align-items: center;\n justify-content: center;\n background: linear-gradient(135deg, transparent 0%, transparent 40%, #e5e7eb 40%, #e5e7eb 100%);\n z-index: 10;\n transition: background 0.2s;\n}\n\n.journy-message-widget-resize-handle:hover {\n background: linear-gradient(135deg, transparent 0%, transparent 40%, #d1d5db 40%, #d1d5db 100%);\n}\n\n.journy-message-widget-resize-handle svg {\n width: 12px;\n height: 12px;\n opacity: 0.6;\n}\n\n.journy-message-widget-resize-handle:hover svg {\n opacity: 1;\n}\n\n.journy-message-widget-message {\n padding: 0;\n position: relative;\n min-height: 60px;\n}\n\n.journy-message-widget-message-close {\n position: absolute;\n top: 4px;\n right: 4px;\n width: 24px;\n height: 24px;\n padding: 0;\n border: none;\n background: transparent;\n color: #6b7280;\n font-size: 18px;\n line-height: 1;\n cursor: pointer;\n border-radius: 4px;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: background-color 0.2s, color 0.2s;\n z-index: 1;\n}\n\n.journy-message-widget-message-close:hover {\n background-color: #e5e7eb;\n color: #374151;\n}\n\n.journy-message-widget-message:has(.journy-message-widget-message-close) .journy-message-content {\n padding-right: 28px;\n}\n\n.journy-message-widget-nav {\n background: none;\n border: none;\n font-size: 18px;\n line-height: 1;\n cursor: pointer;\n color: #6b7280;\n padding: 4px 6px;\n border-radius: 4px;\n transition: background-color 0.2s, color 0.2s;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n}\n\n.journy-message-widget-nav:hover:not(:disabled) {\n background-color: #e5e7eb;\n color: #374151;\n}\n\n.journy-message-widget-nav:disabled {\n opacity: 0.4;\n cursor: default;\n}\n\n.journy-message-widget-message-separated {\n margin-bottom: 24px;\n padding-bottom: 24px;\n border-bottom: 1px solid #e5e7eb;\n}\n\n.journy-message-widget-message-count {\n font-size: 12px;\n color: #6b7280;\n font-weight: 400;\n margin-left: 8px;\n}\n\n.journy-message-widget-position {\n padding: 4px 12px;\n font-size: 11px;\n color: #9ca3af;\n font-weight: 500;\n text-align: right;\n flex-shrink: 0;\n}\n\n.journy-message-widget-message.journy-message-info {\n border-left: 4px solid #3b82f6;\n padding-left: 12px;\n}\n\n.journy-message-widget-message.journy-message-success {\n border-left: 4px solid #10b981;\n padding-left: 12px;\n}\n\n.journy-message-widget-message.journy-message-warning {\n border-left: 4px solid #f59e0b;\n padding-left: 12px;\n}\n\n.journy-message-widget-message.journy-message-error {\n border-left: 4px solid #ef4444;\n padding-left: 12px;\n}\n\n.journy-message-widget-message.journy-message-viewed {\n border-left: 4px solid #6b7280;\n padding-left: 12px;\n}\n\n/* Message modal (80vw x 60vh) */\n.journy-message-modal-overlay {\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background-color: rgba(0, 0, 0, 0.5);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 10002;\n padding: 20px;\n}\n\n.journy-message-modal {\n width: 80vw;\n max-width: 80vw;\n height: 60vh;\n max-height: 60vh;\n background: white;\n border-radius: 12px;\n box-shadow: 0 4px 24px rgba(0, 0, 0, 0.2);\n overflow: hidden;\n display: flex;\n flex-direction: column;\n position: relative;\n}\n\n.journy-message-modal-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 16px 24px;\n background: #f9fafb;\n border-bottom: 1px solid #e5e7eb;\n flex-shrink: 0;\n}\n\n.journy-message-modal-title {\n font-size: 18px;\n font-weight: 600;\n color: #111827;\n}\n\n.journy-message-modal-close {\n background: none;\n border: none;\n font-size: 24px;\n line-height: 1;\n cursor: pointer;\n color: #6b7280;\n padding: 4px 8px;\n border-radius: 4px;\n transition: background-color 0.2s, color 0.2s;\n}\n\n.journy-message-modal-close:hover {\n background-color: #e5e7eb;\n color: #374151;\n}\n\n.journy-message-modal .journy-message-widget-message {\n padding: 24px 24px 24px 24px;\n overflow-y: auto;\n flex: 1;\n min-height: 0;\n}\n\n/* Timestamp */\n.journy-message-timestamp {\n font-size: 12px;\n color: #6b7280;\n margin-top: 4px;\n line-height: 1.4;\n}\n\n.journy-message-content-clickable {\n cursor: pointer;\n}\n\n.journy-message-content-clickable:hover {\n opacity: 0.95;\n}\n\n/* Legacy Modal Overlay Styles (kept for backward compatibility) */\n.journy-message-overlay {\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background-color: rgba(0, 0, 0, 0.5);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 10000;\n opacity: 0;\n transition: opacity 0.3s ease-in-out;\n pointer-events: none;\n}\n\n.journy-message-overlay.journy-message-visible {\n opacity: 1;\n pointer-events: all;\n}\n\n.journy-message-popup {\n background: white;\n border-radius: 8px;\n padding: 24px;\n max-width: 500px;\n width: 90%;\n max-height: 80vh;\n overflow-y: auto;\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);\n position: relative;\n transform: scale(0.9);\n transition: transform 0.3s ease-in-out;\n}\n\n.journy-message-overlay.journy-message-visible .journy-message-popup {\n transform: scale(1);\n}\n\n.journy-message-popup.journy-message-info {\n border-top: 4px solid #3b82f6;\n}\n\n.journy-message-popup.journy-message-success {\n border-top: 4px solid #10b981;\n}\n\n.journy-message-popup.journy-message-warning {\n border-top: 4px solid #f59e0b;\n}\n\n.journy-message-popup.journy-message-error {\n border-top: 4px solid #ef4444;\n}\n\n.journy-message-popup.journy-message-viewed {\n border-top: 4px solid #6b7280;\n}\n\n.journy-message-close {\n position: absolute;\n top: 12px;\n right: 12px;\n background: none;\n border: none;\n font-size: 24px;\n line-height: 1;\n cursor: pointer;\n color: #6b7280;\n padding: 4px 8px;\n border-radius: 4px;\n transition: background-color 0.2s, color 0.2s;\n}\n\n.journy-message-close:hover {\n background-color: #f3f4f6;\n color: #374151;\n}\n\n.journy-message-title {\n font-size: 20px;\n font-weight: 600;\n margin-bottom: 12px;\n color: #111827;\n}\n\n.journy-message-content {\n font-size: 16px;\n line-height: 1.6;\n color: #374151;\n margin-bottom: 16px;\n}\n\n.journy-message-content a {\n color: #3b82f6;\n text-decoration: underline;\n transition: color 0.2s;\n}\n\n.journy-message-content a:hover {\n color: #2563eb;\n}\n\n.journy-message-content p {\n margin: 0 0 12px 0;\n}\n\n.journy-message-content p:last-child {\n margin-bottom: 0;\n}\n\n.journy-message-content ul,\n.journy-message-content ol {\n margin: 12px 0;\n padding-left: 24px;\n}\n\n.journy-message-content li {\n margin-bottom: 8px;\n}\n\n/* Headings */\n.journy-message-content h1,\n.journy-message-content h2,\n.journy-message-content h3,\n.journy-message-content h4,\n.journy-message-content h5,\n.journy-message-content h6 {\n margin: 16px 0 12px 0;\n font-weight: 600;\n line-height: 1.3;\n color: #111827;\n}\n\n.journy-message-content h1 {\n font-size: 24px;\n}\n\n.journy-message-content h2 {\n font-size: 20px;\n}\n\n.journy-message-content h3 {\n font-size: 18px;\n}\n\n.journy-message-content h4 {\n font-size: 16px;\n}\n\n.journy-message-content h5,\n.journy-message-content h6 {\n font-size: 14px;\n}\n\n/* Code blocks */\n.journy-message-content code {\n background-color: #f3f4f6;\n color: #ef4444;\n padding: 2px 6px;\n border-radius: 4px;\n font-size: 0.9em;\n font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace;\n}\n\n.journy-message-content pre {\n background-color: #1f2937;\n color: #f9fafb;\n padding: 16px;\n border-radius: 8px;\n overflow-x: auto;\n margin: 12px 0;\n font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace;\n font-size: 14px;\n line-height: 1.5;\n}\n\n.journy-message-content pre code {\n background-color: transparent;\n color: inherit;\n padding: 0;\n border-radius: 0;\n font-size: inherit;\n}\n\n/* Text formatting */\n.journy-message-content u {\n text-decoration: underline;\n}\n\n.journy-message-content s,\n.journy-message-content strike,\n.journy-message-content del {\n text-decoration: line-through;\n}\n\n.journy-message-content strong {\n font-weight: 600;\n}\n\n.journy-message-content em {\n font-style: italic;\n}\n\n/* Blockquotes */\n.journy-message-content blockquote {\n border-left: 4px solid #e5e7eb;\n padding-left: 16px;\n margin: 12px 0;\n color: #6b7280;\n font-style: italic;\n}\n\n/* Tables */\n.journy-message-content table {\n width: 100%;\n border-collapse: collapse;\n margin: 12px 0;\n}\n\n.journy-message-content th,\n.journy-message-content td {\n border: 1px solid #e5e7eb;\n padding: 8px 12px;\n text-align: left;\n}\n\n.journy-message-content th {\n background-color: #f9fafb;\n font-weight: 600;\n}\n\n/* Horizontal rule */\n.journy-message-content hr {\n border: none;\n border-top: 1px solid #e5e7eb;\n margin: 16px 0;\n}\n\n.journy-message-actions {\n display: flex;\n gap: 12px;\n margin-top: 20px;\n flex-wrap: wrap;\n}\n\n.journy-message-action {\n padding: 10px 20px;\n border-radius: 6px;\n font-size: 14px;\n font-weight: 500;\n cursor: pointer;\n border: none;\n transition: all 0.2s;\n}\n\n.journy-message-action-primary {\n background-color: #3b82f6;\n color: white;\n}\n\n.journy-message-action-primary:hover {\n background-color: #2563eb;\n}\n\n.journy-message-action-secondary {\n background-color: #e5e7eb;\n color: #374151;\n}\n\n.journy-message-action-secondary:hover {\n background-color: #d1d5db;\n}\n\n.journy-message-action-link {\n background: none;\n color: #3b82f6;\n text-decoration: underline;\n padding: 10px 0;\n}\n\n.journy-message-action-link:hover {\n color: #2563eb;\n}\n\n/* Settings Panel */\n.journy-settings-panel {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n width: 100%;\n background: white;\n z-index: 10;\n display: flex;\n flex-direction: column;\n transform: translateX(100%);\n transition: transform 0.3s ease;\n}\n\n.journy-settings-panel-open {\n transform: translateX(0);\n}\n\n.journy-settings-panel-closed {\n transform: translateX(100%);\n}\n\n.journy-settings-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 12px 16px;\n background: #f9fafb;\n border-bottom: 1px solid #e5e7eb;\n flex-shrink: 0;\n}\n\n.journy-settings-title {\n font-size: 14px;\n font-weight: 600;\n color: #111827;\n}\n\n.journy-settings-close {\n background: none;\n border: none;\n font-size: 18px;\n line-height: 1;\n cursor: pointer;\n color: #6b7280;\n padding: 4px 8px;\n border-radius: 4px;\n transition: background-color 0.2s, color 0.2s;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n}\n\n.journy-settings-close:hover {\n background-color: #e5e7eb;\n color: #374151;\n}\n\n.journy-settings-body {\n padding: 16px;\n overflow-y: auto;\n flex: 1;\n}\n\n.journy-settings-item {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 12px 0;\n border-bottom: 1px solid #f3f4f6;\n}\n\n.journy-settings-item:last-child {\n border-bottom: none;\n}\n\n.journy-settings-label {\n font-size: 13px;\n font-weight: 500;\n color: #374151;\n}\n\n.journy-settings-select {\n padding: 6px 10px;\n border: 1px solid #d1d5db;\n border-radius: 6px;\n font-size: 13px;\n color: #374151;\n background: white;\n cursor: pointer;\n outline: none;\n transition: border-color 0.2s;\n}\n\n.journy-settings-select:focus {\n border-color: #3b82f6;\n}\n\n.journy-settings-toggle {\n position: relative;\n width: 40px;\n height: 22px;\n border-radius: 11px;\n border: none;\n background: #d1d5db;\n cursor: pointer;\n padding: 0;\n transition: background-color 0.2s;\n flex-shrink: 0;\n}\n\n.journy-settings-toggle-on {\n background: #3b82f6;\n}\n\n.journy-settings-toggle-knob {\n position: absolute;\n top: 2px;\n left: 2px;\n width: 18px;\n height: 18px;\n border-radius: 50%;\n background: white;\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);\n transition: transform 0.2s;\n}\n\n.journy-settings-toggle-on .journy-settings-toggle-knob {\n transform: translateX(18px);\n}\n\n.journy-settings-item-vertical {\n flex-direction: column;\n align-items: flex-start;\n gap: 6px;\n}\n\n.journy-settings-input {\n width: 100%;\n padding: 6px 10px;\n border: 1px solid #d1d5db;\n border-radius: 6px;\n font-size: 13px;\n color: #374151;\n background: white;\n outline: none;\n transition: border-color 0.2s;\n box-sizing: border-box;\n}\n\n.journy-settings-input:focus {\n border-color: #3b82f6;\n}\n\n.journy-settings-input::placeholder {\n color: #9ca3af;\n}\n\n.journy-settings-value {\n font-size: 13px;\n color: #6b7280;\n font-weight: 400;\n max-width: 60%;\n text-align: right;\n word-break: break-all;\n}\n\n.journy-message-widget-empty {\n display: flex;\n align-items: center;\n justify-content: center;\n color: #9ca3af;\n font-size: 14px;\n flex: 1;\n min-height: 80px;\n}\n\n.journy-settings-advanced-btn {\n background: none;\n border: none;\n font-size: 13px;\n font-weight: 500;\n color: #6b7280;\n cursor: pointer;\n padding: 0;\n transition: color 0.2s;\n}\n\n.journy-settings-advanced-btn:hover {\n color: #374151;\n}\n\n.journy-message-widget-settings-btn {\n background: none;\n border: none;\n font-size: 16px;\n line-height: 1;\n cursor: pointer;\n color: #6b7280;\n padding: 4px 8px;\n border-radius: 4px;\n transition: background-color 0.2s, color 0.2s;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n}\n\n.journy-message-widget-settings-btn:hover {\n background-color: #e5e7eb;\n color: #374151;\n}\n\n/* Responsive design */\n@media (max-width: 640px) {\n .journy-message-widget {\n min-width: calc(100vw - 32px);\n max-width: calc(100vw - 32px);\n left: 16px !important;\n right: 16px !important;\n }\n\n .journy-message-popup {\n width: 95%;\n padding: 20px;\n }\n\n .journy-message-title {\n font-size: 18px;\n }\n\n .journy-message-content {\n font-size: 14px;\n }\n\n .journy-message-actions {\n flex-direction: column;\n }\n\n .journy-message-action {\n width: 100%;\n }\n}\n";
389
450
 
390
451
  function useMessagingStore(store) {
@@ -397,7 +458,7 @@ function useMessagingStore(store) {
397
458
  return state;
398
459
  }
399
460
 
400
- /*! @license DOMPurify 3.3.1 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.3.1/LICENSE */
461
+ /*! @license DOMPurify 3.3.3 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.3.3/LICENSE */
401
462
 
402
463
  const {
403
464
  entries,
@@ -695,7 +756,7 @@ const _createHooksMap = function _createHooksMap() {
695
756
  function createDOMPurify() {
696
757
  let window = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getGlobal();
697
758
  const DOMPurify = root => createDOMPurify(root);
698
- DOMPurify.version = '3.3.1';
759
+ DOMPurify.version = '3.3.3';
699
760
  DOMPurify.removed = [];
700
761
  if (!window || !window.document || window.document.nodeType !== NODE_TYPE.document || !window.Element) {
701
762
  // Not running in a browser, provide a factory function
@@ -990,7 +1051,7 @@ function createDOMPurify() {
990
1051
  /* Parse profile info */
991
1052
  if (USE_PROFILES) {
992
1053
  ALLOWED_TAGS = addToSet({}, text);
993
- ALLOWED_ATTR = [];
1054
+ ALLOWED_ATTR = create(null);
994
1055
  if (USE_PROFILES.html === true) {
995
1056
  addToSet(ALLOWED_TAGS, html$1);
996
1057
  addToSet(ALLOWED_ATTR, html);
@@ -1011,6 +1072,13 @@ function createDOMPurify() {
1011
1072
  addToSet(ALLOWED_ATTR, xml);
1012
1073
  }
1013
1074
  }
1075
+ /* Prevent function-based ADD_ATTR / ADD_TAGS from leaking across calls */
1076
+ if (!objectHasOwnProperty(cfg, 'ADD_TAGS')) {
1077
+ EXTRA_ELEMENT_HANDLING.tagCheck = null;
1078
+ }
1079
+ if (!objectHasOwnProperty(cfg, 'ADD_ATTR')) {
1080
+ EXTRA_ELEMENT_HANDLING.attributeCheck = null;
1081
+ }
1014
1082
  /* Merge configuration parameters */
1015
1083
  if (cfg.ADD_TAGS) {
1016
1084
  if (typeof cfg.ADD_TAGS === 'function') {
@@ -1408,6 +1476,10 @@ function createDOMPurify() {
1408
1476
  */
1409
1477
  // eslint-disable-next-line complexity
1410
1478
  const _isValidAttribute = function _isValidAttribute(lcTag, lcName, value) {
1479
+ /* FORBID_ATTR must always win, even if ADD_ATTR predicate would allow it */
1480
+ if (FORBID_ATTR[lcName]) {
1481
+ return false;
1482
+ }
1411
1483
  /* Make sure attribute cannot clobber */
1412
1484
  if (SANITIZE_DOM && (lcName === 'id' || lcName === 'name') && (value in document || value in formElement)) {
1413
1485
  return false;
@@ -1500,7 +1572,7 @@ function createDOMPurify() {
1500
1572
  value = SANITIZE_NAMED_PROPS_PREFIX + value;
1501
1573
  }
1502
1574
  /* Work around a security issue with comments inside attributes */
1503
- if (SAFE_FOR_XML && regExpTest(/((--!?|])>)|<\/(style|title|textarea)/i, value)) {
1575
+ if (SAFE_FOR_XML && regExpTest(/((--!?|])>)|<\/(style|script|title|xmp|textarea|noscript|iframe|noembed|noframes)/i, value)) {
1504
1576
  _removeAttribute(name, currentNode);
1505
1577
  continue;
1506
1578
  }
@@ -1915,7 +1987,7 @@ const SettingsPanel = ({ isOpen, onClose, settings, onSettingsChange, configInfo
1915
1987
  showAdvanced && (React.createElement(React.Fragment, null,
1916
1988
  React.createElement("div", { className: "journy-settings-item journy-settings-item-vertical" },
1917
1989
  React.createElement("label", { className: "journy-settings-label" }, "API endpoint"),
1918
- React.createElement("input", { type: "text", className: "journy-settings-input", value: settings.apiEndpoint, onChange: (e) => update({ apiEndpoint: e.target.value }), placeholder: "https://jtm.journy.io" })),
1990
+ React.createElement("input", { type: "text", className: "journy-settings-input", value: settings.apiEndpoint, onChange: (e) => update({ apiEndpoint: e.target.value }), placeholder: DEFAULT_API_ENDPOINT })),
1919
1991
  configInfo && (React.createElement(React.Fragment, null,
1920
1992
  React.createElement("div", { className: "journy-settings-item" },
1921
1993
  React.createElement("label", { className: "journy-settings-label" }, "Entity type"),
@@ -1954,6 +2026,17 @@ const ResizeHandle = ({ onMouseDown, title = 'Resize', className = 'journy-messa
1954
2026
  React.createElement("svg", { width: ICON_SIZE, height: ICON_SIZE, viewBox: `0 0 ${ICON_SIZE} ${ICON_SIZE}`, fill: "none", xmlns: "http://www.w3.org/2000/svg" },
1955
2027
  React.createElement("path", { d: `${PATH_MAIN} ${PATH_INNER}`, stroke: "#9ca3af", strokeWidth: STROKE_WIDTH, strokeLinecap: "round" }))));
1956
2028
 
2029
+ const defaultContext = {
2030
+ targetWindow: typeof window !== 'undefined' ? window : null,
2031
+ targetDocument: typeof document !== 'undefined' ? document : null,
2032
+ isRemote: false,
2033
+ sourceWindow: typeof window !== 'undefined' ? window : null,
2034
+ };
2035
+ const RenderCtx = React.createContext(defaultContext);
2036
+ function useRenderContext() {
2037
+ return React.useContext(RenderCtx);
2038
+ }
2039
+
1957
2040
  const COLLAPSED_WIDTH = 300;
1958
2041
  const COLLAPSED_HEIGHT = 80;
1959
2042
  const WIDGET_MODE_EXPANDED_WIDTH = 340;
@@ -1966,12 +2049,13 @@ const MIN_LIST_WIDTH = 300;
1966
2049
  const MIN_LIST_HEIGHT = 200;
1967
2050
  const MIN_POSITION_THRESHOLD = 0;
1968
2051
  function useWidgetDragResize({ isCollapsed, isListMode }) {
2052
+ const { targetWindow, targetDocument } = useRenderContext();
1969
2053
  const [position, setPosition] = useState(() => {
1970
2054
  const saved = getItem('widget_position');
1971
2055
  if (saved && typeof saved.left === 'number' && typeof saved.top === 'number') {
1972
2056
  return {
1973
- left: Math.max(0, Math.min(saved.left, window.innerWidth - COLLAPSED_WIDTH)),
1974
- top: Math.max(0, Math.min(saved.top, window.innerHeight - COLLAPSED_HEIGHT)),
2057
+ left: Math.max(0, Math.min(saved.left, targetWindow.innerWidth - COLLAPSED_WIDTH)),
2058
+ top: Math.max(0, Math.min(saved.top, targetWindow.innerHeight - COLLAPSED_HEIGHT)),
1975
2059
  };
1976
2060
  }
1977
2061
  if (saved && typeof saved.x === 'number' && typeof saved.y === 'number') {
@@ -1981,8 +2065,8 @@ function useWidgetDragResize({ isCollapsed, isListMode }) {
1981
2065
  };
1982
2066
  }
1983
2067
  return {
1984
- left: window.innerWidth - DEFAULT_EDGE_OFFSET - COLLAPSED_WIDTH,
1985
- top: window.innerHeight - DEFAULT_EDGE_OFFSET - COLLAPSED_HEIGHT,
2068
+ left: targetWindow.innerWidth - DEFAULT_EDGE_OFFSET - COLLAPSED_WIDTH,
2069
+ top: targetWindow.innerHeight - DEFAULT_EDGE_OFFSET - COLLAPSED_HEIGHT,
1986
2070
  };
1987
2071
  });
1988
2072
  const [size, setSize] = useState({ width: LIST_MODE_DEFAULT_WIDTH, height: LIST_MODE_DEFAULT_HEIGHT });
@@ -2008,18 +2092,18 @@ function useWidgetDragResize({ isCollapsed, isListMode }) {
2008
2092
  const savedSize = getItem('widget_size');
2009
2093
  if (savedSize) {
2010
2094
  setSize({
2011
- width: Math.max(MIN_LIST_WIDTH, Math.min(savedSize.width, window.innerWidth - VIEWPORT_PADDING)),
2012
- height: Math.max(MIN_LIST_HEIGHT, Math.min(savedSize.height, window.innerHeight - VIEWPORT_PADDING)),
2095
+ width: Math.max(MIN_LIST_WIDTH, Math.min(savedSize.width, targetWindow.innerWidth - VIEWPORT_PADDING)),
2096
+ height: Math.max(MIN_LIST_HEIGHT, Math.min(savedSize.height, targetWindow.innerHeight - VIEWPORT_PADDING)),
2013
2097
  });
2014
2098
  }
2015
2099
  }, []);
2016
2100
  // Clamp position so widget stays on screen
2017
2101
  useEffect(() => {
2018
2102
  setPosition(prev => ({
2019
- left: Math.max(0, Math.min(prev.left, window.innerWidth - currentWidth)),
2020
- top: Math.max(0, Math.min(prev.top, window.innerHeight - currentHeight)),
2103
+ left: Math.max(0, Math.min(prev.left, targetWindow.innerWidth - currentWidth)),
2104
+ top: Math.max(0, Math.min(prev.top, targetWindow.innerHeight - currentHeight)),
2021
2105
  }));
2022
- }, [currentWidth, currentHeight]);
2106
+ }, [currentWidth, currentHeight, targetWindow]);
2023
2107
  // Save position when it changes
2024
2108
  useEffect(() => {
2025
2109
  if (position.left > MIN_POSITION_THRESHOLD || position.top > MIN_POSITION_THRESHOLD) {
@@ -2036,13 +2120,13 @@ function useWidgetDragResize({ isCollapsed, isListMode }) {
2036
2120
  useEffect(() => {
2037
2121
  const handleResize = () => {
2038
2122
  setPosition(prev => ({
2039
- left: Math.max(0, Math.min(prev.left, window.innerWidth - currentWidth)),
2040
- top: Math.max(0, Math.min(prev.top, window.innerHeight - currentHeight)),
2123
+ left: Math.max(0, Math.min(prev.left, targetWindow.innerWidth - currentWidth)),
2124
+ top: Math.max(0, Math.min(prev.top, targetWindow.innerHeight - currentHeight)),
2041
2125
  }));
2042
2126
  };
2043
- window.addEventListener('resize', handleResize);
2044
- return () => window.removeEventListener('resize', handleResize);
2045
- }, [currentWidth, currentHeight]);
2127
+ targetWindow.addEventListener('resize', handleResize);
2128
+ return () => targetWindow.removeEventListener('resize', handleResize);
2129
+ }, [currentWidth, currentHeight, targetWindow]);
2046
2130
  const handleMouseDown = (e) => {
2047
2131
  const target = e.target;
2048
2132
  if (!target.closest('.journy-message-widget-drag-handle'))
@@ -2067,15 +2151,15 @@ function useWidgetDragResize({ isCollapsed, isListMode }) {
2067
2151
  const newLeft = e.clientX - dragOffset.x;
2068
2152
  const newTop = e.clientY - dragOffset.y;
2069
2153
  setPosition({
2070
- left: Math.max(0, Math.min(newLeft, window.innerWidth - currentWidth)),
2071
- top: Math.max(0, Math.min(newTop, window.innerHeight - currentHeight)),
2154
+ left: Math.max(0, Math.min(newLeft, targetWindow.innerWidth - currentWidth)),
2155
+ top: Math.max(0, Math.min(newTop, targetWindow.innerHeight - currentHeight)),
2072
2156
  });
2073
2157
  }
2074
2158
  else if (isResizing && resizeStart) {
2075
2159
  const deltaX = e.clientX - resizeStart.x;
2076
2160
  const deltaY = e.clientY - resizeStart.y;
2077
- const maxWidth = window.innerWidth - position.left;
2078
- const maxHeight = window.innerHeight - position.top;
2161
+ const maxWidth = targetWindow.innerWidth - position.left;
2162
+ const maxHeight = targetWindow.innerHeight - position.top;
2079
2163
  const newWidth = Math.max(MIN_LIST_WIDTH, Math.min(resizeStart.width + deltaX, maxWidth));
2080
2164
  const newHeight = Math.max(MIN_LIST_HEIGHT, Math.min(resizeStart.height + deltaY, maxHeight));
2081
2165
  setSize({ width: newWidth, height: newHeight });
@@ -2092,13 +2176,13 @@ function useWidgetDragResize({ isCollapsed, isListMode }) {
2092
2176
  setIsResizing(false);
2093
2177
  setResizeStart(null);
2094
2178
  };
2095
- document.addEventListener('mousemove', handleMouseMove);
2096
- document.addEventListener('mouseup', handleMouseUp);
2179
+ targetDocument.addEventListener('mousemove', handleMouseMove);
2180
+ targetDocument.addEventListener('mouseup', handleMouseUp);
2097
2181
  return () => {
2098
- document.removeEventListener('mousemove', handleMouseMove);
2099
- document.removeEventListener('mouseup', handleMouseUp);
2182
+ targetDocument.removeEventListener('mousemove', handleMouseMove);
2183
+ targetDocument.removeEventListener('mouseup', handleMouseUp);
2100
2184
  };
2101
- }, [isDragging, isResizing, dragOffset, resizeStart, position, currentWidth, currentHeight]);
2185
+ }, [isDragging, isResizing, dragOffset, resizeStart, position, currentWidth, currentHeight, targetDocument]);
2102
2186
  const handleResizeStart = (e) => {
2103
2187
  e.stopPropagation();
2104
2188
  if (!widgetRef.current)
@@ -2128,8 +2212,14 @@ function useWidgetDragResize({ isCollapsed, isListMode }) {
2128
2212
  const TRANSITION_DURATION_S = 0.25;
2129
2213
 
2130
2214
  const SETTINGS_STORAGE_KEY = 'widget_settings';
2131
- /** Mark message as received when its element is visible in the viewport. */
2215
+ /**
2216
+ * Mark message as received when its element is visible in the viewport.
2217
+ * When running inside an iframe without remote rendering (renderTarget: 'self'),
2218
+ * the IntersectionObserver would use the tiny iframe viewport and fire immediately,
2219
+ * so we skip it and rely on explicit user interactions instead.
2220
+ */
2132
2221
  function useMessageVisibility(ref, messageId, received, onMessageReceived) {
2222
+ const { isRemote } = useRenderContext();
2133
2223
  const hasFiredRef = useRef(false);
2134
2224
  useEffect(() => {
2135
2225
  if (received) {
@@ -2140,6 +2230,17 @@ function useMessageVisibility(ref, messageId, received, onMessageReceived) {
2140
2230
  const el = ref.current;
2141
2231
  if (!el || !onMessageReceived)
2142
2232
  return;
2233
+ // Skip IntersectionObserver when inside an iframe but rendering to self —
2234
+ // the iframe viewport is likely too small, causing false positives.
2235
+ let isInIframe;
2236
+ try {
2237
+ isInIframe = window !== window.top;
2238
+ }
2239
+ catch {
2240
+ isInIframe = true;
2241
+ }
2242
+ if (isInIframe && !isRemote)
2243
+ return;
2143
2244
  const observer = new IntersectionObserver((entries) => {
2144
2245
  const [entry] = entries;
2145
2246
  if (entry?.isIntersecting && !hasFiredRef.current) {
@@ -2150,7 +2251,7 @@ function useMessageVisibility(ref, messageId, received, onMessageReceived) {
2150
2251
  }, { threshold: 0.1, root: null });
2151
2252
  observer.observe(el);
2152
2253
  return () => observer.disconnect();
2153
- }, [messageId, received, onMessageReceived]);
2254
+ }, [messageId, received, onMessageReceived, isRemote]);
2154
2255
  }
2155
2256
  /** Wraps a message row with a ref and viewport visibility observer to mark as received when visible. */
2156
2257
  const MessageRow = ({ message, isSeparated, onMessageReceived, onDismissMessage, children }) => {
@@ -2170,7 +2271,6 @@ function useMessageCounts(messages) {
2170
2271
  const unreadCount = messages.filter((m) => !m.received).length;
2171
2272
  const readCount = messages.filter((m) => m.received).length;
2172
2273
  return {
2173
- totalCount: messages.length,
2174
2274
  unreadCount,
2175
2275
  readCount,
2176
2276
  };
@@ -2178,7 +2278,7 @@ function useMessageCounts(messages) {
2178
2278
  }
2179
2279
  const MessageWidget = ({ store, onClose, onCloseWidget, onLinkClick, onToggleExpand, onMessageReceived, onDismissMessage, onNextMessage, onPrevMessage, onSettingsChange, configInfo, }) => {
2180
2280
  const { messages, currentMessage, isCollapsed, displayMode, widgetVisible } = useMessagingStore(store);
2181
- const { totalCount, unreadCount, readCount } = useMessageCounts(messages);
2281
+ const { unreadCount, readCount } = useMessageCounts(messages);
2182
2282
  const [modalMessage, setModalMessage] = useState(null);
2183
2283
  const [isSettingsOpen, setIsSettingsOpen] = useState(false);
2184
2284
  const [settings, setSettings] = useState(() => {
@@ -2307,7 +2407,7 @@ const MessageWidget = ({ store, onClose, onCloseWidget, onLinkClick, onToggleExp
2307
2407
  allMessagesToShow.length,
2308
2408
  " messages)"))))),
2309
2409
  React.createElement("div", { className: "journy-message-widget-controls" },
2310
- !isCollapsed && (React.createElement("button", { className: "journy-message-widget-settings-btn", onClick: (e) => {
2410
+ isDebugSettings() && !isCollapsed && (React.createElement("button", { className: "journy-message-widget-settings-btn", onClick: (e) => {
2311
2411
  e.stopPropagation();
2312
2412
  e.preventDefault();
2313
2413
  setIsSettingsOpen(true);
@@ -2328,7 +2428,7 @@ const MessageWidget = ({ store, onClose, onCloseWidget, onLinkClick, onToggleExp
2328
2428
  }, onMouseDown: (e) => {
2329
2429
  e.stopPropagation();
2330
2430
  }, title: "Close widget", "aria-label": "Close" }, "\u00D7")),
2331
- !isCollapsed && isListMode === false && allMessagesToShow.length > 1 && (React.createElement(React.Fragment, null,
2431
+ !isCollapsed && !isListMode && allMessagesToShow.length > 1 && (React.createElement(React.Fragment, null,
2332
2432
  React.createElement("button", { className: "journy-message-widget-nav", onClick: (e) => {
2333
2433
  e.stopPropagation();
2334
2434
  e.preventDefault();
@@ -2354,7 +2454,7 @@ const MessageWidget = ({ store, onClose, onCloseWidget, onLinkClick, onToggleExp
2354
2454
  React.createElement("div", { className: "journy-message-widget-content journy-message-widget-empty" },
2355
2455
  React.createElement("p", null, "No unread messages")),
2356
2456
  isListMode && React.createElement(ResizeHandle, { onMouseDown: handleResizeStart }))),
2357
- !isCollapsed && (React.createElement(SettingsPanel, { isOpen: isSettingsOpen, onClose: () => setIsSettingsOpen(false), settings: settings, onSettingsChange: handleSettingsChange, configInfo: configInfo })),
2457
+ isDebugSettings() && !isCollapsed && (React.createElement(SettingsPanel, { isOpen: isSettingsOpen, onClose: () => setIsSettingsOpen(false), settings: settings, onSettingsChange: handleSettingsChange, configInfo: configInfo })),
2358
2458
  modalMessage && (React.createElement(MessagePopup, { message: modalMessage, onClose: () => setModalMessage(null), onLinkClick: onLinkClick }))));
2359
2459
  };
2360
2460
 
@@ -2393,7 +2493,7 @@ const SEND_ANALYTICS_PATH = "/frontend/v1/b";
2393
2493
  class AnalyticsClient {
2394
2494
  constructor(config) {
2395
2495
  this.config = config;
2396
- this.analyticsHost = config.apiEndpoint || "https://jtm.journy.io";
2496
+ this.analyticsHost = config.apiEndpoint || DEFAULT_API_ENDPOINT;
2397
2497
  }
2398
2498
  get trackIdentity() {
2399
2499
  return {
@@ -2426,19 +2526,6 @@ class AnalyticsClient {
2426
2526
  }
2427
2527
  }
2428
2528
 
2429
- const DEFAULT_API_ENDPOINT = 'https://jtm.journy.io';
2430
- const DEFAULT_POLLING_INTERVAL = 30000;
2431
- const ROOT_ELEMENT_ID = 'journy-messages-root';
2432
- const DEFAULT_STYLE_ID = 'journy-messages-default';
2433
- const REACT_CHECK_INTERVAL = 100;
2434
- const REACT_WAIT_TIMEOUT = 10000;
2435
- const MESSAGE_CLOSE_DELAY = 300;
2436
- const STORAGE_KEYS = {
2437
- WIDGET_SETTINGS: 'widget_settings',
2438
- WIDGET_VISIBLE: 'widget_visible',
2439
- WIDGET_COLLAPSED: 'widget_collapsed',
2440
- CURRENT_MESSAGE_ID: 'current_message_id',
2441
- };
2442
2529
  class JournyMessaging {
2443
2530
  constructor(config) {
2444
2531
  this.initialized = false;
@@ -2447,6 +2534,7 @@ class JournyMessaging {
2447
2534
  this.reactRootContainer = null;
2448
2535
  this.rootElementId = ROOT_ELEMENT_ID;
2449
2536
  this.unsubscribePersistence = null;
2537
+ this.beforeUnloadHandler = null;
2450
2538
  this.config = {
2451
2539
  apiEndpoint: DEFAULT_API_ENDPOINT,
2452
2540
  pollingInterval: DEFAULT_POLLING_INTERVAL,
@@ -2458,22 +2546,32 @@ class JournyMessaging {
2458
2546
  if (!this.config.entityType) {
2459
2547
  throw new Error('entityType is required');
2460
2548
  }
2549
+ this.renderCtx = resolveRenderContext(config.renderTarget);
2550
+ // Clean up parent DOM when iframe unloads
2551
+ if (this.renderCtx.isRemote) {
2552
+ this.beforeUnloadHandler = () => this.destroy();
2553
+ window.addEventListener('beforeunload', this.beforeUnloadHandler);
2554
+ }
2461
2555
  this.apiClient = new ApiClient(this.config);
2462
2556
  this.messageQueue = new MessageQueue();
2463
2557
  this.analyticsClient = new AnalyticsClient(this.config);
2464
2558
  this.eventTracker = new EventTracker(this.analyticsClient);
2465
- // Build default settings from config, then overlay any saved settings
2466
- const configDefaults = {
2467
- ...DEFAULT_WIDGET_SETTINGS,
2468
- pollingInterval: this.config.pollingInterval || DEFAULT_WIDGET_SETTINGS.pollingInterval,
2469
- displayMode: this.config.displayMode || DEFAULT_WIDGET_SETTINGS.displayMode,
2470
- apiEndpoint: this.config.apiEndpoint || DEFAULT_WIDGET_SETTINGS.apiEndpoint,
2471
- styles: this.config.styles || DEFAULT_WIDGET_SETTINGS.styles,
2559
+ // Build default settings from config, then overlay any saved settings.
2560
+ // Explicit config values (provided by the host) always take precedence over
2561
+ // saved localStorage settings so that changing config.js is never silently
2562
+ // ignored.
2563
+ const configOverrides = {
2564
+ ...(config.pollingInterval != null && { pollingInterval: config.pollingInterval }),
2565
+ ...(config.displayMode != null && { displayMode: config.displayMode }),
2566
+ ...(config.apiEndpoint != null && { apiEndpoint: config.apiEndpoint }),
2567
+ ...(config.styles != null && { styles: config.styles }),
2472
2568
  };
2473
2569
  const savedSettings = getItem(STORAGE_KEYS.WIDGET_SETTINGS);
2474
- this.widgetSettings = savedSettings
2475
- ? { ...configDefaults, ...savedSettings }
2476
- : configDefaults;
2570
+ this.widgetSettings = {
2571
+ ...DEFAULT_WIDGET_SETTINGS,
2572
+ ...(savedSettings || {}),
2573
+ ...configOverrides,
2574
+ };
2477
2575
  // Apply settings back to config so they take effect
2478
2576
  this.config.pollingInterval = this.widgetSettings.pollingInterval;
2479
2577
  this.config.apiEndpoint = this.widgetSettings.apiEndpoint;
@@ -2508,12 +2606,48 @@ class JournyMessaging {
2508
2606
  return;
2509
2607
  // Load existing messages
2510
2608
  await this.loadMessages();
2609
+ // In debug mode, inject mock messages if none were loaded so the widget shows instantly
2610
+ if (isDebugMockMessages() && this.messageQueue.getActiveCount() === 0) {
2611
+ this.injectDebugMessages();
2612
+ }
2511
2613
  // Set up polling or WebSocket connection
2512
2614
  this.startPolling();
2513
2615
  // Initialize UI
2514
2616
  this.initializeUI();
2515
2617
  this.initialized = true;
2516
2618
  }
2619
+ injectDebugMessages() {
2620
+ const now = new Date().toISOString();
2621
+ const mockMessages = [
2622
+ {
2623
+ id: 'debug-msg-1',
2624
+ appId: 'debug',
2625
+ status: 'sent',
2626
+ scope: this.config.entityType,
2627
+ message: '<h3>Welcome!</h3><p>This is a <strong>debug preview</strong> message. It only appears when <code>window.__JOURNY_DEBUG__</code> is enabled.</p>',
2628
+ received: false,
2629
+ expired: false,
2630
+ createdAt: now,
2631
+ },
2632
+ {
2633
+ id: 'debug-msg-2',
2634
+ appId: 'debug',
2635
+ status: 'sent',
2636
+ scope: this.config.entityType,
2637
+ message: '<h3>Second Message</h3><p>Use this to test <a href="https://example.com">links</a>, navigation, and layout.</p>',
2638
+ received: true,
2639
+ expired: false,
2640
+ createdAt: now,
2641
+ },
2642
+ ];
2643
+ this.messageQueue.addMessages(mockMessages);
2644
+ this.store.setState({
2645
+ messages: this.messageQueue.getAllMessages(),
2646
+ widgetVisible: true,
2647
+ isCollapsed: false,
2648
+ });
2649
+ this.displayNextMessage();
2650
+ }
2517
2651
  async loadMessages() {
2518
2652
  try {
2519
2653
  const messages = await this.apiClient.getUnreadMessages();
@@ -2559,23 +2693,24 @@ class JournyMessaging {
2559
2693
  }, this.config.pollingInterval || DEFAULT_POLLING_INTERVAL);
2560
2694
  }
2561
2695
  applyStyles(stylesConfig) {
2562
- removeInjectedStyles();
2696
+ const doc = this.renderCtx.targetDocument;
2697
+ removeInjectedStyles(doc);
2563
2698
  if (stylesConfig === 'none') ;
2564
2699
  else if (stylesConfig && stylesConfig !== 'default') {
2565
2700
  if ('url' in stylesConfig && stylesConfig.url) {
2566
- injectStyleLink(stylesConfig.url);
2701
+ injectStyleLink(stylesConfig.url, doc);
2567
2702
  }
2568
2703
  else if ('css' in stylesConfig && stylesConfig.css) {
2569
- injectStyleTag(stylesConfig.css);
2704
+ injectStyleTag(stylesConfig.css, undefined, doc);
2570
2705
  }
2571
2706
  }
2572
2707
  else {
2573
- injectStyleTag(defaultStyles, DEFAULT_STYLE_ID);
2708
+ injectStyleTag(defaultStyles, DEFAULT_STYLE_ID, doc);
2574
2709
  }
2575
2710
  }
2576
2711
  initializeUI() {
2577
- // Create root element for React
2578
- const rootElement = createRootElement(this.rootElementId);
2712
+ // Create root element for React in the target document
2713
+ const rootElement = createRootElement(this.rootElementId, this.renderCtx.targetDocument);
2579
2714
  this.applyStyles(this.widgetSettings.styles);
2580
2715
  // Always render UI (even if no messages) so widget can show/hide itself
2581
2716
  // Dynamically import React and render component
@@ -2633,7 +2768,8 @@ class JournyMessaging {
2633
2768
  accountId: this.config.accountId,
2634
2769
  },
2635
2770
  };
2636
- const element = React.createElement(MessageWidget$1, props);
2771
+ const widgetElement = React.createElement(MessageWidget$1, props);
2772
+ const element = React.createElement(RenderCtx.Provider, { value: this.renderCtx }, widgetElement);
2637
2773
  // Use React 18 createRoot if available, otherwise fall back to render
2638
2774
  if (ReactDOM.createRoot) {
2639
2775
  if (!this.reactRoot) {
@@ -2819,8 +2955,12 @@ class JournyMessaging {
2819
2955
  this.reactRoot = null;
2820
2956
  this.reactRootContainer = null;
2821
2957
  }
2822
- removeRootElement(this.rootElementId);
2823
- removeInjectedStyles();
2958
+ if (this.beforeUnloadHandler) {
2959
+ window.removeEventListener('beforeunload', this.beforeUnloadHandler);
2960
+ this.beforeUnloadHandler = null;
2961
+ }
2962
+ removeRootElement(this.rootElementId, this.renderCtx.targetDocument);
2963
+ removeInjectedStyles(this.renderCtx.targetDocument);
2824
2964
  }
2825
2965
  }
2826
2966