@microsoft/omnichannel-chat-widget 2.0.0 → 2.0.1-main.6ca1911

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 +5 -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 +8 -7
package/README.md CHANGED
@@ -53,10 +53,12 @@ This package requires Node.js `>=22.12.0` for consumer builds. Node 20 is not su
53
53
  For a production application, pin the exact stable versions:
54
54
 
55
55
  ```bash
56
- npm install --save-exact @microsoft/omnichannel-chat-widget@2.0.0 @microsoft/omnichannel-chat-components@1.2.0 @microsoft/omnichannel-chat-sdk@2.0.0
56
+ npm install --save-exact @microsoft/omnichannel-chat-widget@2.0.1 @microsoft/omnichannel-chat-components@1.2.0 @microsoft/omnichannel-chat-sdk@2.0.1
57
57
  ```
58
58
 
59
- `2.0.0` release date is 2026-08-18. Official support ends on 2027-08-18. See the [root support table](https://github.com/microsoft/omnichannel-chat-widget#chat-widget).
59
+ `2.0.1` release date is 2026-10-05. Official support ends on 2027-10-05. See the [root support table](https://github.com/microsoft/omnichannel-chat-widget#chat-widget).
60
+
61
+ _**Known issue:**_ Chat SDK versions earlier than `1.11.6` can start duplicate conversations when `startChat()` is called again before the previous call is complete. Chat Widget `1.8.1` and earlier can use these versions. Upgrade the Chat SDK to `1.11.8` or later. See [Duplicate conversations with Chat SDK versions before 1.11.6](https://github.com/microsoft/omnichannel-chat-widget/blob/main/docs/KNOWN_ISSUE_CONCURRENT_STARTCHAT.md).
60
62
 
61
63
  TypeScript consumers that imported the exported Web Chat middleware factories now receive Redux 5 `unknown` actions. Narrow with `isWebChatAction` before reading `type` or `payload`. Runtime behavior is unchanged.
62
64
 
@@ -79,6 +81,7 @@ This hotfix still declares these vulnerable transitive versions:
79
81
  | `swiper` | `8.4.7` |
80
82
  | `valibot` | `1.1.0` |
81
83
  | `uuid` | `3.4.0`, `8.3.2`, and `9.0.1` |
84
+ | `deepmerge` (via `sanitize-html@2.14.0`) | `4.3.1` |
82
85
 
83
86
  The consumer audit permits only the reviewed advisories from this hotfix. It rejects each new advisory.
84
87
 
@@ -111,6 +111,7 @@ let TelemetryEvent = exports.TelemetryEvent = /*#__PURE__*/function (TelemetryEv
111
111
  TelemetryEvent["MidConversationAuthSucceeded"] = "MidConversationAuthSucceeded";
112
112
  TelemetryEvent["MidConversationAuthFailed"] = "MidConversationAuthFailed";
113
113
  TelemetryEvent["MidConversationAuthReset"] = "MidConversationAuthReset";
114
+ TelemetryEvent["RecoveryEligibilityEvaluated"] = "RecoveryEligibilityEvaluated";
114
115
  TelemetryEvent["GetConversationDetailsCallStarted"] = "GetConversationDetailsCallStarted";
115
116
  TelemetryEvent["GetConversationDetailsCallFailed"] = "GetConversationDetailsCallFailed";
116
117
  TelemetryEvent["EndChatSDKCallFailed"] = "EndChatSDKCallFailed";
@@ -142,6 +143,8 @@ let TelemetryEvent = exports.TelemetryEvent = /*#__PURE__*/function (TelemetryEv
142
143
  TelemetryEvent["PrechatSubmitted"] = "PrechatSubmitted";
143
144
  TelemetryEvent["StartChatSDKCall"] = "StartChatCall";
144
145
  TelemetryEvent["StartChatEventReceived"] = "StartChatEventReceived";
146
+ TelemetryEvent["ConcurrentStartChatRejected"] = "ConcurrentStartChatRejected";
147
+ TelemetryEvent["ChatSDKStateCleanUpDisconnectFailed"] = "ChatSDKStateCleanUpDisconnectFailed";
145
148
  TelemetryEvent["EndChatSDKCall"] = "EndChatSDKCall";
146
149
  TelemetryEvent["PrepareEndChat"] = "PrepareEndChat";
147
150
  TelemetryEvent["EndChatEventReceived"] = "EndChatEventReceived";
@@ -414,6 +417,9 @@ let TelemetryConstants = exports.TelemetryConstants = /*#__PURE__*/function () {
414
417
  case TelemetryEvent.SecureEventBusDispatchError:
415
418
  case TelemetryEvent.AgentJoinedConversation:
416
419
  case TelemetryEvent.BrowserTabHidden:
420
+ case TelemetryEvent.ConcurrentStartChatRejected:
421
+ case TelemetryEvent.ChatSDKStateCleanUpDisconnectFailed:
422
+ case TelemetryEvent.RecoveryEligibilityEvaluated:
417
423
  return ScenarioType.ACTIONS;
418
424
  case TelemetryEvent.StartChatSDKCall:
419
425
  case TelemetryEvent.StartChatEventReceived:
@@ -58,37 +58,54 @@ const ChatButtonStateful = props => {
58
58
  const ref = (0, _react.useRef)(() => {
59
59
  return;
60
60
  });
61
+ // Optimistic guard against rapid double-clicks. `conversationState` only flips to
62
+ // `Loading` after React re-renders, so the button stays visible and clickable for the
63
+ // whole duration of the first click's async work. Without this ref two concurrent
64
+ // startChat() calls can create two conversations on the backend.
65
+ const startChatInProgressRef = (0, _react.useRef)(false);
61
66
  ref.current = async () => {
62
- _TelemetryHelper.TelemetryHelper.logActionEventToAllTelemetry(_TelemetryConstants.LogLevel.INFO, {
63
- Event: _TelemetryConstants.TelemetryEvent.LCWChatButtonClicked,
64
- Description: "Chat button click action started",
65
- CustomProperties: {
66
- ConversationStage: _TelemetryConstants.ConversationStage.Initialization
67
- }
68
- });
69
- if (state.appStates.isMinimized) {
70
- dispatch({
71
- type: _LiveChatWidgetActionType.LiveChatWidgetActionType.SET_MINIMIZED,
72
- payload: false
67
+ if (startChatInProgressRef.current) {
68
+ _TelemetryHelper.TelemetryHelper.logActionEvent(_TelemetryConstants.LogLevel.WARN, {
69
+ Event: _TelemetryConstants.TelemetryEvent.ConcurrentStartChatRejected,
70
+ Description: "Chat button click ignored, a start chat operation is already in progress."
73
71
  });
74
- dispatch({
75
- type: _LiveChatWidgetActionType.LiveChatWidgetActionType.SET_UNREAD_MESSAGE_COUNT,
76
- payload: 0
72
+ return;
73
+ }
74
+ startChatInProgressRef.current = true;
75
+ try {
76
+ _TelemetryHelper.TelemetryHelper.logActionEventToAllTelemetry(_TelemetryConstants.LogLevel.INFO, {
77
+ Event: _TelemetryConstants.TelemetryEvent.LCWChatButtonClicked,
78
+ Description: "Chat button click action started",
79
+ CustomProperties: {
80
+ ConversationStage: _TelemetryConstants.ConversationStage.Initialization
81
+ }
77
82
  });
78
- // If chat is minimized and then unminimized, start a chat if convesation state is closed.
79
- if (state.appStates.conversationState === _ConversationState.ConversationState.Closed) {
83
+ if (state.appStates.isMinimized) {
84
+ dispatch({
85
+ type: _LiveChatWidgetActionType.LiveChatWidgetActionType.SET_MINIMIZED,
86
+ payload: false
87
+ });
88
+ dispatch({
89
+ type: _LiveChatWidgetActionType.LiveChatWidgetActionType.SET_UNREAD_MESSAGE_COUNT,
90
+ payload: 0
91
+ });
92
+ // If chat is minimized and then unminimized, start a chat if convesation state is closed.
93
+ if (state.appStates.conversationState === _ConversationState.ConversationState.Closed) {
94
+ await startChat();
95
+ }
96
+ } else {
80
97
  await startChat();
81
98
  }
82
- } else {
83
- await startChat();
99
+ _TelemetryHelper.TelemetryHelper.logActionEventToAllTelemetry(_TelemetryConstants.LogLevel.INFO, {
100
+ Event: _TelemetryConstants.TelemetryEvent.LCWChatButtonActionCompleted,
101
+ Description: "Chat button action completed",
102
+ CustomProperties: {
103
+ ConversationStage: _TelemetryConstants.ConversationStage.Initialization
104
+ }
105
+ });
106
+ } finally {
107
+ startChatInProgressRef.current = false;
84
108
  }
85
- _TelemetryHelper.TelemetryHelper.logActionEventToAllTelemetry(_TelemetryConstants.LogLevel.INFO, {
86
- Event: _TelemetryConstants.TelemetryEvent.LCWChatButtonActionCompleted,
87
- Description: "Chat button action completed",
88
- CustomProperties: {
89
- ConversationStage: _TelemetryConstants.ConversationStage.Initialization
90
- }
91
- });
92
109
  };
93
110
  const outOfOfficeStyleProps = Object.assign({}, _defaultOutOfOfficeChatButtonStyleProps.defaultOutOfOfficeChatButtonStyleProps, outOfOfficeButtonProps === null || outOfOfficeButtonProps === void 0 ? void 0 : outOfOfficeButtonProps.styleProps);
94
111
  const controlProps = {
@@ -360,13 +360,52 @@ const closeChatStateCleanUp = dispatch => {
360
360
 
361
361
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
362
362
  exports.closeChatStateCleanUp = closeChatStateCleanUp;
363
+ const logCleanUpDisconnectFailure = error => {
364
+ _TelemetryHelper.TelemetryHelper.logActionEvent(_TelemetryConstants.LogLevel.WARN, {
365
+ Event: _TelemetryConstants.TelemetryEvent.ChatSDKStateCleanUpDisconnectFailed,
366
+ Description: "Failed to disconnect the previous conversation during chat SDK state clean up.",
367
+ ExceptionDetails: {
368
+ exception: (error === null || error === void 0 ? void 0 : error.message) ?? error
369
+ }
370
+ });
371
+ };
372
+
373
+ // Releases the previous session's conversation. Mirrors the cleanup OmnichannelChatSDK.endChat()
374
+ // runs in its finally block (disconnect, then drop the reference). Without it, a conversation
375
+ // left over from a failed or orphaned start chat, or from a chat ended in another tab (which
376
+ // skips the SDK endChat), keeps its messaging session open and leaks into the next chat.
377
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
378
+ const disconnectConversation = chatSDK => {
379
+ const conversation = chatSDK.conversation;
380
+ if (!conversation || typeof conversation.disconnect !== "function") {
381
+ return;
382
+ }
383
+ try {
384
+ // ACS conversations disconnect asynchronously; legacy IC3 conversations may not return a promise.
385
+ const result = conversation.disconnect();
386
+ if (result && typeof result.catch === "function") {
387
+ result.catch(logCleanUpDisconnectFailure);
388
+ }
389
+ } catch (error) {
390
+ logCleanUpDisconnectFailure(error);
391
+ }
392
+ };
393
+
394
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
363
395
  const chatSDKStateCleanUp = chatSDK => {
396
+ if (!chatSDK) {
397
+ return;
398
+ }
399
+
364
400
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
365
401
  chatSDK.requestId = (0, _omnichannelChatSdk.uuidv4)();
366
402
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
367
403
  chatSDK.chatToken = {};
368
404
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
369
405
  chatSDK.reconnectId = null;
406
+ disconnectConversation(chatSDK);
407
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
408
+ chatSDK.conversation = null;
370
409
  };
371
410
 
372
411
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -67,7 +67,7 @@ const initWebChatComposer = (props, state, dispatch, facadeChatSDK, endChat) =>
67
67
  // Initialize Web Chat's redux store
68
68
  let webChatStore = _WebChatStoreLoader.WebChatStoreLoader.store;
69
69
  if (!webChatStore) {
70
- var _state$domainStates$l, _state$domainStates$l2, _state$domainStates$l3, _props$webChatContain7;
70
+ var _state$domainStates$l, _state$domainStates$l2, _props$controlProps, _state$domainStates$l3, _props$webChatContain7;
71
71
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
72
72
  const addConversationalSurveyTagsCallback = action => {
73
73
  var _inMemoryState$appSta;
@@ -126,7 +126,7 @@ const initWebChatComposer = (props, state, dispatch, facadeChatSDK, endChat) =>
126
126
  };
127
127
  webChatStore = (0, _botframeworkWebchat.createStore)({},
128
128
  //initial state
129
- _preProcessingMiddleware.default, _attachmentProcessingMiddleware.default, (0, _attachmentUploadValidatorMiddleware.default)((_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.default, (0, _customEventMiddleware.default)(_omnichannelChatComponents.BroadcastService), (0, _queueOverflowHandlerMiddleware.createQueueOverflowMiddleware)(state, dispatch), (0, _channelDataMiddleware.default)(addConversationalSurveyTagsCallback), (0, _conversationEndMiddleware.default)(conversationEndCallback, startConversationalSurveyCallback, endConversationalSurveyCallback), (0, _dataMaskingMiddleware.default)((_state$domainStates$l3 = state.domainStates.liveChatConfig) === null || _state$domainStates$l3 === void 0 ? void 0 : _state$domainStates$l3.DataMaskingInfo), _messageTimestampMiddleware.createMessageTimeStampMiddleware, _messageSequenceIdOverrideMiddleware.createMessageSequenceIdOverrideMiddleware, (0, _citationsMiddleware.createCitationsMiddleware)(state, dispatch), _gifUploadMiddleware.default, _htmlPlayerMiddleware.default, (0, _htmlTextMiddleware.default)(honorsTargetInHTMLLinks), (0, _maxMessageSizeValidator.default)(localizedTexts), _sanitizationMiddleware.default, (0, _callActionMiddleware.default)(),
129
+ _preProcessingMiddleware.default, _attachmentProcessingMiddleware.default, (0, _attachmentUploadValidatorMiddleware.default)((_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), (0, _attachmentSentAnnouncementMiddleware.default)(localizedTexts, ((_props$controlProps = props.controlProps) === null || _props$controlProps === void 0 ? void 0 : _props$controlProps.id) || "oc-lcw"), (0, _customEventMiddleware.default)(_omnichannelChatComponents.BroadcastService), (0, _queueOverflowHandlerMiddleware.createQueueOverflowMiddleware)(state, dispatch), (0, _channelDataMiddleware.default)(addConversationalSurveyTagsCallback), (0, _conversationEndMiddleware.default)(conversationEndCallback, startConversationalSurveyCallback, endConversationalSurveyCallback), (0, _dataMaskingMiddleware.default)((_state$domainStates$l3 = state.domainStates.liveChatConfig) === null || _state$domainStates$l3 === void 0 ? void 0 : _state$domainStates$l3.DataMaskingInfo), _messageTimestampMiddleware.createMessageTimeStampMiddleware, _messageSequenceIdOverrideMiddleware.createMessageSequenceIdOverrideMiddleware, (0, _citationsMiddleware.createCitationsMiddleware)(state, dispatch), _gifUploadMiddleware.default, _htmlPlayerMiddleware.default, (0, _htmlTextMiddleware.default)(honorsTargetInHTMLLinks), (0, _maxMessageSizeValidator.default)(localizedTexts), _sanitizationMiddleware.default, (0, _callActionMiddleware.default)(),
130
130
  // Pass a callback so middleware can push initials into React context for reactivity
131
131
  (0, _localizedStringsBotInitialsMiddleware.localizedStringsBotInitialsMiddleware)(initials => {
132
132
  dispatch({
@@ -256,6 +256,12 @@ const initWebChatComposer = (props, state, dispatch, facadeChatSDK, endChat) =>
256
256
  // Track what would be removed
257
257
  const removedTags = [];
258
258
  const removedAttributes = [];
259
+ const trackAttribute = name => {
260
+ const attrName = name.toLowerCase();
261
+ if (!strictConfig.ALLOWED_ATTR.includes(attrName) && attrName !== "class" && attrName !== "id") {
262
+ removedAttributes.push(attrName);
263
+ }
264
+ };
259
265
 
260
266
  // Add hooks to the isolated monitoring instance
261
267
  monitorDOMPurify.addHook("uponSanitizeElement", (node, data) => {
@@ -264,6 +270,8 @@ const initWebChatComposer = (props, state, dispatch, facadeChatSDK, endChat) =>
264
270
  // Filter out "body" tag which is DOMPurify's internal wrapper
265
271
  if (node.nodeType === 1 && !strictConfig.ALLOWED_TAGS.includes(tagName) && tagName !== "body") {
266
272
  removedTags.push(tagName);
273
+ // DOMPurify 3.4.16+ skips uponSanitizeAttribute for removed elements
274
+ Array.from(node.attributes ?? []).forEach(attr => trackAttribute(attr.name));
267
275
  }
268
276
  } catch (hookError) {
269
277
  // Silently ignore hook errors
@@ -271,10 +279,7 @@ const initWebChatComposer = (props, state, dispatch, facadeChatSDK, endChat) =>
271
279
  });
272
280
  monitorDOMPurify.addHook("uponSanitizeAttribute", (node, data) => {
273
281
  try {
274
- const attrName = data.attrName.toLowerCase();
275
- if (!strictConfig.ALLOWED_ATTR.includes(attrName) && attrName !== "class" && attrName !== "id") {
276
- removedAttributes.push(attrName);
277
- }
282
+ trackAttribute(data.attrName);
278
283
  } catch (hookError) {
279
284
  // Silently ignore hook errors
280
285
  }
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.logRecoveryEligibilityTelemetry = exports.getRecoveryEligibilityTelemetryProperties = exports.RecoveryEligibilityReason = void 0;
7
+ var _ConversationState = require("../../../contexts/common/ConversationState");
8
+ var _TelemetryConstants = require("../../../common/telemetry/TelemetryConstants");
9
+ var _TelemetryHelper = require("../../../common/telemetry/TelemetryHelper");
10
+ var _utils = require("../../../common/utils");
11
+ let RecoveryEligibilityReason = exports.RecoveryEligibilityReason = /*#__PURE__*/function (RecoveryEligibilityReason) {
12
+ RecoveryEligibilityReason["ReconnectIdPresent"] = "ReconnectIdPresent";
13
+ RecoveryEligibilityReason["ActiveContextPresent"] = "ActiveContextPresent";
14
+ RecoveryEligibilityReason["MissingContext"] = "MissingContext";
15
+ RecoveryEligibilityReason["CachedContextNonActive"] = "CachedContextNonActive";
16
+ return RecoveryEligibilityReason;
17
+ }({});
18
+ const getConversationStateName = conversationState => {
19
+ if (typeof conversationState !== "number") {
20
+ return "Unknown";
21
+ }
22
+ const stateName = _ConversationState.ConversationState[conversationState];
23
+ return typeof stateName === "string" ? stateName : "Unknown";
24
+ };
25
+ const getRecoveryEligibilityTelemetryProperties = (state, eligible) => {
26
+ var _state$appStates, _state$domainStates, _state$appStates2, _state$appStates3;
27
+ const reconnectId = (_state$appStates = state.appStates) === null || _state$appStates === void 0 ? void 0 : _state$appStates.reconnectId;
28
+ const liveChatContext = (_state$domainStates = state.domainStates) === null || _state$domainStates === void 0 ? void 0 : _state$domainStates.liveChatContext;
29
+ const hasReconnectId = !(0, _utils.isUndefinedOrEmpty)(reconnectId);
30
+ const hasLiveChatContext = !(0, _utils.isUndefinedOrEmpty)(liveChatContext);
31
+ const hasRequestId = !(0, _utils.isUndefinedOrEmpty)(liveChatContext === null || liveChatContext === void 0 ? void 0 : liveChatContext.requestId);
32
+ let reason = RecoveryEligibilityReason.ActiveContextPresent;
33
+ if (hasReconnectId) {
34
+ reason = RecoveryEligibilityReason.ReconnectIdPresent;
35
+ } else if (!hasLiveChatContext) {
36
+ reason = RecoveryEligibilityReason.MissingContext;
37
+ } else if (((_state$appStates2 = state.appStates) === null || _state$appStates2 === void 0 ? void 0 : _state$appStates2.conversationState) !== _ConversationState.ConversationState.Active) {
38
+ reason = RecoveryEligibilityReason.CachedContextNonActive;
39
+ }
40
+ return {
41
+ conversationState: getConversationStateName((_state$appStates3 = state.appStates) === null || _state$appStates3 === void 0 ? void 0 : _state$appStates3.conversationState),
42
+ hasReconnectId,
43
+ hasLiveChatContext,
44
+ hasRequestId,
45
+ eligible,
46
+ reason
47
+ };
48
+ };
49
+ exports.getRecoveryEligibilityTelemetryProperties = getRecoveryEligibilityTelemetryProperties;
50
+ const logRecoveryEligibilityTelemetry = (state, eligible) => {
51
+ try {
52
+ _TelemetryHelper.TelemetryHelper.logActionEvent(_TelemetryConstants.LogLevel.INFO, {
53
+ Event: _TelemetryConstants.TelemetryEvent.RecoveryEligibilityEvaluated,
54
+ CustomProperties: getRecoveryEligibilityTelemetryProperties(state, eligible)
55
+ });
56
+ } catch (error) {
57
+ console.warn(error);
58
+ }
59
+ };
60
+ exports.logRecoveryEligibilityTelemetry = logRecoveryEligibilityTelemetry;
@@ -24,18 +24,87 @@ var _authHelper = require("./authHelper");
24
24
  var _setPostChatContextAndLoadSurvey = require("./setPostChatContextAndLoadSurvey");
25
25
  var _persistentChatHelper = require("./persistentChatHelper");
26
26
  var _updateSessionDataForTelemetry = require("./updateSessionDataForTelemetry");
27
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
28
- let optionalParams = {};
29
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
30
- let widgetInstanceId;
31
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
32
- let popoutWidgetInstanceId;
27
+ /**
28
+ * Per widget instance mutexes for the two chat creation entry points.
29
+ *
30
+ * Every guard in the prepareStartChat -> setPreChatAndInitiateChat -> initStartChat chain
31
+ * checks `conversationState === Closed`, but React state is updated asynchronously. A second
32
+ * invocation fired within the same render cycle (for example a rapid double click on the chat
33
+ * button, or a chat button click racing a BroadcastEvent.StartChat) therefore still observes
34
+ * the stale `Closed` state, reaches `facadeChatSDK.startChat()` and creates a second
35
+ * conversation on the backend which is then orphaned and never ended.
36
+ *
37
+ * The locks are keyed by FacadeChatSDK instance rather than being module wide, because a host
38
+ * page can mount several independent widgets, each with its own widgetInstanceId and its own
39
+ * FacadeChatSDK (created once per widget in LiveChatWidget.tsx). A module wide lock would make
40
+ * widget A's start chat silently drop widget B's legitimate simultaneous start chat.
41
+ *
42
+ * The two maps are separate because prepareStartChat calls initStartChat; sharing a single lock
43
+ * would make the nested call see its own parent as in flight and drop it. prepareStartChat is
44
+ * additionally blocked by an in flight initStartChat (pre-chat submit, reconnect, or the
45
+ * hideStartChatButton bootstrap call initStartChat directly). Otherwise a later prepareStartChat
46
+ * would dispatch Loading and maximize the widget, and only then have its nested initStartChat
47
+ * dropped.
48
+ */
49
+ const prepareStartChatGuards = new WeakMap();
50
+ const initStartChatGuards = new WeakMap();
51
+
52
+ // Fallback owner used only when no FacadeChatSDK instance is available to key on, so the guard
53
+ // degrades to the module wide behaviour instead of disappearing altogether.
54
+ const unkeyedGuardOwner = {};
55
+ const getGuard = (guards, facadeChatSDK) => {
56
+ const owner = facadeChatSDK ?? unkeyedGuardOwner;
57
+ let guard = guards.get(owner);
58
+ if (!guard) {
59
+ guard = {
60
+ inFlight: false
61
+ };
62
+ guards.set(owner, guard);
63
+ }
64
+ return guard;
65
+ };
66
+
67
+ /**
68
+ * Runs `operation` only if no other invocation for the same widget instance is already in
69
+ * flight. Concurrent invocations are dropped (and reported through telemetry) rather than
70
+ * awaiting the in-flight promise, so a future re-entrant call can never deadlock on itself.
71
+ * No caller acts on the resolved value, so dropping is safe.
72
+ *
73
+ * `blockedBy` lets an operation also yield to a different, already running operation for the
74
+ * same widget instance without claiming that operation's lock.
75
+ */
76
+ const runExclusively = async (guards, facadeChatSDK, description, operation, props, blockedBy) => {
77
+ const guard = getGuard(guards, facadeChatSDK);
78
+ const blockingGuard = blockedBy ? getGuard(blockedBy, facadeChatSDK) : undefined;
79
+ if (guard.inFlight || blockingGuard !== null && blockingGuard !== void 0 && blockingGuard.inFlight) {
80
+ _TelemetryHelper.TelemetryHelper.logActionEvent(_TelemetryConstants.LogLevel.WARN, {
81
+ Event: _TelemetryConstants.TelemetryEvent.ConcurrentStartChatRejected,
82
+ Description: description,
83
+ CustomProperties: {
84
+ WidgetCacheId: props ? (0, _utils.getWidgetCacheIdfromProps)(props) : undefined
85
+ }
86
+ });
87
+ return;
88
+ }
89
+
90
+ // Claim the lock before invoking the operation, so a re-entrant call made from the
91
+ // operation's synchronous preamble (before its first await) cannot slip past the check above.
92
+ guard.inFlight = true;
93
+ try {
94
+ await operation();
95
+ } finally {
96
+ guard.inFlight = false;
97
+ }
98
+ };
33
99
 
34
100
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
35
101
  const prepareStartChat = async (props, facadeChatSDK, state, dispatch, setAdapter) => {
36
- optionalParams = {}; //Resetting to ensure no stale values
37
- widgetInstanceId = (0, _utils.getWidgetCacheIdfromProps)(props);
102
+ return runExclusively(prepareStartChatGuards, facadeChatSDK, "Concurrent prepareStartChat call ignored, a start chat operation is already in progress for this widget instance.", () => prepareStartChatInternal(props, facadeChatSDK, state, dispatch, setAdapter), props, initStartChatGuards);
103
+ };
38
104
 
105
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
106
+ exports.prepareStartChat = prepareStartChat;
107
+ const prepareStartChatInternal = async (props, facadeChatSDK, state, dispatch, setAdapter) => {
39
108
  // reconnect > chat from cache
40
109
  if ((0, _reconnectChatHelper.isReconnectEnabled)(props.chatConfig) === true && !(0, _reconnectChatHelper.isPersistentEnabled)(props.chatConfig)) {
41
110
  const shouldStartChatNormally = await (0, _reconnectChatHelper.handleChatReconnect)(facadeChatSDK, props, dispatch, setAdapter, initStartChat, state);
@@ -63,7 +132,6 @@ const prepareStartChat = async (props, facadeChatSDK, state, dispatch, setAdapte
63
132
  };
64
133
 
65
134
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
66
- exports.prepareStartChat = prepareStartChat;
67
135
  const setPreChatAndInitiateChat = async (facadeChatSDK, dispatch, setAdapter, isProactiveChat, proactiveChatEnablePrechatState, state, props) => {
68
136
  var _props$preChatSurveyP, _props$controlProps, _state$domainStates, _state$appStates2;
69
137
  // Reset before loading prechat to avoid starting chat with previous requestId
@@ -163,6 +231,12 @@ const setPreChatAndInitiateChat = async (facadeChatSDK, dispatch, setAdapter, is
163
231
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
164
232
  exports.setPreChatAndInitiateChat = setPreChatAndInitiateChat;
165
233
  const initStartChat = async (facadeChatSDK, dispatch, setAdapter, state, props, params, persistedState) => {
234
+ return runExclusively(initStartChatGuards, facadeChatSDK, "Concurrent initStartChat call ignored, a start chat operation is already in progress for this widget instance.", () => initStartChatInternal(facadeChatSDK, dispatch, setAdapter, state, props, params, persistedState), props);
235
+ };
236
+
237
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
238
+ exports.initStartChat = initStartChat;
239
+ const initStartChatInternal = async (facadeChatSDK, dispatch, setAdapter, state, props, params, persistedState) => {
166
240
  var _state$domainStates7;
167
241
  let isStartChatSuccessful = false;
168
242
  const persistentChatEnabled = (0, _liveChatConfigUtils.isPersistentChatEnabled)(state === null || state === void 0 || (_state$domainStates7 = state.domainStates) === null || _state$domainStates7 === void 0 || (_state$domainStates7 = _state$domainStates7.liveChatConfig) === null || _state$domainStates7 === void 0 || (_state$domainStates7 = _state$domainStates7.LiveWSAndLiveChatEngJoin) === null || _state$domainStates7 === void 0 ? void 0 : _state$domainStates7.msdyn_conversationmode);
@@ -201,14 +275,15 @@ const initStartChat = async (facadeChatSDK, dispatch, setAdapter, state, props,
201
275
  }
202
276
  try {
203
277
  var _window$Microsoft, _state$domainStates8;
204
- // Set custom context params
205
- await setCustomContextParams(state, props);
278
+ // Set custom context params. Returned (not stored module-side) so that concurrent
279
+ // widget instances cannot overwrite each other's custom context.
280
+ const customContextParams = await setCustomContextParams(state, props);
206
281
  const defaultOptionalParams = {
207
282
  sendDefaultInitContext: true,
208
283
  isProactiveChat: !!(params !== null && params !== void 0 && params.isProactiveChat),
209
284
  portalContactId: (_window$Microsoft = window.Microsoft) === null || _window$Microsoft === void 0 || (_window$Microsoft = _window$Microsoft.Dynamic365) === null || _window$Microsoft === void 0 || (_window$Microsoft = _window$Microsoft.Portal) === null || _window$Microsoft === void 0 || (_window$Microsoft = _window$Microsoft.User) === null || _window$Microsoft === void 0 ? void 0 : _window$Microsoft.contactId
210
285
  };
211
- const startChatOptionalParams = Object.assign({}, params, optionalParams, defaultOptionalParams);
286
+ const startChatOptionalParams = Object.assign({}, params, customContextParams, defaultOptionalParams);
212
287
 
213
288
  // MID-AUTH: Add wasAuthenticated flag for reconnect scenarios
214
289
  // Tells FacadeChatSDK whether the previous session was authenticated
@@ -291,14 +366,10 @@ const initStartChat = async (facadeChatSDK, dispatch, setAdapter, state, props,
291
366
  await (0, _updateSessionDataForTelemetry.updateTelemetryData)(facadeChatSDK, dispatch);
292
367
  } catch (ex) {
293
368
  (0, _startChatErrorHandler.handleStartChatError)(dispatch, facadeChatSDK, props, ex, isStartChatSuccessful);
294
- } finally {
295
- optionalParams = {};
296
- widgetInstanceId = "";
297
369
  }
298
370
  };
299
371
 
300
372
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
301
- exports.initStartChat = initStartChat;
302
373
  const createAdapterAndSubscribe = async (facadeChatSDK, dispatch, setAdapter, startTime, props) => {
303
374
  // New adapter creation
304
375
  const newAdapter = await (0, _createAdapter.createAdapter)(facadeChatSDK, props);
@@ -338,19 +409,22 @@ const canConnectToExistingChat = async (props, facadeChatSDK, state, dispatch, s
338
409
  return false;
339
410
  };
340
411
 
341
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
412
+ /**
413
+ * Resolves the custom context to start the chat with.
414
+ *
415
+ * Returns the resolved params instead of mutating module-level state: a page can host several
416
+ * independent widgets, and once per-instance start-chat guards allow them to run concurrently,
417
+ * shared mutable state would let one widget overwrite another widget's custom context.
418
+ */
342
419
  const setCustomContextParams = async (state, props) => {
343
420
  var _state$domainStates9, _persistedState$domai5;
344
421
  if (state !== null && state !== void 0 && (_state$domainStates9 = state.domainStates) !== null && _state$domainStates9 !== void 0 && _state$domainStates9.customContext) {
345
422
  var _state$domainStates0;
346
- optionalParams = Object.assign({}, optionalParams, {
423
+ return {
347
424
  customContext: JSON.parse(JSON.stringify(state === null || state === void 0 || (_state$domainStates0 = state.domainStates) === null || _state$domainStates0 === void 0 ? void 0 : _state$domainStates0.customContext))
348
- });
349
- return;
350
- }
351
- if ((0, _utils.isNullOrEmptyString)(widgetInstanceId)) {
352
- widgetInstanceId = (0, _utils.getWidgetCacheIdfromProps)(props);
425
+ };
353
426
  }
427
+ const widgetInstanceId = props ? (0, _utils.getWidgetCacheIdfromProps)(props) : "";
354
428
  // Add custom context only for unauthenticated chat
355
429
  const persistedState = (0, _utils.getStateFromCache)(widgetInstanceId);
356
430
  const customContextLocal = (persistedState === null || persistedState === void 0 || (_persistedState$domai5 = persistedState.domainStates) === null || _persistedState$domai5 === void 0 ? void 0 : _persistedState$domai5.customContext) ?? (props === null || props === void 0 ? void 0 : props.initialCustomContext);
@@ -359,23 +433,23 @@ const setCustomContextParams = async (state, props) => {
359
433
  Event: _TelemetryConstants.TelemetryEvent.SettingCustomContext,
360
434
  Description: "Setting custom context for unauthenticated chat"
361
435
  });
362
- optionalParams = Object.assign({}, optionalParams, {
436
+ return {
363
437
  customContext: JSON.parse(JSON.stringify(customContextLocal))
364
- });
365
- } else {
366
- const customContextFromParent = await getInitContextParamsForPopout();
367
- if (!(0, _utils.isUndefinedOrEmpty)(customContextFromParent === null || customContextFromParent === void 0 ? void 0 : customContextFromParent.contextVariables)) {
368
- optionalParams = Object.assign({}, optionalParams, {
369
- customContext: JSON.parse(JSON.stringify(customContextFromParent.contextVariables))
370
- });
371
- }
438
+ };
439
+ }
440
+ const customContextFromParent = await getInitContextParamsForPopout();
441
+ if (!(0, _utils.isUndefinedOrEmpty)(customContextFromParent === null || customContextFromParent === void 0 ? void 0 : customContextFromParent.contextVariables)) {
442
+ return {
443
+ customContext: JSON.parse(JSON.stringify(customContextFromParent.contextVariables))
444
+ };
372
445
  }
446
+ return {};
373
447
  };
374
448
  const canStartPopoutChat = async props => {
375
449
  if (props.allowSdkChatSupport === false) {
376
450
  return false;
377
451
  }
378
- popoutWidgetInstanceId = (0, _utils.getWidgetCacheIdfromProps)(props, true);
452
+ const popoutWidgetInstanceId = (0, _utils.getWidgetCacheIdfromProps)(props, true);
379
453
  if (!(0, _utils.isNullOrEmptyString)(popoutWidgetInstanceId)) {
380
454
  var _persistedState$domai6, _persistedState$appSt3;
381
455
  const persistedState = (0, _utils.getStateFromCache)(popoutWidgetInstanceId);
@@ -60,6 +60,7 @@ var _useChatAdapterStore3 = _interopRequireDefault(require("../../../hooks/useCh
60
60
  var _useChatContextStore3 = _interopRequireDefault(require("../../../hooks/useChatContextStore"));
61
61
  var _useFacadeChatSDKStore = _interopRequireDefault(require("../../../hooks/useFacadeChatSDKStore"));
62
62
  var _renderSurveyHelpers = require("../common/renderSurveyHelpers");
63
+ var _recoveryEligibilityTelemetry = require("../common/recoveryEligibilityTelemetry");
63
64
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
64
65
  function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
65
66
  function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
@@ -157,6 +158,7 @@ const LiveChatWidgetStateful = props => {
157
158
  activeCachedChatExist = false;
158
159
  optionalParams = {};
159
160
  }
161
+ (0, _recoveryEligibilityTelemetry.logRecoveryEligibilityTelemetry)(state, activeCachedChatExist);
160
162
  };
161
163
 
162
164
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -1172,18 +1174,18 @@ const LiveChatWidgetStateful = props => {
1172
1174
  .webchat__auto-resize-textarea.webchat__send-box-text-box__text-area {
1173
1175
  min-height: ${sendBoxTextArea === null || sendBoxTextArea === void 0 ? void 0 : sendBoxTextArea.minHeight} !important;
1174
1176
  }`}
1175
- `), /*#__PURE__*/_react2.default.createElement(_DraggableChatWidget.default, chatWidgetDraggableConfig, /*#__PURE__*/_react2.default.createElement(Composer, _extends({}, webChatProps, {
1176
- userID: userID,
1177
- styleOptions: styleOptions,
1178
- directLine: directLine
1179
- }), /*#__PURE__*/_react2.default.createElement(_react.Stack, {
1177
+ `), /*#__PURE__*/_react2.default.createElement(_DraggableChatWidget.default, chatWidgetDraggableConfig, /*#__PURE__*/_react2.default.createElement(_react.Stack, {
1180
1178
  id: widgetElementId,
1181
1179
  styles: generalStyles,
1182
1180
  className: (_livechatProps$styleP = livechatProps.styleProps) === null || _livechatProps$styleP === void 0 ? void 0 : _livechatProps$styleP.className,
1183
1181
  role: isWidgetOpen ? "dialog" : undefined,
1184
1182
  "aria-modal": isWidgetOpen ? true : undefined,
1185
1183
  "aria-label": isWidgetOpen ? ((_props$headerProps = props.headerProps) === null || _props$headerProps === void 0 || (_props$headerProps = _props$headerProps.controlProps) === null || _props$headerProps === void 0 || (_props$headerProps = _props$headerProps.headerTitleProps) === null || _props$headerProps === void 0 ? void 0 : _props$headerProps.text) ?? "Live Chat" : undefined
1186
- }, !((_livechatProps$contro = livechatProps.controlProps) !== null && _livechatProps$contro !== void 0 && _livechatProps$contro.hideChatButton) && !((_livechatProps$contro2 = livechatProps.controlProps) !== null && _livechatProps$contro2 !== void 0 && _livechatProps$contro2.hideStartChatButton) && (0, _componentController.shouldShowChatButton)(state) && ((0, _omnichannelChatComponents.decodeComponentString)((_livechatProps$compon = livechatProps.componentOverrides) === null || _livechatProps$compon === void 0 ? void 0 : _livechatProps$compon.chatButton) || /*#__PURE__*/_react2.default.createElement(_ChatButtonStateful.default, {
1184
+ }, /*#__PURE__*/_react2.default.createElement(Composer, _extends({}, webChatProps, {
1185
+ userID: userID,
1186
+ styleOptions: styleOptions,
1187
+ directLine: directLine
1188
+ }), !((_livechatProps$contro = livechatProps.controlProps) !== null && _livechatProps$contro !== void 0 && _livechatProps$contro.hideChatButton) && !((_livechatProps$contro2 = livechatProps.controlProps) !== null && _livechatProps$contro2 !== void 0 && _livechatProps$contro2.hideStartChatButton) && (0, _componentController.shouldShowChatButton)(state) && ((0, _omnichannelChatComponents.decodeComponentString)((_livechatProps$compon = livechatProps.componentOverrides) === null || _livechatProps$compon === void 0 ? void 0 : _livechatProps$compon.chatButton) || /*#__PURE__*/_react2.default.createElement(_ChatButtonStateful.default, {
1187
1189
  buttonProps: livechatProps.chatButtonProps,
1188
1190
  outOfOfficeButtonProps: livechatProps.outOfOfficeChatButtonProps,
1189
1191
  startChat: prepareStartChatRelay
@@ -537,9 +537,9 @@ const WebChatContainerStateful = props => {
537
537
 
538
538
  /* Suggested actions carousel previous/next navigation focus */
539
539
  .webchat__suggested-actions .webchat__suggested-actions__carousel .react-film__flipper:focus-visible .react-film__flipper__body {
540
- outline: ${(webChatContainerProps === null || webChatContainerProps === void 0 || (_webChatContainerProp20 = webChatContainerProps.webChatStyles) === null || _webChatContainerProp20 === void 0 ? void 0 : _webChatContainerProp20.suggestedActionKeyboardFocusIndicatorBorderStyle) ?? "dashed"} ${(webChatContainerProps === null || webChatContainerProps === void 0 || (_webChatContainerProp21 = webChatContainerProps.webChatStyles) === null || _webChatContainerProp21 === void 0 ? void 0 : _webChatContainerProp21.suggestedActionKeyboardFocusIndicatorBorderWidth) ?? "1px"} ${(webChatContainerProps === null || webChatContainerProps === void 0 || (_webChatContainerProp22 = webChatContainerProps.webChatStyles) === null || _webChatContainerProp22 === void 0 ? void 0 : _webChatContainerProp22.suggestedActionKeyboardFocusIndicatorBorderColor) ?? "#605E5C"} !important;
540
+ outline: ${(webChatContainerProps === null || webChatContainerProps === void 0 || (_webChatContainerProp20 = webChatContainerProps.webChatStyles) === null || _webChatContainerProp20 === void 0 ? void 0 : _webChatContainerProp20.suggestedActionKeyboardFocusIndicatorBorderStyle) ?? "solid"} ${(webChatContainerProps === null || webChatContainerProps === void 0 || (_webChatContainerProp21 = webChatContainerProps.webChatStyles) === null || _webChatContainerProp21 === void 0 ? void 0 : _webChatContainerProp21.suggestedActionKeyboardFocusIndicatorBorderWidth) ?? "2px"} ${(webChatContainerProps === null || webChatContainerProps === void 0 || (_webChatContainerProp22 = webChatContainerProps.webChatStyles) === null || _webChatContainerProp22 === void 0 ? void 0 : _webChatContainerProp22.suggestedActionKeyboardFocusIndicatorBorderColor) ?? "#605E5C"} !important;
541
541
  outline-offset: ${(webChatContainerProps === null || webChatContainerProps === void 0 || (_webChatContainerProp23 = webChatContainerProps.webChatStyles) === null || _webChatContainerProp23 === void 0 ? void 0 : _webChatContainerProp23.suggestedActionKeyboardFocusIndicatorInset) ?? "2px"} !important;
542
-
542
+ }
543
543
  `), /*#__PURE__*/_react2.default.createElement(_react.Stack, {
544
544
  styles: containerStyles,
545
545
  className: "webchat__stacked-layout_container"
@@ -33,7 +33,8 @@ const defaultMiddlewareLocalizedTexts = exports.defaultMiddlewareLocalizedTexts
33
33
  MIDDLEWARE_BANNER_CHAT_DISCONNECT: "Your conversation has been disconnected. For additional assistance, please start a new chat.",
34
34
  THIRD_PARTY_COOKIES_BLOCKED_ALERT_MESSAGE: "Allow sites to save/read cookies in browser settings. Reloading page starts a new chat.",
35
35
  MIDDLEWARE_BANNER_FILE_IS_MALICIOUS: "{0} has been blocked because the file may contain a malware.",
36
- MIDDLEWARE_BANNER_FILE_SENT: "File sent successfully.",
36
+ MIDDLEWARE_BANNER_FILE_SENT: "File {2} sent",
37
+ MIDDLEWARE_BANNER_FILES_SENT: "{0} files sent",
37
38
  MIDDLEWARE_BANNER_FILE_EMAIL_ADDRESS_RECORDED_SUCCESS: "Email will be sent after chat ends!",
38
39
  MIDDLEWARE_BANNER_FILE_EMAIL_ADDRESS_RECORDED_ERROR: "Email {0} could not be saved, try again later.",
39
40
  MIDDLEWARE_SR_PREFIX_SUCCESS: "Success. ",
@@ -94,6 +94,19 @@ const AdaptiveCardAccessibilityWrapper = _ref => {
94
94
  }
95
95
  });
96
96
 
97
+ // action-button-double-read: the adaptivecards renderer writes the action
98
+ // title to the button text, aria-label AND title. The title becomes the
99
+ // accessible description, so TalkBack reads the label twice ("Submit,
100
+ // button, Submit"). Drop the title only when it repeats the button name;
101
+ // a distinct title (an action tooltip) adds information and is kept.
102
+ container.querySelectorAll("button[title]").forEach(btn => {
103
+ const title = (btn.getAttribute("title") || "").trim();
104
+ const name = (btn.getAttribute("aria-label") ?? btn.textContent ?? "").trim();
105
+ if (title && title === name) {
106
+ btn.removeAttribute("title");
107
+ }
108
+ });
109
+
97
110
  // Find all radio inputs inside the adaptive card
98
111
  const radios = container.querySelectorAll("input[type='radio']");
99
112
  if (radios.length === 0) return;
@@ -175,7 +188,8 @@ const AdaptiveCardAccessibilityWrapper = _ref => {
175
188
 
176
189
  // Use MutationObserver to patch after WebChat finishes rendering the card.
177
190
  // Also watch attribute changes on aria-hidden so we can re-apply if React
178
- // reconciliation removes it from labels.
191
+ // reconciliation removes it from labels, and on title so a re-added
192
+ // duplicate button title is removed again.
179
193
  const observer = new MutationObserver(() => {
180
194
  patchRadioGroups();
181
195
  });
@@ -183,7 +197,7 @@ const AdaptiveCardAccessibilityWrapper = _ref => {
183
197
  childList: true,
184
198
  subtree: true,
185
199
  attributes: true,
186
- attributeFilter: ["aria-hidden"]
200
+ attributeFilter: ["aria-hidden", "title"]
187
201
  });
188
202
 
189
203
  // Also run immediately in case the card is already rendered