@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.
- package/README.md +5 -2
- package/lib/cjs/common/telemetry/TelemetryConstants.js +6 -0
- package/lib/cjs/components/chatbuttonstateful/ChatButtonStateful.js +42 -25
- package/lib/cjs/components/livechatwidget/common/endChat.js +39 -0
- package/lib/cjs/components/livechatwidget/common/initWebChatComposer.js +11 -6
- package/lib/cjs/components/livechatwidget/common/recoveryEligibilityTelemetry.js +60 -0
- package/lib/cjs/components/livechatwidget/common/startChat.js +107 -33
- package/lib/cjs/components/livechatwidget/livechatwidgetstateful/LiveChatWidgetStateful.js +8 -6
- package/lib/cjs/components/webchatcontainerstateful/WebChatContainerStateful.js +2 -2
- package/lib/cjs/components/webchatcontainerstateful/common/defaultProps/defaultMiddlewareLocalizedTexts.js +2 -1
- package/lib/cjs/components/webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/attachments/AdaptiveCardAccessibilityWrapper.js +16 -2
- package/lib/cjs/components/webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/attachmentSentAnnouncementMiddleware.js +68 -40
- package/lib/cjs/components/webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/attachmentUploadValidatorMiddleware.js +0 -27
- package/lib/esm/common/telemetry/TelemetryConstants.js +6 -0
- package/lib/esm/components/chatbuttonstateful/ChatButtonStateful.js +42 -25
- package/lib/esm/components/livechatwidget/common/endChat.js +39 -0
- package/lib/esm/components/livechatwidget/common/initWebChatComposer.js +12 -7
- package/lib/esm/components/livechatwidget/common/recoveryEligibilityTelemetry.js +52 -0
- package/lib/esm/components/livechatwidget/common/startChat.js +104 -31
- package/lib/esm/components/livechatwidget/livechatwidgetstateful/LiveChatWidgetStateful.js +8 -6
- package/lib/esm/components/webchatcontainerstateful/WebChatContainerStateful.js +2 -2
- package/lib/esm/components/webchatcontainerstateful/common/defaultProps/defaultMiddlewareLocalizedTexts.js +2 -1
- package/lib/esm/components/webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/attachments/AdaptiveCardAccessibilityWrapper.js +16 -2
- package/lib/esm/components/webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/attachmentSentAnnouncementMiddleware.js +65 -39
- package/lib/esm/components/webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/attachmentUploadValidatorMiddleware.js +0 -27
- package/lib/types/common/telemetry/TelemetryConstants.d.ts +3 -0
- package/lib/types/components/livechatwidget/common/recoveryEligibilityTelemetry.d.ts +17 -0
- package/lib/types/components/webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/attachmentSentAnnouncementMiddleware.d.ts +11 -4
- package/lib/types/contexts/common/ILiveChatWidgetLocalizedTexts.d.ts +9 -2
- package/package.json +8 -7
|
@@ -18,19 +18,86 @@ import { isMidAuthEnabled } from "./authHelper";
|
|
|
18
18
|
import { setPostChatContextAndLoadSurvey } from "./setPostChatContextAndLoadSurvey";
|
|
19
19
|
import { shouldSetPreChatIfPersistentChat } from "./persistentChatHelper";
|
|
20
20
|
import { updateTelemetryData } from "./updateSessionDataForTelemetry";
|
|
21
|
+
/**
|
|
22
|
+
* Per widget instance mutexes for the two chat creation entry points.
|
|
23
|
+
*
|
|
24
|
+
* Every guard in the prepareStartChat -> setPreChatAndInitiateChat -> initStartChat chain
|
|
25
|
+
* checks `conversationState === Closed`, but React state is updated asynchronously. A second
|
|
26
|
+
* invocation fired within the same render cycle (for example a rapid double click on the chat
|
|
27
|
+
* button, or a chat button click racing a BroadcastEvent.StartChat) therefore still observes
|
|
28
|
+
* the stale `Closed` state, reaches `facadeChatSDK.startChat()` and creates a second
|
|
29
|
+
* conversation on the backend which is then orphaned and never ended.
|
|
30
|
+
*
|
|
31
|
+
* The locks are keyed by FacadeChatSDK instance rather than being module wide, because a host
|
|
32
|
+
* page can mount several independent widgets, each with its own widgetInstanceId and its own
|
|
33
|
+
* FacadeChatSDK (created once per widget in LiveChatWidget.tsx). A module wide lock would make
|
|
34
|
+
* widget A's start chat silently drop widget B's legitimate simultaneous start chat.
|
|
35
|
+
*
|
|
36
|
+
* The two maps are separate because prepareStartChat calls initStartChat; sharing a single lock
|
|
37
|
+
* would make the nested call see its own parent as in flight and drop it. prepareStartChat is
|
|
38
|
+
* additionally blocked by an in flight initStartChat (pre-chat submit, reconnect, or the
|
|
39
|
+
* hideStartChatButton bootstrap call initStartChat directly). Otherwise a later prepareStartChat
|
|
40
|
+
* would dispatch Loading and maximize the widget, and only then have its nested initStartChat
|
|
41
|
+
* dropped.
|
|
42
|
+
*/
|
|
43
|
+
const prepareStartChatGuards = new WeakMap();
|
|
44
|
+
const initStartChatGuards = new WeakMap();
|
|
21
45
|
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
let
|
|
46
|
+
// Fallback owner used only when no FacadeChatSDK instance is available to key on, so the guard
|
|
47
|
+
// degrades to the module wide behaviour instead of disappearing altogether.
|
|
48
|
+
const unkeyedGuardOwner = {};
|
|
49
|
+
const getGuard = (guards, facadeChatSDK) => {
|
|
50
|
+
const owner = facadeChatSDK ?? unkeyedGuardOwner;
|
|
51
|
+
let guard = guards.get(owner);
|
|
52
|
+
if (!guard) {
|
|
53
|
+
guard = {
|
|
54
|
+
inFlight: false
|
|
55
|
+
};
|
|
56
|
+
guards.set(owner, guard);
|
|
57
|
+
}
|
|
58
|
+
return guard;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Runs `operation` only if no other invocation for the same widget instance is already in
|
|
63
|
+
* flight. Concurrent invocations are dropped (and reported through telemetry) rather than
|
|
64
|
+
* awaiting the in-flight promise, so a future re-entrant call can never deadlock on itself.
|
|
65
|
+
* No caller acts on the resolved value, so dropping is safe.
|
|
66
|
+
*
|
|
67
|
+
* `blockedBy` lets an operation also yield to a different, already running operation for the
|
|
68
|
+
* same widget instance without claiming that operation's lock.
|
|
69
|
+
*/
|
|
70
|
+
const runExclusively = async (guards, facadeChatSDK, description, operation, props, blockedBy) => {
|
|
71
|
+
const guard = getGuard(guards, facadeChatSDK);
|
|
72
|
+
const blockingGuard = blockedBy ? getGuard(blockedBy, facadeChatSDK) : undefined;
|
|
73
|
+
if (guard.inFlight || blockingGuard !== null && blockingGuard !== void 0 && blockingGuard.inFlight) {
|
|
74
|
+
TelemetryHelper.logActionEvent(LogLevel.WARN, {
|
|
75
|
+
Event: TelemetryEvent.ConcurrentStartChatRejected,
|
|
76
|
+
Description: description,
|
|
77
|
+
CustomProperties: {
|
|
78
|
+
WidgetCacheId: props ? getWidgetCacheIdfromProps(props) : undefined
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Claim the lock before invoking the operation, so a re-entrant call made from the
|
|
85
|
+
// operation's synchronous preamble (before its first await) cannot slip past the check above.
|
|
86
|
+
guard.inFlight = true;
|
|
87
|
+
try {
|
|
88
|
+
await operation();
|
|
89
|
+
} finally {
|
|
90
|
+
guard.inFlight = false;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
28
93
|
|
|
29
94
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
30
95
|
const prepareStartChat = async (props, facadeChatSDK, state, dispatch, setAdapter) => {
|
|
31
|
-
|
|
32
|
-
|
|
96
|
+
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);
|
|
97
|
+
};
|
|
33
98
|
|
|
99
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
100
|
+
const prepareStartChatInternal = async (props, facadeChatSDK, state, dispatch, setAdapter) => {
|
|
34
101
|
// reconnect > chat from cache
|
|
35
102
|
if (isReconnectEnabled(props.chatConfig) === true && !isPersistentEnabled(props.chatConfig)) {
|
|
36
103
|
const shouldStartChatNormally = await handleChatReconnect(facadeChatSDK, props, dispatch, setAdapter, initStartChat, state);
|
|
@@ -156,6 +223,11 @@ const setPreChatAndInitiateChat = async (facadeChatSDK, dispatch, setAdapter, is
|
|
|
156
223
|
|
|
157
224
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
158
225
|
const initStartChat = async (facadeChatSDK, dispatch, setAdapter, state, props, params, persistedState) => {
|
|
226
|
+
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);
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
230
|
+
const initStartChatInternal = async (facadeChatSDK, dispatch, setAdapter, state, props, params, persistedState) => {
|
|
159
231
|
var _state$domainStates7;
|
|
160
232
|
let isStartChatSuccessful = false;
|
|
161
233
|
const persistentChatEnabled = 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);
|
|
@@ -194,14 +266,15 @@ const initStartChat = async (facadeChatSDK, dispatch, setAdapter, state, props,
|
|
|
194
266
|
}
|
|
195
267
|
try {
|
|
196
268
|
var _window$Microsoft, _state$domainStates8;
|
|
197
|
-
// Set custom context params
|
|
198
|
-
|
|
269
|
+
// Set custom context params. Returned (not stored module-side) so that concurrent
|
|
270
|
+
// widget instances cannot overwrite each other's custom context.
|
|
271
|
+
const customContextParams = await setCustomContextParams(state, props);
|
|
199
272
|
const defaultOptionalParams = {
|
|
200
273
|
sendDefaultInitContext: true,
|
|
201
274
|
isProactiveChat: !!(params !== null && params !== void 0 && params.isProactiveChat),
|
|
202
275
|
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
|
|
203
276
|
};
|
|
204
|
-
const startChatOptionalParams = Object.assign({}, params,
|
|
277
|
+
const startChatOptionalParams = Object.assign({}, params, customContextParams, defaultOptionalParams);
|
|
205
278
|
|
|
206
279
|
// MID-AUTH: Add wasAuthenticated flag for reconnect scenarios
|
|
207
280
|
// Tells FacadeChatSDK whether the previous session was authenticated
|
|
@@ -284,9 +357,6 @@ const initStartChat = async (facadeChatSDK, dispatch, setAdapter, state, props,
|
|
|
284
357
|
await updateTelemetryData(facadeChatSDK, dispatch);
|
|
285
358
|
} catch (ex) {
|
|
286
359
|
handleStartChatError(dispatch, facadeChatSDK, props, ex, isStartChatSuccessful);
|
|
287
|
-
} finally {
|
|
288
|
-
optionalParams = {};
|
|
289
|
-
widgetInstanceId = "";
|
|
290
360
|
}
|
|
291
361
|
};
|
|
292
362
|
|
|
@@ -330,19 +400,22 @@ const canConnectToExistingChat = async (props, facadeChatSDK, state, dispatch, s
|
|
|
330
400
|
return false;
|
|
331
401
|
};
|
|
332
402
|
|
|
333
|
-
|
|
403
|
+
/**
|
|
404
|
+
* Resolves the custom context to start the chat with.
|
|
405
|
+
*
|
|
406
|
+
* Returns the resolved params instead of mutating module-level state: a page can host several
|
|
407
|
+
* independent widgets, and once per-instance start-chat guards allow them to run concurrently,
|
|
408
|
+
* shared mutable state would let one widget overwrite another widget's custom context.
|
|
409
|
+
*/
|
|
334
410
|
const setCustomContextParams = async (state, props) => {
|
|
335
411
|
var _state$domainStates9, _persistedState$domai5;
|
|
336
412
|
if (state !== null && state !== void 0 && (_state$domainStates9 = state.domainStates) !== null && _state$domainStates9 !== void 0 && _state$domainStates9.customContext) {
|
|
337
413
|
var _state$domainStates0;
|
|
338
|
-
|
|
414
|
+
return {
|
|
339
415
|
customContext: JSON.parse(JSON.stringify(state === null || state === void 0 || (_state$domainStates0 = state.domainStates) === null || _state$domainStates0 === void 0 ? void 0 : _state$domainStates0.customContext))
|
|
340
|
-
}
|
|
341
|
-
return;
|
|
342
|
-
}
|
|
343
|
-
if (isNullOrEmptyString(widgetInstanceId)) {
|
|
344
|
-
widgetInstanceId = getWidgetCacheIdfromProps(props);
|
|
416
|
+
};
|
|
345
417
|
}
|
|
418
|
+
const widgetInstanceId = props ? getWidgetCacheIdfromProps(props) : "";
|
|
346
419
|
// Add custom context only for unauthenticated chat
|
|
347
420
|
const persistedState = getStateFromCache(widgetInstanceId);
|
|
348
421
|
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);
|
|
@@ -351,23 +424,23 @@ const setCustomContextParams = async (state, props) => {
|
|
|
351
424
|
Event: TelemetryEvent.SettingCustomContext,
|
|
352
425
|
Description: "Setting custom context for unauthenticated chat"
|
|
353
426
|
});
|
|
354
|
-
|
|
427
|
+
return {
|
|
355
428
|
customContext: JSON.parse(JSON.stringify(customContextLocal))
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
}
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
const customContextFromParent = await getInitContextParamsForPopout();
|
|
432
|
+
if (!isUndefinedOrEmpty(customContextFromParent === null || customContextFromParent === void 0 ? void 0 : customContextFromParent.contextVariables)) {
|
|
433
|
+
return {
|
|
434
|
+
customContext: JSON.parse(JSON.stringify(customContextFromParent.contextVariables))
|
|
435
|
+
};
|
|
364
436
|
}
|
|
437
|
+
return {};
|
|
365
438
|
};
|
|
366
439
|
const canStartPopoutChat = async props => {
|
|
367
440
|
if (props.allowSdkChatSupport === false) {
|
|
368
441
|
return false;
|
|
369
442
|
}
|
|
370
|
-
popoutWidgetInstanceId = getWidgetCacheIdfromProps(props, true);
|
|
443
|
+
const popoutWidgetInstanceId = getWidgetCacheIdfromProps(props, true);
|
|
371
444
|
if (!isNullOrEmptyString(popoutWidgetInstanceId)) {
|
|
372
445
|
var _persistedState$domai6, _persistedState$appSt3;
|
|
373
446
|
const persistedState = getStateFromCache(popoutWidgetInstanceId);
|
|
@@ -62,6 +62,7 @@ import useChatAdapterStore from "../../../hooks/useChatAdapterStore";
|
|
|
62
62
|
import useChatContextStore from "../../../hooks/useChatContextStore";
|
|
63
63
|
import useFacadeSDKStore from "../../../hooks/useFacadeChatSDKStore";
|
|
64
64
|
import { getPostChatContext, initiatePostChat } from "../common/renderSurveyHelpers";
|
|
65
|
+
import { logRecoveryEligibilityTelemetry } from "../common/recoveryEligibilityTelemetry";
|
|
65
66
|
let uiTimer;
|
|
66
67
|
export const LiveChatWidgetStateful = props => {
|
|
67
68
|
var _props$webChatContain, _props$webChatContain2, _props$webChatContain3, _props$webChatContain4, _props$webChatContain5, _props$styleProps, _props$webChatContain6, _props$controlProps, _props$controlProps3, _state$appStates7, _props$webChatContain9, _state$appStates8, _props$webChatContain1, _props$webChatContain10, _props$controlProps10, _props$draggableChatW, _props$draggableChatW2, _props$draggableChatW3, _props$draggableChatW4, _props$draggableChatW5, _livechatProps$webCha, _livechatProps$styleP, _props$headerProps, _livechatProps$contro, _livechatProps$contro2, _livechatProps$compon, _livechatProps$contro3, _livechatProps$compon2, _livechatProps$contro4, _livechatProps$compon3, _livechatProps$contro5, _livechatProps$compon4, _livechatProps$contro6, _livechatProps$compon5, _livechatProps$contro7, _livechatProps$compon6, _livechatProps$contro8, _livechatProps$compon7, _livechatProps$contro9, _livechatProps$compon8, _livechatProps$contro0, _livechatProps$contro1, _livechatProps$compon9, _livechatProps$contro10, _livechatProps$compon0, _livechatProps$contro11, _livechatProps$compon1, _livechatProps$compon10, _livechatProps$compon11;
|
|
@@ -150,6 +151,7 @@ export const LiveChatWidgetStateful = props => {
|
|
|
150
151
|
activeCachedChatExist = false;
|
|
151
152
|
optionalParams = {};
|
|
152
153
|
}
|
|
154
|
+
logRecoveryEligibilityTelemetry(state, activeCachedChatExist);
|
|
153
155
|
};
|
|
154
156
|
|
|
155
157
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -1165,18 +1167,18 @@ export const LiveChatWidgetStateful = props => {
|
|
|
1165
1167
|
.webchat__auto-resize-textarea.webchat__send-box-text-box__text-area {
|
|
1166
1168
|
min-height: ${sendBoxTextArea === null || sendBoxTextArea === void 0 ? void 0 : sendBoxTextArea.minHeight} !important;
|
|
1167
1169
|
}`}
|
|
1168
|
-
`), /*#__PURE__*/React.createElement(DraggableChatWidget, chatWidgetDraggableConfig, /*#__PURE__*/React.createElement(
|
|
1169
|
-
userID: userID,
|
|
1170
|
-
styleOptions: styleOptions,
|
|
1171
|
-
directLine: directLine
|
|
1172
|
-
}), /*#__PURE__*/React.createElement(Stack, {
|
|
1170
|
+
`), /*#__PURE__*/React.createElement(DraggableChatWidget, chatWidgetDraggableConfig, /*#__PURE__*/React.createElement(Stack, {
|
|
1173
1171
|
id: widgetElementId,
|
|
1174
1172
|
styles: generalStyles,
|
|
1175
1173
|
className: (_livechatProps$styleP = livechatProps.styleProps) === null || _livechatProps$styleP === void 0 ? void 0 : _livechatProps$styleP.className,
|
|
1176
1174
|
role: isWidgetOpen ? "dialog" : undefined,
|
|
1177
1175
|
"aria-modal": isWidgetOpen ? true : undefined,
|
|
1178
1176
|
"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
|
|
1179
|
-
},
|
|
1177
|
+
}, /*#__PURE__*/React.createElement(Composer, _extends({}, webChatProps, {
|
|
1178
|
+
userID: userID,
|
|
1179
|
+
styleOptions: styleOptions,
|
|
1180
|
+
directLine: directLine
|
|
1181
|
+
}), !((_livechatProps$contro = livechatProps.controlProps) !== null && _livechatProps$contro !== void 0 && _livechatProps$contro.hideChatButton) && !((_livechatProps$contro2 = livechatProps.controlProps) !== null && _livechatProps$contro2 !== void 0 && _livechatProps$contro2.hideStartChatButton) && shouldShowChatButton(state) && (decodeComponentString((_livechatProps$compon = livechatProps.componentOverrides) === null || _livechatProps$compon === void 0 ? void 0 : _livechatProps$compon.chatButton) || /*#__PURE__*/React.createElement(ChatButtonStateful, {
|
|
1180
1182
|
buttonProps: livechatProps.chatButtonProps,
|
|
1181
1183
|
outOfOfficeButtonProps: livechatProps.outOfOfficeChatButtonProps,
|
|
1182
1184
|
startChat: prepareStartChatRelay
|
|
@@ -529,9 +529,9 @@ export const WebChatContainerStateful = props => {
|
|
|
529
529
|
|
|
530
530
|
/* Suggested actions carousel previous/next navigation focus */
|
|
531
531
|
.webchat__suggested-actions .webchat__suggested-actions__carousel .react-film__flipper:focus-visible .react-film__flipper__body {
|
|
532
|
-
outline: ${(webChatContainerProps === null || webChatContainerProps === void 0 || (_webChatContainerProp20 = webChatContainerProps.webChatStyles) === null || _webChatContainerProp20 === void 0 ? void 0 : _webChatContainerProp20.suggestedActionKeyboardFocusIndicatorBorderStyle) ?? "
|
|
532
|
+
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;
|
|
533
533
|
outline-offset: ${(webChatContainerProps === null || webChatContainerProps === void 0 || (_webChatContainerProp23 = webChatContainerProps.webChatStyles) === null || _webChatContainerProp23 === void 0 ? void 0 : _webChatContainerProp23.suggestedActionKeyboardFocusIndicatorInset) ?? "2px"} !important;
|
|
534
|
-
|
|
534
|
+
}
|
|
535
535
|
`), /*#__PURE__*/React.createElement(Stack, {
|
|
536
536
|
styles: containerStyles,
|
|
537
537
|
className: "webchat__stacked-layout_container"
|
|
@@ -27,7 +27,8 @@ export const defaultMiddlewareLocalizedTexts = {
|
|
|
27
27
|
MIDDLEWARE_BANNER_CHAT_DISCONNECT: "Your conversation has been disconnected. For additional assistance, please start a new chat.",
|
|
28
28
|
THIRD_PARTY_COOKIES_BLOCKED_ALERT_MESSAGE: "Allow sites to save/read cookies in browser settings. Reloading page starts a new chat.",
|
|
29
29
|
MIDDLEWARE_BANNER_FILE_IS_MALICIOUS: "{0} has been blocked because the file may contain a malware.",
|
|
30
|
-
MIDDLEWARE_BANNER_FILE_SENT: "File sent
|
|
30
|
+
MIDDLEWARE_BANNER_FILE_SENT: "File {2} sent",
|
|
31
|
+
MIDDLEWARE_BANNER_FILES_SENT: "{0} files sent",
|
|
31
32
|
MIDDLEWARE_BANNER_FILE_EMAIL_ADDRESS_RECORDED_SUCCESS: "Email will be sent after chat ends!",
|
|
32
33
|
MIDDLEWARE_BANNER_FILE_EMAIL_ADDRESS_RECORDED_ERROR: "Email {0} could not be saved, try again later.",
|
|
33
34
|
MIDDLEWARE_SR_PREFIX_SUCCESS: "Success. ",
|
|
@@ -88,6 +88,19 @@ const AdaptiveCardAccessibilityWrapper = _ref => {
|
|
|
88
88
|
}
|
|
89
89
|
});
|
|
90
90
|
|
|
91
|
+
// action-button-double-read: the adaptivecards renderer writes the action
|
|
92
|
+
// title to the button text, aria-label AND title. The title becomes the
|
|
93
|
+
// accessible description, so TalkBack reads the label twice ("Submit,
|
|
94
|
+
// button, Submit"). Drop the title only when it repeats the button name;
|
|
95
|
+
// a distinct title (an action tooltip) adds information and is kept.
|
|
96
|
+
container.querySelectorAll("button[title]").forEach(btn => {
|
|
97
|
+
const title = (btn.getAttribute("title") || "").trim();
|
|
98
|
+
const name = (btn.getAttribute("aria-label") ?? btn.textContent ?? "").trim();
|
|
99
|
+
if (title && title === name) {
|
|
100
|
+
btn.removeAttribute("title");
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
91
104
|
// Find all radio inputs inside the adaptive card
|
|
92
105
|
const radios = container.querySelectorAll("input[type='radio']");
|
|
93
106
|
if (radios.length === 0) return;
|
|
@@ -169,7 +182,8 @@ const AdaptiveCardAccessibilityWrapper = _ref => {
|
|
|
169
182
|
|
|
170
183
|
// Use MutationObserver to patch after WebChat finishes rendering the card.
|
|
171
184
|
// Also watch attribute changes on aria-hidden so we can re-apply if React
|
|
172
|
-
// reconciliation removes it from labels
|
|
185
|
+
// reconciliation removes it from labels, and on title so a re-added
|
|
186
|
+
// duplicate button title is removed again.
|
|
173
187
|
const observer = new MutationObserver(() => {
|
|
174
188
|
patchRadioGroups();
|
|
175
189
|
});
|
|
@@ -177,7 +191,7 @@ const AdaptiveCardAccessibilityWrapper = _ref => {
|
|
|
177
191
|
childList: true,
|
|
178
192
|
subtree: true,
|
|
179
193
|
attributes: true,
|
|
180
|
-
attributeFilter: ["aria-hidden"]
|
|
194
|
+
attributeFilter: ["aria-hidden", "title"]
|
|
181
195
|
});
|
|
182
196
|
|
|
183
197
|
// Also run immediately in case the card is already rendered
|
|
@@ -2,26 +2,36 @@
|
|
|
2
2
|
* AttachmentSentAnnouncementMiddleware
|
|
3
3
|
*
|
|
4
4
|
* When a file attachment is successfully sent, announces the result to
|
|
5
|
-
* screen readers
|
|
6
|
-
*
|
|
5
|
+
* screen readers so that assistive technology users receive immediate
|
|
6
|
+
* feedback ("File report.pdf sent").
|
|
7
|
+
*
|
|
8
|
+
* The announcement region lives inside the chat dialog. While the dialog is
|
|
9
|
+
* open it is aria-modal and its host siblings are aria-hidden, so screen
|
|
10
|
+
* readers (TalkBack, VoiceOver) ignore live regions outside it.
|
|
7
11
|
******/
|
|
8
12
|
|
|
9
13
|
import { isWebChatAction } from "../../../interfaces/IWebChatAction";
|
|
10
14
|
import { WebChatActionType } from "../../enums/WebChatActionType";
|
|
11
|
-
const
|
|
15
|
+
export const ATTACHMENT_ANNOUNCEMENT_REGION_ID = "oc-lcw-attachment-announcement";
|
|
16
|
+
const DEFAULT_FILE_SENT_TEXT = "File {2} sent";
|
|
17
|
+
const DEFAULT_FILES_SENT_TEXT = "{0} files sent";
|
|
18
|
+
|
|
19
|
+
// Let the send-box focus shift settle before speaking, then keep the node
|
|
20
|
+
// long enough for the screen reader to read it.
|
|
21
|
+
const ANNOUNCE_DELAY_MS = 100;
|
|
22
|
+
const ANNOUNCEMENT_LIFETIME_MS = 5000;
|
|
12
23
|
|
|
13
24
|
/**
|
|
14
|
-
* Ensures a visually-hidden aria-live region exists
|
|
15
|
-
* announcing attachment upload results to screen readers.
|
|
25
|
+
* Ensures a visually-hidden aria-live region exists inside the chat dialog
|
|
26
|
+
* for announcing attachment upload results to screen readers.
|
|
16
27
|
*/
|
|
17
|
-
const ensureAriaLiveRegion =
|
|
18
|
-
|
|
28
|
+
const ensureAriaLiveRegion = widgetElementId => {
|
|
29
|
+
const host = document.getElementById(widgetElementId) || document.body;
|
|
30
|
+
let region = document.getElementById(ATTACHMENT_ANNOUNCEMENT_REGION_ID);
|
|
19
31
|
if (!region) {
|
|
20
32
|
region = document.createElement("div");
|
|
21
|
-
region.id =
|
|
33
|
+
region.id = ATTACHMENT_ANNOUNCEMENT_REGION_ID;
|
|
22
34
|
region.setAttribute("aria-live", "polite");
|
|
23
|
-
region.setAttribute("role", "status");
|
|
24
|
-
region.setAttribute("aria-atomic", "true");
|
|
25
35
|
// Visually hidden but available to screen readers
|
|
26
36
|
Object.assign(region.style, {
|
|
27
37
|
position: "absolute",
|
|
@@ -34,44 +44,60 @@ const ensureAriaLiveRegion = () => {
|
|
|
34
44
|
whiteSpace: "nowrap",
|
|
35
45
|
border: "0"
|
|
36
46
|
});
|
|
37
|
-
|
|
38
|
-
|
|
47
|
+
}
|
|
48
|
+
if (region.parentElement !== host) {
|
|
49
|
+
host.appendChild(region);
|
|
39
50
|
}
|
|
40
51
|
return region;
|
|
41
52
|
};
|
|
42
53
|
|
|
43
54
|
/**
|
|
44
|
-
* Announces a message
|
|
45
|
-
*
|
|
55
|
+
* Announces a message by appending a fresh node to the live region.
|
|
56
|
+
* TalkBack reliably speaks newly added nodes but often misses text changes
|
|
57
|
+
* on an existing node, so each announcement gets its own element.
|
|
46
58
|
*/
|
|
47
|
-
const announce = message => {
|
|
48
|
-
const region = ensureAriaLiveRegion();
|
|
49
|
-
region.textContent = "";
|
|
50
|
-
// Small delay so the screen reader detects the content change
|
|
59
|
+
const announce = (message, widgetElementId) => {
|
|
60
|
+
const region = ensureAriaLiveRegion(widgetElementId);
|
|
51
61
|
setTimeout(() => {
|
|
52
|
-
|
|
53
|
-
|
|
62
|
+
const node = document.createElement("div");
|
|
63
|
+
node.textContent = message;
|
|
64
|
+
region.appendChild(node);
|
|
65
|
+
setTimeout(() => node.remove(), ANNOUNCEMENT_LIFETIME_MS);
|
|
66
|
+
}, ANNOUNCE_DELAY_MS);
|
|
54
67
|
};
|
|
55
68
|
|
|
56
|
-
// eslint-disable-next-line @typescript-eslint/no-
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
69
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
70
|
+
export const getFileSentMessage = (attachments, localizedTexts) => {
|
|
71
|
+
var _attachments$;
|
|
72
|
+
if (attachments.length > 1) {
|
|
73
|
+
// Replacer callbacks insert values literally; a string would treat "$&" or "$$" as patterns.
|
|
74
|
+
return ((localizedTexts === null || localizedTexts === void 0 ? void 0 : localizedTexts.MIDDLEWARE_BANNER_FILES_SENT) || DEFAULT_FILES_SENT_TEXT).replace("{0}", () => String(attachments.length));
|
|
75
|
+
}
|
|
76
|
+
const name = typeof ((_attachments$ = attachments[0]) === null || _attachments$ === void 0 ? void 0 : _attachments$.name) === "string" ? attachments[0].name.trim() : "";
|
|
77
|
+
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();
|
|
78
|
+
};
|
|
79
|
+
export const createAttachmentSentAnnouncementMiddleware = function (localizedTexts) {
|
|
80
|
+
let widgetElementId = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "oc-lcw";
|
|
81
|
+
return (
|
|
82
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
83
|
+
_ref => {
|
|
84
|
+
let dispatch = _ref.dispatch;
|
|
85
|
+
return next => action => {
|
|
86
|
+
if (!isWebChatAction(action)) {
|
|
87
|
+
return next(action);
|
|
88
|
+
}
|
|
63
89
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
90
|
+
// Intercept successfully posted activities that contain attachments
|
|
91
|
+
if (action.type === WebChatActionType.DIRECT_LINE_POST_ACTIVITY_FULFILLED) {
|
|
92
|
+
var _action$payload;
|
|
93
|
+
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;
|
|
94
|
+
if (attachments && attachments.length > 0) {
|
|
95
|
+
announce(getFileSentMessage(attachments, localizedTexts), widgetElementId);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return next(action);
|
|
99
|
+
};
|
|
73
100
|
}
|
|
74
|
-
|
|
75
|
-
};
|
|
101
|
+
);
|
|
76
102
|
};
|
|
77
|
-
export default
|
|
103
|
+
export default createAttachmentSentAnnouncementMiddleware;
|
|
@@ -12,27 +12,6 @@ import { NotificationScenarios } from "../../enums/NotificationScenarios";
|
|
|
12
12
|
import { TelemetryHelper } from "../../../../../common/telemetry/TelemetryHelper";
|
|
13
13
|
import { WebChatActionType } from "../../enums/WebChatActionType";
|
|
14
14
|
const MBtoBRatio = 1000000;
|
|
15
|
-
const announceFileSent = message => {
|
|
16
|
-
// TalkBack on Android WebView reliably announces newly *appended*
|
|
17
|
-
// role="alert" nodes but often misses text-content updates on existing
|
|
18
|
-
// nodes. Wait 500ms for the send-box focus shift to settle, then inject
|
|
19
|
-
// a fresh alert element appended to document.body and remove it after 3s.
|
|
20
|
-
setTimeout(() => {
|
|
21
|
-
const el = document.createElement("div");
|
|
22
|
-
el.setAttribute("role", "alert");
|
|
23
|
-
el.setAttribute("aria-live", "assertive");
|
|
24
|
-
el.setAttribute("aria-atomic", "true");
|
|
25
|
-
el.style.cssText = "position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden;";
|
|
26
|
-
el.textContent = message;
|
|
27
|
-
document.body.appendChild(el);
|
|
28
|
-
// Remove after TalkBack has had time to read it
|
|
29
|
-
setTimeout(() => {
|
|
30
|
-
if (el.parentNode) {
|
|
31
|
-
el.parentNode.removeChild(el);
|
|
32
|
-
}
|
|
33
|
-
}, 3000);
|
|
34
|
-
}, 500);
|
|
35
|
-
};
|
|
36
15
|
|
|
37
16
|
/*
|
|
38
17
|
* If an attachment is invalid, delete this attachment from the attachments list
|
|
@@ -221,13 +200,7 @@ const createAttachmentUploadValidatorMiddleware = (allowedFileExtensions, maxFil
|
|
|
221
200
|
var _payload$activity, _payload$activity2;
|
|
222
201
|
const payload = action.payload;
|
|
223
202
|
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)) {
|
|
224
|
-
var _validatedAction$payl;
|
|
225
203
|
const validatedAction = validateAttachment(action, allowedFileExtensions, maxFileSizeSupportedByDynamics, localizedTexts);
|
|
226
|
-
|
|
227
|
-
// Announce to screen readers that the file was sent when validation passes
|
|
228
|
-
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) {
|
|
229
|
-
announceFileSent(localizedTexts.MIDDLEWARE_BANNER_FILE_SENT ?? "File sent successfully.");
|
|
230
|
-
}
|
|
231
204
|
return next(validatedAction);
|
|
232
205
|
}
|
|
233
206
|
}
|
|
@@ -95,6 +95,7 @@ export declare enum TelemetryEvent {
|
|
|
95
95
|
MidConversationAuthSucceeded = "MidConversationAuthSucceeded",
|
|
96
96
|
MidConversationAuthFailed = "MidConversationAuthFailed",
|
|
97
97
|
MidConversationAuthReset = "MidConversationAuthReset",
|
|
98
|
+
RecoveryEligibilityEvaluated = "RecoveryEligibilityEvaluated",
|
|
98
99
|
GetConversationDetailsCallStarted = "GetConversationDetailsCallStarted",
|
|
99
100
|
GetConversationDetailsCallFailed = "GetConversationDetailsCallFailed",
|
|
100
101
|
EndChatSDKCallFailed = "EndChatSDKCallFailed",
|
|
@@ -126,6 +127,8 @@ export declare enum TelemetryEvent {
|
|
|
126
127
|
PrechatSubmitted = "PrechatSubmitted",
|
|
127
128
|
StartChatSDKCall = "StartChatCall",
|
|
128
129
|
StartChatEventReceived = "StartChatEventReceived",
|
|
130
|
+
ConcurrentStartChatRejected = "ConcurrentStartChatRejected",
|
|
131
|
+
ChatSDKStateCleanUpDisconnectFailed = "ChatSDKStateCleanUpDisconnectFailed",
|
|
129
132
|
EndChatSDKCall = "EndChatSDKCall",
|
|
130
133
|
PrepareEndChat = "PrepareEndChat",
|
|
131
134
|
EndChatEventReceived = "EndChatEventReceived",
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ILiveChatWidgetContext } from "../../../contexts/common/ILiveChatWidgetContext";
|
|
2
|
+
export declare enum RecoveryEligibilityReason {
|
|
3
|
+
ReconnectIdPresent = "ReconnectIdPresent",
|
|
4
|
+
ActiveContextPresent = "ActiveContextPresent",
|
|
5
|
+
MissingContext = "MissingContext",
|
|
6
|
+
CachedContextNonActive = "CachedContextNonActive"
|
|
7
|
+
}
|
|
8
|
+
export interface RecoveryEligibilityTelemetryProperties {
|
|
9
|
+
conversationState: string;
|
|
10
|
+
hasReconnectId: boolean;
|
|
11
|
+
hasLiveChatContext: boolean;
|
|
12
|
+
hasRequestId: boolean;
|
|
13
|
+
eligible: boolean;
|
|
14
|
+
reason: RecoveryEligibilityReason;
|
|
15
|
+
}
|
|
16
|
+
export declare const getRecoveryEligibilityTelemetryProperties: (state: ILiveChatWidgetContext, eligible: boolean) => RecoveryEligibilityTelemetryProperties;
|
|
17
|
+
export declare const logRecoveryEligibilityTelemetry: (state: ILiveChatWidgetContext, eligible: boolean) => void;
|
|
@@ -2,9 +2,16 @@
|
|
|
2
2
|
* AttachmentSentAnnouncementMiddleware
|
|
3
3
|
*
|
|
4
4
|
* When a file attachment is successfully sent, announces the result to
|
|
5
|
-
* screen readers
|
|
6
|
-
*
|
|
5
|
+
* screen readers so that assistive technology users receive immediate
|
|
6
|
+
* feedback ("File report.pdf sent").
|
|
7
|
+
*
|
|
8
|
+
* The announcement region lives inside the chat dialog. While the dialog is
|
|
9
|
+
* open it is aria-modal and its host siblings are aria-hidden, so screen
|
|
10
|
+
* readers (TalkBack, VoiceOver) ignore live regions outside it.
|
|
7
11
|
******/
|
|
12
|
+
import { ILiveChatWidgetLocalizedTexts } from "../../../../../contexts/common/ILiveChatWidgetLocalizedTexts";
|
|
8
13
|
import { WebChatStoreMiddleware } from "../../../interfaces/IWebChatAction";
|
|
9
|
-
declare const
|
|
10
|
-
export
|
|
14
|
+
export declare const ATTACHMENT_ANNOUNCEMENT_REGION_ID = "oc-lcw-attachment-announcement";
|
|
15
|
+
export declare const getFileSentMessage: (attachments: any[], localizedTexts?: ILiveChatWidgetLocalizedTexts) => string;
|
|
16
|
+
export declare const createAttachmentSentAnnouncementMiddleware: (localizedTexts?: ILiveChatWidgetLocalizedTexts, widgetElementId?: string) => WebChatStoreMiddleware;
|
|
17
|
+
export default createAttachmentSentAnnouncementMiddleware;
|
|
@@ -35,10 +35,17 @@ export interface ILiveChatWidgetLocalizedTexts {
|
|
|
35
35
|
*/
|
|
36
36
|
MIDDLEWARE_BANNER_FILE_IS_MALICIOUS?: string;
|
|
37
37
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
38
|
+
* Message announced by screen readers when one file is successfully sent.
|
|
39
|
+
* {2} - File name
|
|
40
|
+
* e.g. "File {2} sent"
|
|
40
41
|
*/
|
|
41
42
|
MIDDLEWARE_BANNER_FILE_SENT?: string;
|
|
43
|
+
/**
|
|
44
|
+
* Message announced by screen readers when several files are sent in one message.
|
|
45
|
+
* {0} - Number of files
|
|
46
|
+
* e.g. "{0} files sent"
|
|
47
|
+
*/
|
|
48
|
+
MIDDLEWARE_BANNER_FILES_SENT?: string;
|
|
42
49
|
/**
|
|
43
50
|
* Success message, indicating the email address introduced has been registered to receive the transcript.
|
|
44
51
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@microsoft/omnichannel-chat-widget",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1-main.6ca1911",
|
|
4
4
|
"description": "Microsoft Omnichannel Chat Widget",
|
|
5
5
|
"main": "lib/cjs/index.js",
|
|
6
6
|
"types": "lib/types/index.d.ts",
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
"@axe-core/react": "^4.10.2",
|
|
73
73
|
"accessibility-insights-scan": "^3.0.0",
|
|
74
74
|
"@guidepup/guidepup": "^0.24.1",
|
|
75
|
-
"postcss": "
|
|
75
|
+
"postcss": "8.5.19",
|
|
76
76
|
"react": "^18.3.1",
|
|
77
77
|
"react-dom": "^18.3.1",
|
|
78
78
|
"react-native-web": "^0.21.2",
|
|
@@ -93,18 +93,18 @@
|
|
|
93
93
|
"@azure/core-tracing": "^1.2.0",
|
|
94
94
|
"@microsoft/applicationinsights-web": "^3.3.6",
|
|
95
95
|
"@microsoft/omnichannel-chat-components": "1.2.0",
|
|
96
|
-
"@microsoft/omnichannel-chat-sdk": "2.0.
|
|
96
|
+
"@microsoft/omnichannel-chat-sdk": "2.0.2-main.a1ba517",
|
|
97
97
|
"@opentelemetry/api": "^1.9.0",
|
|
98
98
|
"abort-controller": "^3",
|
|
99
99
|
"botframework-webchat": "4.18.1-hotfix.20260308.b15b405",
|
|
100
100
|
"core-js-pure": "^3.42.0",
|
|
101
|
-
"dompurify": "
|
|
102
|
-
"markdown-it": "^14.3.
|
|
101
|
+
"dompurify": "3.4.17",
|
|
102
|
+
"markdown-it": "^14.3.3",
|
|
103
103
|
"markdown-it-attrs": "^4.1.6",
|
|
104
104
|
"markdown-it-for-inline": "^0.1.1",
|
|
105
105
|
"md5-typescript": "^1.0.5",
|
|
106
106
|
"rxjs": "5.5.12",
|
|
107
|
-
"sanitize-html": "2.
|
|
107
|
+
"sanitize-html": "2.18.1",
|
|
108
108
|
"simple-update-in": "2.2.0",
|
|
109
109
|
"slack-markdown-it": "^1.0.5"
|
|
110
110
|
},
|
|
@@ -163,7 +163,8 @@
|
|
|
163
163
|
"**/@types/minimatch": "3.0.5",
|
|
164
164
|
"**/@types/trusted-types": "2.0.7",
|
|
165
165
|
"**/join-images": "1.1.5",
|
|
166
|
-
"**/valibot": "1.4.2"
|
|
166
|
+
"**/valibot": "1.4.2",
|
|
167
|
+
"**/nanoid": "^3.3.20"
|
|
167
168
|
},
|
|
168
169
|
"jest": {
|
|
169
170
|
"verbose": true,
|