@microsoft/omnichannel-chat-widget 2.0.0 → 2.0.1-main.38e7390

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.
Files changed (30) hide show
  1. package/README.md +4 -2
  2. package/lib/cjs/common/telemetry/TelemetryConstants.js +6 -0
  3. package/lib/cjs/components/chatbuttonstateful/ChatButtonStateful.js +42 -25
  4. package/lib/cjs/components/livechatwidget/common/endChat.js +39 -0
  5. package/lib/cjs/components/livechatwidget/common/initWebChatComposer.js +11 -6
  6. package/lib/cjs/components/livechatwidget/common/recoveryEligibilityTelemetry.js +60 -0
  7. package/lib/cjs/components/livechatwidget/common/startChat.js +107 -33
  8. package/lib/cjs/components/livechatwidget/livechatwidgetstateful/LiveChatWidgetStateful.js +8 -6
  9. package/lib/cjs/components/webchatcontainerstateful/WebChatContainerStateful.js +2 -2
  10. package/lib/cjs/components/webchatcontainerstateful/common/defaultProps/defaultMiddlewareLocalizedTexts.js +2 -1
  11. package/lib/cjs/components/webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/attachments/AdaptiveCardAccessibilityWrapper.js +16 -2
  12. package/lib/cjs/components/webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/attachmentSentAnnouncementMiddleware.js +68 -40
  13. package/lib/cjs/components/webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/attachmentUploadValidatorMiddleware.js +0 -27
  14. package/lib/esm/common/telemetry/TelemetryConstants.js +6 -0
  15. package/lib/esm/components/chatbuttonstateful/ChatButtonStateful.js +42 -25
  16. package/lib/esm/components/livechatwidget/common/endChat.js +39 -0
  17. package/lib/esm/components/livechatwidget/common/initWebChatComposer.js +12 -7
  18. package/lib/esm/components/livechatwidget/common/recoveryEligibilityTelemetry.js +52 -0
  19. package/lib/esm/components/livechatwidget/common/startChat.js +104 -31
  20. package/lib/esm/components/livechatwidget/livechatwidgetstateful/LiveChatWidgetStateful.js +8 -6
  21. package/lib/esm/components/webchatcontainerstateful/WebChatContainerStateful.js +2 -2
  22. package/lib/esm/components/webchatcontainerstateful/common/defaultProps/defaultMiddlewareLocalizedTexts.js +2 -1
  23. package/lib/esm/components/webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/attachments/AdaptiveCardAccessibilityWrapper.js +16 -2
  24. package/lib/esm/components/webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/attachmentSentAnnouncementMiddleware.js +65 -39
  25. package/lib/esm/components/webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/attachmentUploadValidatorMiddleware.js +0 -27
  26. package/lib/types/common/telemetry/TelemetryConstants.d.ts +3 -0
  27. package/lib/types/components/livechatwidget/common/recoveryEligibilityTelemetry.d.ts +17 -0
  28. package/lib/types/components/webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/attachmentSentAnnouncementMiddleware.d.ts +11 -4
  29. package/lib/types/contexts/common/ILiveChatWidgetLocalizedTexts.d.ts +9 -2
  30. package/package.json +7 -6
@@ -3,31 +3,41 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.default = void 0;
6
+ exports.getFileSentMessage = exports.default = exports.createAttachmentSentAnnouncementMiddleware = exports.ATTACHMENT_ANNOUNCEMENT_REGION_ID = void 0;
7
7
  var _IWebChatAction = require("../../../interfaces/IWebChatAction");
8
8
  var _WebChatActionType = require("../../enums/WebChatActionType");
9
9
  /******
10
10
  * AttachmentSentAnnouncementMiddleware
11
11
  *
12
12
  * When a file attachment is successfully sent, announces the result to
13
- * screen readers via an aria-live region so that assistive technology
14
- * users receive immediate feedback.
13
+ * screen readers so that assistive technology users receive immediate
14
+ * feedback ("File report.pdf sent").
15
+ *
16
+ * The announcement region lives inside the chat dialog. While the dialog is
17
+ * open it is aria-modal and its host siblings are aria-hidden, so screen
18
+ * readers (TalkBack, VoiceOver) ignore live regions outside it.
15
19
  ******/
16
20
 
17
- const ARIA_LIVE_REGION_ID = "oc-lcw-attachment-announcement";
21
+ const ATTACHMENT_ANNOUNCEMENT_REGION_ID = exports.ATTACHMENT_ANNOUNCEMENT_REGION_ID = "oc-lcw-attachment-announcement";
22
+ const DEFAULT_FILE_SENT_TEXT = "File {2} sent";
23
+ const DEFAULT_FILES_SENT_TEXT = "{0} files sent";
24
+
25
+ // Let the send-box focus shift settle before speaking, then keep the node
26
+ // long enough for the screen reader to read it.
27
+ const ANNOUNCE_DELAY_MS = 100;
28
+ const ANNOUNCEMENT_LIFETIME_MS = 5000;
18
29
 
19
30
  /**
20
- * Ensures a visually-hidden aria-live region exists in the DOM for
21
- * announcing attachment upload results to screen readers.
31
+ * Ensures a visually-hidden aria-live region exists inside the chat dialog
32
+ * for announcing attachment upload results to screen readers.
22
33
  */
23
- const ensureAriaLiveRegion = () => {
24
- let region = document.getElementById(ARIA_LIVE_REGION_ID);
34
+ const ensureAriaLiveRegion = widgetElementId => {
35
+ const host = document.getElementById(widgetElementId) || document.body;
36
+ let region = document.getElementById(ATTACHMENT_ANNOUNCEMENT_REGION_ID);
25
37
  if (!region) {
26
38
  region = document.createElement("div");
27
- region.id = ARIA_LIVE_REGION_ID;
39
+ region.id = ATTACHMENT_ANNOUNCEMENT_REGION_ID;
28
40
  region.setAttribute("aria-live", "polite");
29
- region.setAttribute("role", "status");
30
- region.setAttribute("aria-atomic", "true");
31
41
  // Visually hidden but available to screen readers
32
42
  Object.assign(region.style, {
33
43
  position: "absolute",
@@ -40,44 +50,62 @@ const ensureAriaLiveRegion = () => {
40
50
  whiteSpace: "nowrap",
41
51
  border: "0"
42
52
  });
43
- const container = document.getElementById("oc-lcw-container");
44
- (container || document.body).appendChild(region);
53
+ }
54
+ if (region.parentElement !== host) {
55
+ host.appendChild(region);
45
56
  }
46
57
  return region;
47
58
  };
48
59
 
49
60
  /**
50
- * Announces a message to screen readers by updating the aria-live region.
51
- * Clears it briefly first so repeated identical messages are still announced.
61
+ * Announces a message by appending a fresh node to the live region.
62
+ * TalkBack reliably speaks newly added nodes but often misses text changes
63
+ * on an existing node, so each announcement gets its own element.
52
64
  */
53
- const announce = message => {
54
- const region = ensureAriaLiveRegion();
55
- region.textContent = "";
56
- // Small delay so the screen reader detects the content change
65
+ const announce = (message, widgetElementId) => {
66
+ const region = ensureAriaLiveRegion(widgetElementId);
57
67
  setTimeout(() => {
58
- region.textContent = message;
59
- }, 100);
68
+ const node = document.createElement("div");
69
+ node.textContent = message;
70
+ region.appendChild(node);
71
+ setTimeout(() => node.remove(), ANNOUNCEMENT_LIFETIME_MS);
72
+ }, ANNOUNCE_DELAY_MS);
60
73
  };
61
74
 
62
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
63
- const attachmentSentAnnouncementMiddleware = _ref => {
64
- let dispatch = _ref.dispatch;
65
- return next => action => {
66
- if (!(0, _IWebChatAction.isWebChatAction)(action)) {
67
- return next(action);
68
- }
75
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
76
+ const getFileSentMessage = (attachments, localizedTexts) => {
77
+ var _attachments$;
78
+ if (attachments.length > 1) {
79
+ // Replacer callbacks insert values literally; a string would treat "$&" or "$$" as patterns.
80
+ return ((localizedTexts === null || localizedTexts === void 0 ? void 0 : localizedTexts.MIDDLEWARE_BANNER_FILES_SENT) || DEFAULT_FILES_SENT_TEXT).replace("{0}", () => String(attachments.length));
81
+ }
82
+ const name = typeof ((_attachments$ = attachments[0]) === null || _attachments$ === void 0 ? void 0 : _attachments$.name) === "string" ? attachments[0].name.trim() : "";
83
+ return ((localizedTexts === null || localizedTexts === void 0 ? void 0 : localizedTexts.MIDDLEWARE_BANNER_FILE_SENT) || DEFAULT_FILE_SENT_TEXT).replace("{2}", () => name).replace(/\s{2,}/g, " ").trim();
84
+ };
85
+ exports.getFileSentMessage = getFileSentMessage;
86
+ const createAttachmentSentAnnouncementMiddleware = function (localizedTexts) {
87
+ let widgetElementId = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "oc-lcw";
88
+ return (
89
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
90
+ _ref => {
91
+ let dispatch = _ref.dispatch;
92
+ return next => action => {
93
+ if (!(0, _IWebChatAction.isWebChatAction)(action)) {
94
+ return next(action);
95
+ }
69
96
 
70
- // Intercept successfully posted activities that contain attachments
71
- if (action.type === _WebChatActionType.WebChatActionType.DIRECT_LINE_POST_ACTIVITY_FULFILLED) {
72
- var _action$payload;
73
- const attachments = (_action$payload = action.payload) === null || _action$payload === void 0 || (_action$payload = _action$payload.activity) === null || _action$payload === void 0 ? void 0 : _action$payload.attachments;
74
- if (attachments && attachments.length > 0) {
75
- const count = attachments.length;
76
- const message = count === 1 ? "File sent" : `${count} files sent`;
77
- announce(message);
78
- }
97
+ // Intercept successfully posted activities that contain attachments
98
+ if (action.type === _WebChatActionType.WebChatActionType.DIRECT_LINE_POST_ACTIVITY_FULFILLED) {
99
+ var _action$payload;
100
+ const attachments = (_action$payload = action.payload) === null || _action$payload === void 0 || (_action$payload = _action$payload.activity) === null || _action$payload === void 0 ? void 0 : _action$payload.attachments;
101
+ if (attachments && attachments.length > 0) {
102
+ announce(getFileSentMessage(attachments, localizedTexts), widgetElementId);
103
+ }
104
+ }
105
+ return next(action);
106
+ };
79
107
  }
80
- return next(action);
81
- };
108
+ );
82
109
  };
83
- var _default = exports.default = attachmentSentAnnouncementMiddleware;
110
+ exports.createAttachmentSentAnnouncementMiddleware = createAttachmentSentAnnouncementMiddleware;
111
+ var _default = exports.default = createAttachmentSentAnnouncementMiddleware;
@@ -18,27 +18,6 @@ var _WebChatActionType = require("../../enums/WebChatActionType");
18
18
  ******/
19
19
 
20
20
  const MBtoBRatio = 1000000;
21
- const announceFileSent = message => {
22
- // TalkBack on Android WebView reliably announces newly *appended*
23
- // role="alert" nodes but often misses text-content updates on existing
24
- // nodes. Wait 500ms for the send-box focus shift to settle, then inject
25
- // a fresh alert element appended to document.body and remove it after 3s.
26
- setTimeout(() => {
27
- const el = document.createElement("div");
28
- el.setAttribute("role", "alert");
29
- el.setAttribute("aria-live", "assertive");
30
- el.setAttribute("aria-atomic", "true");
31
- el.style.cssText = "position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden;";
32
- el.textContent = message;
33
- document.body.appendChild(el);
34
- // Remove after TalkBack has had time to read it
35
- setTimeout(() => {
36
- if (el.parentNode) {
37
- el.parentNode.removeChild(el);
38
- }
39
- }, 3000);
40
- }, 500);
41
- };
42
21
 
43
22
  /*
44
23
  * If an attachment is invalid, delete this attachment from the attachments list
@@ -227,13 +206,7 @@ const createAttachmentUploadValidatorMiddleware = (allowedFileExtensions, maxFil
227
206
  var _payload$activity, _payload$activity2;
228
207
  const payload = action.payload;
229
208
  if (payload !== null && payload !== void 0 && payload.activity.attachments && payload.activity.attachments.length > 0 && (payload === null || payload === void 0 || (_payload$activity = payload.activity) === null || _payload$activity === void 0 || (_payload$activity = _payload$activity.attachments) === null || _payload$activity === void 0 ? void 0 : _payload$activity.length) === (payload === null || payload === void 0 || (_payload$activity2 = payload.activity) === null || _payload$activity2 === void 0 || (_payload$activity2 = _payload$activity2.channelData) === null || _payload$activity2 === void 0 || (_payload$activity2 = _payload$activity2.attachmentSizes) === null || _payload$activity2 === void 0 ? void 0 : _payload$activity2.length)) {
230
- var _validatedAction$payl;
231
209
  const validatedAction = validateAttachment(action, allowedFileExtensions, maxFileSizeSupportedByDynamics, localizedTexts);
232
-
233
- // Announce to screen readers that the file was sent when validation passes
234
- if (((_validatedAction$payl = validatedAction.payload) === null || _validatedAction$payl === void 0 || (_validatedAction$payl = _validatedAction$payl.activity) === null || _validatedAction$payl === void 0 || (_validatedAction$payl = _validatedAction$payl.attachments) === null || _validatedAction$payl === void 0 ? void 0 : _validatedAction$payl.length) > 0) {
235
- announceFileSent(localizedTexts.MIDDLEWARE_BANNER_FILE_SENT ?? "File sent successfully.");
236
- }
237
210
  return next(validatedAction);
238
211
  }
239
212
  }
@@ -109,6 +109,7 @@ export let TelemetryEvent = /*#__PURE__*/function (TelemetryEvent) {
109
109
  TelemetryEvent["MidConversationAuthSucceeded"] = "MidConversationAuthSucceeded";
110
110
  TelemetryEvent["MidConversationAuthFailed"] = "MidConversationAuthFailed";
111
111
  TelemetryEvent["MidConversationAuthReset"] = "MidConversationAuthReset";
112
+ TelemetryEvent["RecoveryEligibilityEvaluated"] = "RecoveryEligibilityEvaluated";
112
113
  TelemetryEvent["GetConversationDetailsCallStarted"] = "GetConversationDetailsCallStarted";
113
114
  TelemetryEvent["GetConversationDetailsCallFailed"] = "GetConversationDetailsCallFailed";
114
115
  TelemetryEvent["EndChatSDKCallFailed"] = "EndChatSDKCallFailed";
@@ -140,6 +141,8 @@ export let TelemetryEvent = /*#__PURE__*/function (TelemetryEvent) {
140
141
  TelemetryEvent["PrechatSubmitted"] = "PrechatSubmitted";
141
142
  TelemetryEvent["StartChatSDKCall"] = "StartChatCall";
142
143
  TelemetryEvent["StartChatEventReceived"] = "StartChatEventReceived";
144
+ TelemetryEvent["ConcurrentStartChatRejected"] = "ConcurrentStartChatRejected";
145
+ TelemetryEvent["ChatSDKStateCleanUpDisconnectFailed"] = "ChatSDKStateCleanUpDisconnectFailed";
143
146
  TelemetryEvent["EndChatSDKCall"] = "EndChatSDKCall";
144
147
  TelemetryEvent["PrepareEndChat"] = "PrepareEndChat";
145
148
  TelemetryEvent["EndChatEventReceived"] = "EndChatEventReceived";
@@ -412,6 +415,9 @@ export let TelemetryConstants = /*#__PURE__*/function () {
412
415
  case TelemetryEvent.SecureEventBusDispatchError:
413
416
  case TelemetryEvent.AgentJoinedConversation:
414
417
  case TelemetryEvent.BrowserTabHidden:
418
+ case TelemetryEvent.ConcurrentStartChatRejected:
419
+ case TelemetryEvent.ChatSDKStateCleanUpDisconnectFailed:
420
+ case TelemetryEvent.RecoveryEligibilityEvaluated:
415
421
  return ScenarioType.ACTIONS;
416
422
  case TelemetryEvent.StartChatSDKCall:
417
423
  case TelemetryEvent.StartChatEventReceived:
@@ -50,37 +50,54 @@ export const ChatButtonStateful = props => {
50
50
  const ref = useRef(() => {
51
51
  return;
52
52
  });
53
+ // Optimistic guard against rapid double-clicks. `conversationState` only flips to
54
+ // `Loading` after React re-renders, so the button stays visible and clickable for the
55
+ // whole duration of the first click's async work. Without this ref two concurrent
56
+ // startChat() calls can create two conversations on the backend.
57
+ const startChatInProgressRef = useRef(false);
53
58
  ref.current = async () => {
54
- TelemetryHelper.logActionEventToAllTelemetry(LogLevel.INFO, {
55
- Event: TelemetryEvent.LCWChatButtonClicked,
56
- Description: "Chat button click action started",
57
- CustomProperties: {
58
- ConversationStage: ConversationStage.Initialization
59
- }
60
- });
61
- if (state.appStates.isMinimized) {
62
- dispatch({
63
- type: LiveChatWidgetActionType.SET_MINIMIZED,
64
- payload: false
59
+ if (startChatInProgressRef.current) {
60
+ TelemetryHelper.logActionEvent(LogLevel.WARN, {
61
+ Event: TelemetryEvent.ConcurrentStartChatRejected,
62
+ Description: "Chat button click ignored, a start chat operation is already in progress."
65
63
  });
66
- dispatch({
67
- type: LiveChatWidgetActionType.SET_UNREAD_MESSAGE_COUNT,
68
- payload: 0
64
+ return;
65
+ }
66
+ startChatInProgressRef.current = true;
67
+ try {
68
+ TelemetryHelper.logActionEventToAllTelemetry(LogLevel.INFO, {
69
+ Event: TelemetryEvent.LCWChatButtonClicked,
70
+ Description: "Chat button click action started",
71
+ CustomProperties: {
72
+ ConversationStage: ConversationStage.Initialization
73
+ }
69
74
  });
70
- // If chat is minimized and then unminimized, start a chat if convesation state is closed.
71
- if (state.appStates.conversationState === ConversationState.Closed) {
75
+ if (state.appStates.isMinimized) {
76
+ dispatch({
77
+ type: LiveChatWidgetActionType.SET_MINIMIZED,
78
+ payload: false
79
+ });
80
+ dispatch({
81
+ type: LiveChatWidgetActionType.SET_UNREAD_MESSAGE_COUNT,
82
+ payload: 0
83
+ });
84
+ // If chat is minimized and then unminimized, start a chat if convesation state is closed.
85
+ if (state.appStates.conversationState === ConversationState.Closed) {
86
+ await startChat();
87
+ }
88
+ } else {
72
89
  await startChat();
73
90
  }
74
- } else {
75
- await startChat();
91
+ TelemetryHelper.logActionEventToAllTelemetry(LogLevel.INFO, {
92
+ Event: TelemetryEvent.LCWChatButtonActionCompleted,
93
+ Description: "Chat button action completed",
94
+ CustomProperties: {
95
+ ConversationStage: ConversationStage.Initialization
96
+ }
97
+ });
98
+ } finally {
99
+ startChatInProgressRef.current = false;
76
100
  }
77
- TelemetryHelper.logActionEventToAllTelemetry(LogLevel.INFO, {
78
- Event: TelemetryEvent.LCWChatButtonActionCompleted,
79
- Description: "Chat button action completed",
80
- CustomProperties: {
81
- ConversationStage: ConversationStage.Initialization
82
- }
83
- });
84
101
  };
85
102
  const outOfOfficeStyleProps = Object.assign({}, defaultOutOfOfficeChatButtonStyleProps, outOfOfficeButtonProps === null || outOfOfficeButtonProps === void 0 ? void 0 : outOfOfficeButtonProps.styleProps);
86
103
  const controlProps = {
@@ -349,14 +349,53 @@ export const closeChatStateCleanUp = dispatch => {
349
349
  });
350
350
  };
351
351
 
352
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
353
+ const logCleanUpDisconnectFailure = error => {
354
+ TelemetryHelper.logActionEvent(LogLevel.WARN, {
355
+ Event: TelemetryEvent.ChatSDKStateCleanUpDisconnectFailed,
356
+ Description: "Failed to disconnect the previous conversation during chat SDK state clean up.",
357
+ ExceptionDetails: {
358
+ exception: (error === null || error === void 0 ? void 0 : error.message) ?? error
359
+ }
360
+ });
361
+ };
362
+
363
+ // Releases the previous session's conversation. Mirrors the cleanup OmnichannelChatSDK.endChat()
364
+ // runs in its finally block (disconnect, then drop the reference). Without it, a conversation
365
+ // left over from a failed or orphaned start chat, or from a chat ended in another tab (which
366
+ // skips the SDK endChat), keeps its messaging session open and leaks into the next chat.
367
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
368
+ const disconnectConversation = chatSDK => {
369
+ const conversation = chatSDK.conversation;
370
+ if (!conversation || typeof conversation.disconnect !== "function") {
371
+ return;
372
+ }
373
+ try {
374
+ // ACS conversations disconnect asynchronously; legacy IC3 conversations may not return a promise.
375
+ const result = conversation.disconnect();
376
+ if (result && typeof result.catch === "function") {
377
+ result.catch(logCleanUpDisconnectFailure);
378
+ }
379
+ } catch (error) {
380
+ logCleanUpDisconnectFailure(error);
381
+ }
382
+ };
383
+
352
384
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
353
385
  export const chatSDKStateCleanUp = chatSDK => {
386
+ if (!chatSDK) {
387
+ return;
388
+ }
389
+
354
390
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
355
391
  chatSDK.requestId = uuidv4();
356
392
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
357
393
  chatSDK.chatToken = {};
358
394
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
359
395
  chatSDK.reconnectId = null;
396
+ disconnectConversation(chatSDK);
397
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
398
+ chatSDK.conversation = null;
360
399
  };
361
400
 
362
401
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -11,7 +11,7 @@ import { LiveChatWidgetActionType } from "../../../contexts/common/LiveChatWidge
11
11
  import { TelemetryHelper } from "../../../common/telemetry/TelemetryHelper";
12
12
  import { WebChatStoreLoader } from "../../webchatcontainerstateful/webchatcontroller/WebChatStoreLoader";
13
13
  import attachmentProcessingMiddleware from "../../webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/attachmentProcessingMiddleware";
14
- import attachmentSentAnnouncementMiddleware from "../../webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/attachmentSentAnnouncementMiddleware";
14
+ import createAttachmentSentAnnouncementMiddleware from "../../webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/attachmentSentAnnouncementMiddleware";
15
15
  import channelDataMiddleware from "../../webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/channelDataMiddleware";
16
16
  import { createActivityMiddleware } from "../../webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/activityMiddleware";
17
17
  import { createActivityStatusMiddleware } from "../../webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/activityStatusMiddleware";
@@ -62,7 +62,7 @@ export const initWebChatComposer = (props, state, dispatch, facadeChatSDK, endCh
62
62
  // Initialize Web Chat's redux store
63
63
  let webChatStore = WebChatStoreLoader.store;
64
64
  if (!webChatStore) {
65
- var _state$domainStates$l, _state$domainStates$l2, _state$domainStates$l3, _props$webChatContain7;
65
+ var _state$domainStates$l, _state$domainStates$l2, _props$controlProps, _state$domainStates$l3, _props$webChatContain7;
66
66
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
67
67
  const addConversationalSurveyTagsCallback = action => {
68
68
  var _inMemoryState$appSta;
@@ -121,7 +121,7 @@ export const initWebChatComposer = (props, state, dispatch, facadeChatSDK, endCh
121
121
  };
122
122
  webChatStore = createStore({},
123
123
  //initial state
124
- preProcessingMiddleware, attachmentProcessingMiddleware, createAttachmentUploadValidatorMiddleware((_state$domainStates$l = state.domainStates.liveChatConfig) === null || _state$domainStates$l === void 0 ? void 0 : _state$domainStates$l.allowedFileExtensions, (_state$domainStates$l2 = state.domainStates.liveChatConfig) === null || _state$domainStates$l2 === void 0 ? void 0 : _state$domainStates$l2.maxUploadFileSize, localizedTexts), attachmentSentAnnouncementMiddleware, createCustomEventMiddleware(BroadcastService), createQueueOverflowMiddleware(state, dispatch), channelDataMiddleware(addConversationalSurveyTagsCallback), createConversationEndMiddleware(conversationEndCallback, startConversationalSurveyCallback, endConversationalSurveyCallback), createDataMaskingMiddleware((_state$domainStates$l3 = state.domainStates.liveChatConfig) === null || _state$domainStates$l3 === void 0 ? void 0 : _state$domainStates$l3.DataMaskingInfo), createMessageTimeStampMiddleware, createMessageSequenceIdOverrideMiddleware, createCitationsMiddleware(state, dispatch), gifUploadMiddleware, htmlPlayerMiddleware, htmlTextMiddleware(honorsTargetInHTMLLinks), createMaxMessageSizeValidator(localizedTexts), sanitizationMiddleware, createCallActionMiddleware(),
124
+ preProcessingMiddleware, attachmentProcessingMiddleware, createAttachmentUploadValidatorMiddleware((_state$domainStates$l = state.domainStates.liveChatConfig) === null || _state$domainStates$l === void 0 ? void 0 : _state$domainStates$l.allowedFileExtensions, (_state$domainStates$l2 = state.domainStates.liveChatConfig) === null || _state$domainStates$l2 === void 0 ? void 0 : _state$domainStates$l2.maxUploadFileSize, localizedTexts), createAttachmentSentAnnouncementMiddleware(localizedTexts, ((_props$controlProps = props.controlProps) === null || _props$controlProps === void 0 ? void 0 : _props$controlProps.id) || "oc-lcw"), createCustomEventMiddleware(BroadcastService), createQueueOverflowMiddleware(state, dispatch), channelDataMiddleware(addConversationalSurveyTagsCallback), createConversationEndMiddleware(conversationEndCallback, startConversationalSurveyCallback, endConversationalSurveyCallback), createDataMaskingMiddleware((_state$domainStates$l3 = state.domainStates.liveChatConfig) === null || _state$domainStates$l3 === void 0 ? void 0 : _state$domainStates$l3.DataMaskingInfo), createMessageTimeStampMiddleware, createMessageSequenceIdOverrideMiddleware, createCitationsMiddleware(state, dispatch), gifUploadMiddleware, htmlPlayerMiddleware, htmlTextMiddleware(honorsTargetInHTMLLinks), createMaxMessageSizeValidator(localizedTexts), sanitizationMiddleware, createCallActionMiddleware(),
125
125
  // Pass a callback so middleware can push initials into React context for reactivity
126
126
  localizedStringsBotInitialsMiddleware(initials => {
127
127
  dispatch({
@@ -251,6 +251,12 @@ export const initWebChatComposer = (props, state, dispatch, facadeChatSDK, endCh
251
251
  // Track what would be removed
252
252
  const removedTags = [];
253
253
  const removedAttributes = [];
254
+ const trackAttribute = name => {
255
+ const attrName = name.toLowerCase();
256
+ if (!strictConfig.ALLOWED_ATTR.includes(attrName) && attrName !== "class" && attrName !== "id") {
257
+ removedAttributes.push(attrName);
258
+ }
259
+ };
254
260
 
255
261
  // Add hooks to the isolated monitoring instance
256
262
  monitorDOMPurify.addHook("uponSanitizeElement", (node, data) => {
@@ -259,6 +265,8 @@ export const initWebChatComposer = (props, state, dispatch, facadeChatSDK, endCh
259
265
  // Filter out "body" tag which is DOMPurify's internal wrapper
260
266
  if (node.nodeType === 1 && !strictConfig.ALLOWED_TAGS.includes(tagName) && tagName !== "body") {
261
267
  removedTags.push(tagName);
268
+ // DOMPurify 3.4.16+ skips uponSanitizeAttribute for removed elements
269
+ Array.from(node.attributes ?? []).forEach(attr => trackAttribute(attr.name));
262
270
  }
263
271
  } catch (hookError) {
264
272
  // Silently ignore hook errors
@@ -266,10 +274,7 @@ export const initWebChatComposer = (props, state, dispatch, facadeChatSDK, endCh
266
274
  });
267
275
  monitorDOMPurify.addHook("uponSanitizeAttribute", (node, data) => {
268
276
  try {
269
- const attrName = data.attrName.toLowerCase();
270
- if (!strictConfig.ALLOWED_ATTR.includes(attrName) && attrName !== "class" && attrName !== "id") {
271
- removedAttributes.push(attrName);
272
- }
277
+ trackAttribute(data.attrName);
273
278
  } catch (hookError) {
274
279
  // Silently ignore hook errors
275
280
  }
@@ -0,0 +1,52 @@
1
+ import { ConversationState } from "../../../contexts/common/ConversationState";
2
+ import { LogLevel, TelemetryEvent } from "../../../common/telemetry/TelemetryConstants";
3
+ import { TelemetryHelper } from "../../../common/telemetry/TelemetryHelper";
4
+ import { isUndefinedOrEmpty } from "../../../common/utils";
5
+ export let RecoveryEligibilityReason = /*#__PURE__*/function (RecoveryEligibilityReason) {
6
+ RecoveryEligibilityReason["ReconnectIdPresent"] = "ReconnectIdPresent";
7
+ RecoveryEligibilityReason["ActiveContextPresent"] = "ActiveContextPresent";
8
+ RecoveryEligibilityReason["MissingContext"] = "MissingContext";
9
+ RecoveryEligibilityReason["CachedContextNonActive"] = "CachedContextNonActive";
10
+ return RecoveryEligibilityReason;
11
+ }({});
12
+ const getConversationStateName = conversationState => {
13
+ if (typeof conversationState !== "number") {
14
+ return "Unknown";
15
+ }
16
+ const stateName = ConversationState[conversationState];
17
+ return typeof stateName === "string" ? stateName : "Unknown";
18
+ };
19
+ export const getRecoveryEligibilityTelemetryProperties = (state, eligible) => {
20
+ var _state$appStates, _state$domainStates, _state$appStates2, _state$appStates3;
21
+ const reconnectId = (_state$appStates = state.appStates) === null || _state$appStates === void 0 ? void 0 : _state$appStates.reconnectId;
22
+ const liveChatContext = (_state$domainStates = state.domainStates) === null || _state$domainStates === void 0 ? void 0 : _state$domainStates.liveChatContext;
23
+ const hasReconnectId = !isUndefinedOrEmpty(reconnectId);
24
+ const hasLiveChatContext = !isUndefinedOrEmpty(liveChatContext);
25
+ const hasRequestId = !isUndefinedOrEmpty(liveChatContext === null || liveChatContext === void 0 ? void 0 : liveChatContext.requestId);
26
+ let reason = RecoveryEligibilityReason.ActiveContextPresent;
27
+ if (hasReconnectId) {
28
+ reason = RecoveryEligibilityReason.ReconnectIdPresent;
29
+ } else if (!hasLiveChatContext) {
30
+ reason = RecoveryEligibilityReason.MissingContext;
31
+ } else if (((_state$appStates2 = state.appStates) === null || _state$appStates2 === void 0 ? void 0 : _state$appStates2.conversationState) !== ConversationState.Active) {
32
+ reason = RecoveryEligibilityReason.CachedContextNonActive;
33
+ }
34
+ return {
35
+ conversationState: getConversationStateName((_state$appStates3 = state.appStates) === null || _state$appStates3 === void 0 ? void 0 : _state$appStates3.conversationState),
36
+ hasReconnectId,
37
+ hasLiveChatContext,
38
+ hasRequestId,
39
+ eligible,
40
+ reason
41
+ };
42
+ };
43
+ export const logRecoveryEligibilityTelemetry = (state, eligible) => {
44
+ try {
45
+ TelemetryHelper.logActionEvent(LogLevel.INFO, {
46
+ Event: TelemetryEvent.RecoveryEligibilityEvaluated,
47
+ CustomProperties: getRecoveryEligibilityTelemetryProperties(state, eligible)
48
+ });
49
+ } catch (error) {
50
+ console.warn(error);
51
+ }
52
+ };