From ca80ceed5f970cd7979338534069d5b473fb9c8a Mon Sep 17 00:00:00 2001 From: Matthew Olker Date: Mon, 13 Jul 2026 14:20:45 -0500 Subject: [PATCH 01/14] feat(station-login): add E911 emergency service notification modal - Add E911 modal state and methods to store (showE911Modal, isEmergencyModalAlreadyDisplayed) - Add User Preferences API types and interfaces for fetching/updating preferences - Implement fetchUserPreferences and updateEmergencyModalAcknowledgment in store wrapper - Create E911Modal presentational component with checkbox acknowledgment - Integrate E911Modal into StationLogin widget - Trigger E911 check after successful BROWSER login - Add unit tests for E911Modal component and store methods - Fix isEndTaskEnabled -> isEndCallEnabled typo in test fixtures The modal displays for BROWSER login if user hasn't previously acknowledged. User preferences are persisted via SDK userPreference service. Note: User Preferences API requires SDK update (pending publish of 3.12.0-next.133) Refs: CAI-7905 --- .../StationLogin/E911Modal/E911Modal.tsx | 109 ++++++++++++++ .../E911Modal/e911-modal.constants.ts | 13 ++ .../E911Modal/e911-modal.style.scss | 103 +++++++++++++ .../E911Modal/e911-modal.types.ts | 5 + .../StationLogin/E911Modal/index.ts | 3 + .../contact-center/cc-components/src/index.ts | 3 + .../E911Modal/e911-modal.test.tsx | 129 ++++++++++++++++ .../station-login/src/helper.ts | 25 ++++ .../station-login/src/station-login/index.tsx | 28 +++- packages/contact-center/store/src/store.ts | 2 + .../contact-center/store/src/store.types.ts | 56 +++++++ .../store/src/storeEventsWrapper.ts | 100 +++++++++++++ .../store/tests/storeEventsWrapper.ts | 139 ++++++++++++++++++ .../test-fixtures/src/fixtures.ts | 2 +- 14 files changed, 714 insertions(+), 3 deletions(-) create mode 100644 packages/contact-center/cc-components/src/components/StationLogin/E911Modal/E911Modal.tsx create mode 100644 packages/contact-center/cc-components/src/components/StationLogin/E911Modal/e911-modal.constants.ts create mode 100644 packages/contact-center/cc-components/src/components/StationLogin/E911Modal/e911-modal.style.scss create mode 100644 packages/contact-center/cc-components/src/components/StationLogin/E911Modal/e911-modal.types.ts create mode 100644 packages/contact-center/cc-components/src/components/StationLogin/E911Modal/index.ts create mode 100644 packages/contact-center/cc-components/tests/components/StationLogin/E911Modal/e911-modal.test.tsx diff --git a/packages/contact-center/cc-components/src/components/StationLogin/E911Modal/E911Modal.tsx b/packages/contact-center/cc-components/src/components/StationLogin/E911Modal/E911Modal.tsx new file mode 100644 index 000000000..7df3a9685 --- /dev/null +++ b/packages/contact-center/cc-components/src/components/StationLogin/E911Modal/E911Modal.tsx @@ -0,0 +1,109 @@ +import React, {useRef, useEffect, useState} from 'react'; +import {Button, Text, Icon, Checkbox} from '@momentum-design/components/dist/react'; +import {E911ModalProps} from './e911-modal.types'; +import {E911ModalLabels} from './e911-modal.constants'; +import './e911-modal.style.scss'; + +const E911Modal: React.FC = ({isOpen, onSaveAndContinue, onCancel}) => { + const dialogRef = useRef(null); + const [isChecked, setIsChecked] = useState(false); + + useEffect(() => { + const dialog = dialogRef.current; + if (!dialog) return; + + if (isOpen) { + if (!dialog.open) { + dialog.showModal(); + } + } else { + if (dialog.open) { + dialog.close(); + } + setIsChecked(false); + } + }, [isOpen]); + + const handleCheckboxChange = (event: CustomEvent) => { + setIsChecked(event.detail.checked); + }; + + const handleCancel = () => { + setIsChecked(false); + onCancel(); + }; + + const handleSaveAndContinue = () => { + if (isChecked) { + onSaveAndContinue(); + } + }; + + return ( + +
+
+ + {E911ModalLabels.TITLE} + +
+ +
+
+ + + {E911ModalLabels.WARNING_TITLE} + +
+ + {E911ModalLabels.WARNING_MESSAGE} + + + {E911ModalLabels.HELP_LINK_TEXT} + +
+ +
+ + {E911ModalLabels.DIALING_TITLE} + + + {E911ModalLabels.DIALING_MESSAGE} + +
+ +
+ +
+ +
+ + +
+
+
+ ); +}; + +export default E911Modal; diff --git a/packages/contact-center/cc-components/src/components/StationLogin/E911Modal/e911-modal.constants.ts b/packages/contact-center/cc-components/src/components/StationLogin/E911Modal/e911-modal.constants.ts new file mode 100644 index 000000000..1064a566b --- /dev/null +++ b/packages/contact-center/cc-components/src/components/StationLogin/E911Modal/e911-modal.constants.ts @@ -0,0 +1,13 @@ +export const E911ModalLabels = { + TITLE: 'Emergency Service Notification', + WARNING_TITLE: 'Important', + WARNING_MESSAGE: + 'If your address has changed, update it in the Emergency Callback Number (ECBN) section of your profile before making any calls.', + HELP_LINK_TEXT: 'See help section', + DIALING_TITLE: 'Dialing Emergency Services', + DIALING_MESSAGE: + 'When you dial an emergency number (e.g., 911), your call will be routed to the appropriate emergency services based on your registered address. Ensure your address is always up to date to receive timely assistance.', + CHECKBOX_LABEL: 'I have read the notification', + CANCEL: 'Cancel', + SAVE_AND_CONTINUE: 'Save & Continue', +}; diff --git a/packages/contact-center/cc-components/src/components/StationLogin/E911Modal/e911-modal.style.scss b/packages/contact-center/cc-components/src/components/StationLogin/E911Modal/e911-modal.style.scss new file mode 100644 index 000000000..9eb54e2db --- /dev/null +++ b/packages/contact-center/cc-components/src/components/StationLogin/E911Modal/e911-modal.style.scss @@ -0,0 +1,103 @@ +.e911-modal { + padding: 0; + border: none; + border-radius: 8px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2); + max-width: 480px; + width: 90%; + + &::backdrop { + background-color: rgba(0, 0, 0, 0.5); + } + + .e911-modal-content { + padding: 24px; + } + + .e911-modal-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; + + .e911-modal-title { + margin: 0; + font-size: 18px; + font-weight: 600; + } + + .e911-close-button { + background: none; + border: none; + cursor: pointer; + padding: 4px; + } + } + + .e911-warning-box { + background-color: #fff3cd; + border: 1px solid #ffc107; + border-radius: 4px; + padding: 12px; + margin-bottom: 16px; + + .e911-warning-title { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 8px; + font-weight: 600; + color: #856404; + } + + .e911-warning-message { + color: #856404; + font-size: 14px; + line-height: 1.5; + } + + .e911-help-link { + color: #0056b3; + text-decoration: underline; + cursor: pointer; + font-size: 14px; + margin-top: 8px; + display: inline-block; + } + } + + .e911-dialing-section { + margin-bottom: 20px; + + .e911-dialing-title { + font-weight: 600; + margin-bottom: 8px; + } + + .e911-dialing-message { + font-size: 14px; + line-height: 1.5; + color: #666; + } + } + + .e911-checkbox-container { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 20px; + + .e911-checkbox-label { + font-size: 14px; + cursor: pointer; + } + } + + .e911-modal-footer { + display: flex; + justify-content: flex-end; + gap: 12px; + padding-top: 16px; + border-top: 1px solid #e0e0e0; + } +} diff --git a/packages/contact-center/cc-components/src/components/StationLogin/E911Modal/e911-modal.types.ts b/packages/contact-center/cc-components/src/components/StationLogin/E911Modal/e911-modal.types.ts new file mode 100644 index 000000000..e14bcf3d2 --- /dev/null +++ b/packages/contact-center/cc-components/src/components/StationLogin/E911Modal/e911-modal.types.ts @@ -0,0 +1,5 @@ +export interface E911ModalProps { + isOpen: boolean; + onSaveAndContinue: () => void; + onCancel: () => void; +} diff --git a/packages/contact-center/cc-components/src/components/StationLogin/E911Modal/index.ts b/packages/contact-center/cc-components/src/components/StationLogin/E911Modal/index.ts new file mode 100644 index 000000000..81fd0a313 --- /dev/null +++ b/packages/contact-center/cc-components/src/components/StationLogin/E911Modal/index.ts @@ -0,0 +1,3 @@ +export {default as E911Modal} from './E911Modal'; +export * from './e911-modal.types'; +export * from './e911-modal.constants'; diff --git a/packages/contact-center/cc-components/src/index.ts b/packages/contact-center/cc-components/src/index.ts index fc43b8061..bc0ce9dc6 100644 --- a/packages/contact-center/cc-components/src/index.ts +++ b/packages/contact-center/cc-components/src/index.ts @@ -9,6 +9,7 @@ import CampaignErrorDialogComponent from './components/task/CampaignErrorDialog/ import CampaignCountdownComponent from './components/task/CampaignCountdown/campaign-countdown'; import CampaignTaskComponent from './components/task/CampaignTask/campaign-task'; import RealTimeTranscriptComponent from './components/task/RealTimeTranscript/real-time-transcript'; +import {E911Modal} from './components/StationLogin/E911Modal'; export { UserStateComponent, @@ -22,8 +23,10 @@ export { CampaignCountdownComponent, CampaignTaskComponent, RealTimeTranscriptComponent, + E911Modal, }; export * from './components/StationLogin/constants'; +export * from './components/StationLogin/E911Modal'; export * from './components/StationLogin/station-login.types'; export * from './components/UserState/user-state.types'; export * from './components/task/task.types'; diff --git a/packages/contact-center/cc-components/tests/components/StationLogin/E911Modal/e911-modal.test.tsx b/packages/contact-center/cc-components/tests/components/StationLogin/E911Modal/e911-modal.test.tsx new file mode 100644 index 000000000..2517eea6d --- /dev/null +++ b/packages/contact-center/cc-components/tests/components/StationLogin/E911Modal/e911-modal.test.tsx @@ -0,0 +1,129 @@ +import React from 'react'; +import {render, screen, fireEvent, waitFor} from '@testing-library/react'; +import '@testing-library/jest-dom'; +import E911Modal from '../../../../src/components/StationLogin/E911Modal/E911Modal'; +import {E911ModalLabels} from '../../../../src/components/StationLogin/E911Modal/e911-modal.constants'; + +jest.mock('@webex/cc-ui-logging', () => ({ + withMetrics: (component: React.ComponentType>) => component, +})); + +describe('E911Modal', () => { + const defaultProps = { + isOpen: true, + onSaveAndContinue: jest.fn(), + onCancel: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + HTMLDialogElement.prototype.showModal = jest.fn(); + HTMLDialogElement.prototype.close = jest.fn(); + }); + + it('should render the modal when isOpen is true', () => { + render(); + expect(screen.getByTestId('e911-modal')).toBeInTheDocument(); + }); + + it('should display the modal title', () => { + render(); + expect(screen.getByText(E911ModalLabels.TITLE)).toBeInTheDocument(); + }); + + it('should display the warning message', () => { + render(); + expect(screen.getByText(E911ModalLabels.WARNING_MESSAGE)).toBeInTheDocument(); + }); + + it('should display the dialing section', () => { + render(); + expect(screen.getByText(E911ModalLabels.DIALING_TITLE)).toBeInTheDocument(); + expect(screen.getByText(E911ModalLabels.DIALING_MESSAGE)).toBeInTheDocument(); + }); + + it('should display the checkbox with label', () => { + render(); + expect(screen.getByTestId('e911-checkbox')).toBeInTheDocument(); + }); + + it('should not call onSaveAndContinue when button clicked without checkbox checked', () => { + render(); + const saveButton = screen.getByTestId('e911-save-button'); + + fireEvent.click(saveButton); + + // Button click should not trigger callback when checkbox is unchecked + expect(defaultProps.onSaveAndContinue).not.toHaveBeenCalled(); + }); + + it('should enable Save & Continue button when checkbox is checked', async () => { + render(); + const checkbox = screen.getByTestId('e911-checkbox'); + + fireEvent(checkbox, new CustomEvent('change', {detail: {checked: true}})); + + await waitFor(() => { + const saveButton = screen.getByTestId('e911-save-button'); + expect(saveButton).not.toBeDisabled(); + }); + }); + + it('should call onCancel when Cancel button is clicked', () => { + render(); + const cancelButton = screen.getByTestId('e911-cancel-button'); + + fireEvent.click(cancelButton); + + expect(defaultProps.onCancel).toHaveBeenCalledTimes(1); + }); + + it('should call onCancel when close button is clicked', () => { + render(); + const closeButton = screen.getByTestId('e911-close-button'); + + fireEvent.click(closeButton); + + expect(defaultProps.onCancel).toHaveBeenCalledTimes(1); + }); + + it('should call onSaveAndContinue when Save & Continue is clicked with checkbox checked', async () => { + render(); + const checkbox = screen.getByTestId('e911-checkbox'); + + fireEvent(checkbox, new CustomEvent('change', {detail: {checked: true}})); + + await waitFor(() => { + const saveButton = screen.getByTestId('e911-save-button'); + expect(saveButton).not.toBeDisabled(); + }); + + const saveButton = screen.getByTestId('e911-save-button'); + fireEvent.click(saveButton); + + expect(defaultProps.onSaveAndContinue).toHaveBeenCalledTimes(1); + }); + + it('should call showModal when isOpen changes to true', () => { + const {rerender} = render(); + + rerender(); + + expect(HTMLDialogElement.prototype.showModal).toHaveBeenCalled(); + }); + + it('should reset checkbox state when modal closes', () => { + const {rerender} = render(); + + rerender(); + + // Modal should attempt to close - the actual close behavior depends on dialog.open state + expect(screen.getByTestId('e911-modal')).toBeInTheDocument(); + }); + + it('should display help link', () => { + render(); + expect(screen.getByTestId('e911-help-link')).toBeInTheDocument(); + expect(screen.getByText(E911ModalLabels.HELP_LINK_TEXT)).toBeInTheDocument(); + }); +}); diff --git a/packages/contact-center/station-login/src/helper.ts b/packages/contact-center/station-login/src/helper.ts index 6aa22c3ce..bf114337e 100644 --- a/packages/contact-center/station-login/src/helper.ts +++ b/packages/contact-center/station-login/src/helper.ts @@ -237,6 +237,29 @@ export const useStationLogin = (props: UseStationLoginProps) => { } }; + const checkE911ModalDisplay = async () => { + try { + if (deviceType !== 'BROWSER') { + return; + } + + await store.fetchUserPreferences(); + + if (!store.isEmergencyModalAlreadyDisplayed) { + store.setShowE911Modal(true); + logger.log('CC-Widgets: E911 modal displayed for BROWSER login', { + module: 'widget-station-login#helper.ts', + method: 'checkE911ModalDisplay', + }); + } + } catch (error) { + logger.error(`CC-Widgets: Error checking E911 modal display - ${error.message}`, { + module: 'widget-station-login#helper.ts', + method: 'checkE911ModalDisplay', + }); + } + }; + const login = () => { try { cc.stationLogin({teamId: team, loginOption: deviceType, dialNumber}) @@ -247,6 +270,8 @@ export const useStationLogin = (props: UseStationLoginProps) => { }); setLoginSuccess(res); setLoginFailure(undefined); + + checkE911ModalDisplay(); }) .catch((error: Error) => { logger.error(`Error logging in: ${error}`, { diff --git a/packages/contact-center/station-login/src/station-login/index.tsx b/packages/contact-center/station-login/src/station-login/index.tsx index d66c9fee1..007cbb9dc 100644 --- a/packages/contact-center/station-login/src/station-login/index.tsx +++ b/packages/contact-center/station-login/src/station-login/index.tsx @@ -3,7 +3,7 @@ import store from '@webex/cc-store'; import {observer} from 'mobx-react-lite'; import {ErrorBoundary} from 'react-error-boundary'; -import {StationLoginComponent, StationLoginComponentProps} from '@webex/cc-components'; +import {StationLoginComponent, StationLoginComponentProps, E911Modal} from '@webex/cc-components'; import {useStationLogin} from '../helper'; import {StationLoginProps} from './station-login.types'; @@ -32,6 +32,9 @@ const StationLoginInternal: React.FunctionComponent = observe setDialNumber, teamId, setTeamId, + showE911Modal, + setShowE911Modal, + updateEmergencyModalAcknowledgment, } = store; const result = useStationLogin({ @@ -67,7 +70,28 @@ const StationLoginInternal: React.FunctionComponent = observe allowInternationalDn, }; - return ; + const handleE911SaveAndContinue = async () => { + try { + await updateEmergencyModalAcknowledgment(); + } catch (error) { + logger.error('CC-Widgets: Failed to update E911 acknowledgment', { + module: 'widget-station-login#index.tsx', + method: 'handleE911SaveAndContinue', + error, + }); + } + }; + + const handleE911Cancel = () => { + setShowE911Modal(false); + }; + + return ( + <> + + + + ); } ); diff --git a/packages/contact-center/store/src/store.ts b/packages/contact-center/store/src/store.ts index 69ab4bcfb..cdda48a0f 100644 --- a/packages/contact-center/store/src/store.ts +++ b/packages/contact-center/store/src/store.ts @@ -55,6 +55,8 @@ class Store implements IStore { dataCenter: string = ''; realtimeTranscriptionData: Partial[] = []; acceptedCampaignIds: Set = new Set(); + showE911Modal: boolean = false; + isEmergencyModalAlreadyDisplayed: boolean = false; constructor() { makeAutoObservable(this, { diff --git a/packages/contact-center/store/src/store.types.ts b/packages/contact-center/store/src/store.types.ts index 8635c184a..cee641c6c 100644 --- a/packages/contact-center/store/src/store.types.ts +++ b/packages/contact-center/store/src/store.types.ts @@ -73,6 +73,7 @@ interface IContactCenter { acceptPreviewContact(payload: PreviewContactPayload): Promise; skipPreviewContact(payload: PreviewContactPayload): Promise; removePreviewContact(payload: PreviewContactPayload): Promise; + userPreference?: IUserPreferenceService; } // To be fixed in SDK - https://jira-eng-sjc12.cisco.com/jira/browse/CAI-6762 type IWebex = { @@ -167,6 +168,8 @@ interface IStore { dataCenter: string; realtimeTranscriptionData: Partial[]; acceptedCampaignIds: Set; + showE911Modal: boolean; + isEmergencyModalAlreadyDisplayed: boolean; init(params: InitParams, callback: (ccSDK: IContactCenter) => void): Promise; registerCC(webex?: WithWebex['webex']): Promise; } @@ -201,6 +204,10 @@ interface IStoreWrapper extends IStore { getAccessToken(): Promise; addAcceptedCampaign(interactionId: string): void; removeAcceptedCampaign(interactionId: string): void; + setShowE911Modal(value: boolean): void; + setIsEmergencyModalAlreadyDisplayed(value: boolean): void; + fetchUserPreferences(): Promise; + updateEmergencyModalAcknowledgment(): Promise; } interface IWrapupCode { @@ -362,6 +369,55 @@ export type Participant = { name?: string; }; +/** + * Desktop preference data structure containing E911 modal acknowledgment. + * @public + */ +export type DesktopPreference = { + isEmergencyModalAlreadyDisplayed?: boolean; +}; + +/** + * User preference response from the SDK. + * @public + */ +export type UserPreferenceResponse = { + id: string; + organizationId: string; + userId: string; + desktopPreference?: string; + createdTime?: number; + lastUpdatedTime?: number; +}; + +/** + * Request payload for updating user preferences. + * @public + */ +export type UpdateUserPreferenceRequest = { + desktopPreference: string; +}; + +/** + * Query parameters for fetching user preferences. + * @public + */ +export type GetUserPreferenceParams = { + userId?: string; + page?: number; + pageSize?: number; +}; + +/** + * User Preference service interface. + * TODO: Remove once SDK exposes this interface - CAI-7906 + * @public + */ +export interface IUserPreferenceService { + getUserPreference(params?: GetUserPreferenceParams): Promise; + updateUserPreference(userId: string, data: UpdateUserPreferenceRequest): Promise; +} + /** Outbound type values that identify a campaign preview task. */ export const CAMPAIGN_PREVIEW_OUTBOUND_TYPES = ['STANDARD_PREVIEW_CAMPAIGN', 'DIRECT_PREVIEW_CAMPAIGN']; diff --git a/packages/contact-center/store/src/storeEventsWrapper.ts b/packages/contact-center/store/src/storeEventsWrapper.ts index fc12ff465..d21e88542 100644 --- a/packages/contact-center/store/src/storeEventsWrapper.ts +++ b/packages/contact-center/store/src/storeEventsWrapper.ts @@ -160,6 +160,14 @@ class StoreWrapper implements IStoreWrapper { return this.store.acceptedCampaignIds; } + get showE911Modal() { + return this.store.showE911Modal; + } + + get isEmergencyModalAlreadyDisplayed() { + return this.store.isEmergencyModalAlreadyDisplayed; + } + setDataCenter = (value: string): void => { this.store.dataCenter = value; }; @@ -581,6 +589,98 @@ class StoreWrapper implements IStoreWrapper { }); }; + setShowE911Modal = (value: boolean): void => { + runInAction(() => { + this.store.showE911Modal = value; + }); + }; + + setIsEmergencyModalAlreadyDisplayed = (value: boolean): void => { + runInAction(() => { + this.store.isEmergencyModalAlreadyDisplayed = value; + }); + }; + + fetchUserPreferences = async (): Promise => { + try { + if (!this.store.cc.userPreference) { + this.store.logger.warn('CC-Widgets: fetchUserPreferences(): userPreference service not available', { + module: 'storeEventsWrapper.ts', + method: 'fetchUserPreferences', + }); + return; + } + + const response = await this.store.cc.userPreference.getUserPreference(); + const desktopPrefString = response?.desktopPreference; + + if (desktopPrefString) { + try { + const desktopPref = JSON.parse(desktopPrefString); + runInAction(() => { + this.store.isEmergencyModalAlreadyDisplayed = desktopPref.isEmergencyModalAlreadyDisplayed ?? false; + }); + } catch (parseError) { + this.store.logger.error('CC-Widgets: fetchUserPreferences(): failed to parse desktopPreference', { + module: 'storeEventsWrapper.ts', + method: 'fetchUserPreferences', + error: parseError, + }); + } + } + } catch (error) { + this.store.logger.error('CC-Widgets: fetchUserPreferences(): failed to fetch user preferences', { + module: 'storeEventsWrapper.ts', + method: 'fetchUserPreferences', + error, + }); + throw error; + } + }; + + updateEmergencyModalAcknowledgment = async (): Promise => { + try { + if (!this.store.cc.userPreference) { + this.store.logger.warn( + 'CC-Widgets: updateEmergencyModalAcknowledgment(): userPreference service not available', + { + module: 'storeEventsWrapper.ts', + method: 'updateEmergencyModalAcknowledgment', + } + ); + return; + } + + const desktopPreference = JSON.stringify({ + isEmergencyModalAlreadyDisplayed: true, + }); + + await this.store.cc.userPreference.updateUserPreference(this.store.agentId, { + desktopPreference, + }); + + runInAction(() => { + this.store.isEmergencyModalAlreadyDisplayed = true; + this.store.showE911Modal = false; + }); + + this.store.logger.info( + 'CC-Widgets: updateEmergencyModalAcknowledgment(): successfully updated emergency modal acknowledgment', + { + module: 'storeEventsWrapper.ts', + method: 'updateEmergencyModalAcknowledgment', + } + ); + } catch (error) { + this.store.logger.error('CC-Widgets: updateEmergencyModalAcknowledgment(): failed to update user preferences', { + module: 'storeEventsWrapper.ts', + method: 'updateEmergencyModalAcknowledgment', + error, + }); + throw error; + } + }; + handleCampaignPreviewReservation = (event: ITask) => { const isCampaignPreview = this.isCampaignPreview(event); diff --git a/packages/contact-center/store/tests/storeEventsWrapper.ts b/packages/contact-center/store/tests/storeEventsWrapper.ts index 5f84dbb3b..2107b1d33 100644 --- a/packages/contact-center/store/tests/storeEventsWrapper.ts +++ b/packages/contact-center/store/tests/storeEventsWrapper.ts @@ -107,6 +107,8 @@ jest.mock('../src/store', () => ({ isDeclineButtonEnabled: false, isDigitalChannelsInitialized: false, acceptedCampaignIds: new Set(), + showE911Modal: false, + isEmergencyModalAlreadyDisplayed: false, setShowMultipleLoginAlert: jest.fn(), setCurrentState: jest.fn(), setLastStateChangeTimestamp: jest.fn(), @@ -2543,4 +2545,141 @@ describe('storeEventsWrapper', () => { }); }); }); + + describe('E911 Modal Methods', () => { + describe('setShowE911Modal', () => { + it('should set showE911Modal to true', () => { + storeWrapper.setShowE911Modal(true); + expect(storeWrapper['store'].showE911Modal).toBe(true); + }); + + it('should set showE911Modal to false', () => { + storeWrapper.setShowE911Modal(false); + expect(storeWrapper['store'].showE911Modal).toBe(false); + }); + }); + + describe('setIsEmergencyModalAlreadyDisplayed', () => { + it('should set isEmergencyModalAlreadyDisplayed to true', () => { + storeWrapper.setIsEmergencyModalAlreadyDisplayed(true); + expect(storeWrapper['store'].isEmergencyModalAlreadyDisplayed).toBe(true); + }); + + it('should set isEmergencyModalAlreadyDisplayed to false', () => { + storeWrapper.setIsEmergencyModalAlreadyDisplayed(false); + expect(storeWrapper['store'].isEmergencyModalAlreadyDisplayed).toBe(false); + }); + }); + + describe('fetchUserPreferences', () => { + it('should return early if userPreference service is not available', async () => { + storeWrapper['store'].cc.userPreference = undefined; + + await storeWrapper.fetchUserPreferences(); + + expect(storeWrapper['store'].logger.warn).toHaveBeenCalledWith( + 'CC-Widgets: fetchUserPreferences(): userPreference service not available', + expect.any(Object) + ); + }); + + it('should fetch and parse user preferences successfully', async () => { + const mockUserPreference = { + getUserPreference: jest.fn().mockResolvedValue({ + desktopPreference: JSON.stringify({isEmergencyModalAlreadyDisplayed: true}), + }), + updateUserPreference: jest.fn(), + }; + storeWrapper['store'].cc.userPreference = mockUserPreference; + + await storeWrapper.fetchUserPreferences(); + + expect(mockUserPreference.getUserPreference).toHaveBeenCalled(); + expect(storeWrapper['store'].isEmergencyModalAlreadyDisplayed).toBe(true); + }); + + it('should handle empty desktopPreference', async () => { + const mockUserPreference = { + getUserPreference: jest.fn().mockResolvedValue({ + desktopPreference: null, + }), + updateUserPreference: jest.fn(), + }; + storeWrapper['store'].cc.userPreference = mockUserPreference; + + await storeWrapper.fetchUserPreferences(); + + expect(mockUserPreference.getUserPreference).toHaveBeenCalled(); + }); + + it('should handle parse error gracefully', async () => { + const mockUserPreference = { + getUserPreference: jest.fn().mockResolvedValue({ + desktopPreference: 'invalid-json', + }), + updateUserPreference: jest.fn(), + }; + storeWrapper['store'].cc.userPreference = mockUserPreference; + + await storeWrapper.fetchUserPreferences(); + + expect(storeWrapper['store'].logger.error).toHaveBeenCalledWith( + 'CC-Widgets: fetchUserPreferences(): failed to parse desktopPreference', + expect.any(Object) + ); + }); + + it('should throw error on API failure', async () => { + const mockError = new Error('API Error'); + const mockUserPreference = { + getUserPreference: jest.fn().mockRejectedValue(mockError), + updateUserPreference: jest.fn(), + }; + storeWrapper['store'].cc.userPreference = mockUserPreference; + + await expect(storeWrapper.fetchUserPreferences()).rejects.toThrow('API Error'); + }); + }); + + describe('updateEmergencyModalAcknowledgment', () => { + it('should return early if userPreference service is not available', async () => { + storeWrapper['store'].cc.userPreference = undefined; + + await storeWrapper.updateEmergencyModalAcknowledgment(); + + expect(storeWrapper['store'].logger.warn).toHaveBeenCalledWith( + 'CC-Widgets: updateEmergencyModalAcknowledgment(): userPreference service not available', + expect.any(Object) + ); + }); + + it('should update user preferences and store state successfully', async () => { + const mockUserPreference = { + getUserPreference: jest.fn(), + updateUserPreference: jest.fn().mockResolvedValue({}), + }; + storeWrapper['store'].cc.userPreference = mockUserPreference; + storeWrapper['store'].agentId = 'test-agent-id'; + + await storeWrapper.updateEmergencyModalAcknowledgment(); + + expect(mockUserPreference.updateUserPreference).toHaveBeenCalledWith('test-agent-id', { + desktopPreference: JSON.stringify({isEmergencyModalAlreadyDisplayed: true}), + }); + expect(storeWrapper['store'].isEmergencyModalAlreadyDisplayed).toBe(true); + expect(storeWrapper['store'].showE911Modal).toBe(false); + }); + + it('should throw error on API failure', async () => { + const mockError = new Error('Update Error'); + const mockUserPreference = { + getUserPreference: jest.fn(), + updateUserPreference: jest.fn().mockRejectedValue(mockError), + }; + storeWrapper['store'].cc.userPreference = mockUserPreference; + + await expect(storeWrapper.updateEmergencyModalAcknowledgment()).rejects.toThrow('Update Error'); + }); + }); + }); }); diff --git a/packages/contact-center/test-fixtures/src/fixtures.ts b/packages/contact-center/test-fixtures/src/fixtures.ts index fbba0fb58..945ec7b50 100644 --- a/packages/contact-center/test-fixtures/src/fixtures.ts +++ b/packages/contact-center/test-fixtures/src/fixtures.ts @@ -55,7 +55,7 @@ const mockProfile: Profile = { isAgentAvailableAfterOutdial: false, isCampaignManagementEnabled: true, outDialEp: '', - isEndTaskEnabled: true, + isEndCallEnabled: true, isEndConsultEnabled: true, agentDbId: 'agentDb123', allowConsultToQueue: true, From 2f023d32e113d4e493bb0382a85ef431ba30cbe1 Mon Sep 17 00:00:00 2001 From: Matthew Olker Date: Wed, 15 Jul 2026 09:43:56 -0500 Subject: [PATCH 02/14] refactor(cc-components): rename E911Modal to lowercase and simplify exports - Rename E911Modal.tsx to e911-modal.tsx for consistency with other files - Remove index.ts barrel file, import component directly - Update cc-components index.ts to use direct imports --- .../{E911Modal.tsx => e911-modal.tsx} | 4 +- .../StationLogin/E911Modal/index.ts | 3 - .../contact-center/cc-components/src/index.ts | 4 +- packages/contact-center/store/package.json | 2 +- yarn.lock | 1186 +++++------------ 5 files changed, 335 insertions(+), 864 deletions(-) rename packages/contact-center/cc-components/src/components/StationLogin/E911Modal/{E911Modal.tsx => e911-modal.tsx} (97%) delete mode 100644 packages/contact-center/cc-components/src/components/StationLogin/E911Modal/index.ts diff --git a/packages/contact-center/cc-components/src/components/StationLogin/E911Modal/E911Modal.tsx b/packages/contact-center/cc-components/src/components/StationLogin/E911Modal/e911-modal.tsx similarity index 97% rename from packages/contact-center/cc-components/src/components/StationLogin/E911Modal/E911Modal.tsx rename to packages/contact-center/cc-components/src/components/StationLogin/E911Modal/e911-modal.tsx index 7df3a9685..1db99c012 100644 --- a/packages/contact-center/cc-components/src/components/StationLogin/E911Modal/E911Modal.tsx +++ b/packages/contact-center/cc-components/src/components/StationLogin/E911Modal/e911-modal.tsx @@ -24,8 +24,8 @@ const E911Modal: React.FC = ({isOpen, onSaveAndContinue, onCance } }, [isOpen]); - const handleCheckboxChange = (event: CustomEvent) => { - setIsChecked(event.detail.checked); + const handleCheckboxChange = () => { + setIsChecked(!isChecked); }; const handleCancel = () => { diff --git a/packages/contact-center/cc-components/src/components/StationLogin/E911Modal/index.ts b/packages/contact-center/cc-components/src/components/StationLogin/E911Modal/index.ts deleted file mode 100644 index 81fd0a313..000000000 --- a/packages/contact-center/cc-components/src/components/StationLogin/E911Modal/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export {default as E911Modal} from './E911Modal'; -export * from './e911-modal.types'; -export * from './e911-modal.constants'; diff --git a/packages/contact-center/cc-components/src/index.ts b/packages/contact-center/cc-components/src/index.ts index bc0ce9dc6..718cbf142 100644 --- a/packages/contact-center/cc-components/src/index.ts +++ b/packages/contact-center/cc-components/src/index.ts @@ -9,7 +9,7 @@ import CampaignErrorDialogComponent from './components/task/CampaignErrorDialog/ import CampaignCountdownComponent from './components/task/CampaignCountdown/campaign-countdown'; import CampaignTaskComponent from './components/task/CampaignTask/campaign-task'; import RealTimeTranscriptComponent from './components/task/RealTimeTranscript/real-time-transcript'; -import {E911Modal} from './components/StationLogin/E911Modal'; +import E911Modal from './components/StationLogin/E911Modal/e911-modal'; export { UserStateComponent, @@ -26,7 +26,7 @@ export { E911Modal, }; export * from './components/StationLogin/constants'; -export * from './components/StationLogin/E911Modal'; +export * from './components/StationLogin/E911Modal/e911-modal.types'; export * from './components/StationLogin/station-login.types'; export * from './components/UserState/user-state.types'; export * from './components/task/task.types'; diff --git a/packages/contact-center/store/package.json b/packages/contact-center/store/package.json index 4718ab223..e2b5422f3 100644 --- a/packages/contact-center/store/package.json +++ b/packages/contact-center/store/package.json @@ -23,7 +23,7 @@ "deploy:npm": "yarn npm publish" }, "dependencies": { - "@webex/contact-center": "3.12.0-next.74", + "@webex/contact-center": "3.12.0-next.81", "mobx": "6.13.5", "typescript": "5.6.3" }, diff --git a/yarn.lock b/yarn.lock index 1177e4815..2576c81b7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9560,42 +9560,18 @@ __metadata: languageName: node linkType: hard -"@webex/calling@npm:3.12.0-next.34": - version: 3.12.0-next.34 - resolution: "@webex/calling@npm:3.12.0-next.34" - dependencies: - "@types/platform": "npm:1.3.4" - "@webex/common": "npm:3.12.0-next.1" - "@webex/common-timers": "npm:3.12.0-next.1" - "@webex/internal-media-core": "npm:2.25.1" - "@webex/internal-plugin-device": "npm:3.12.0-next.15" - "@webex/internal-plugin-feature": "npm:3.12.0-next.15" - "@webex/internal-plugin-metrics": "npm:3.12.0-next.15" - "@webex/media-helpers": "npm:3.12.0-next.4" - async-mutex: "npm:0.4.0" - backoff: "npm:2.5.0" - buffer: "npm:6.0.3" - lodash: "npm:^4.17.21" - platform: "npm:1.3.6" - uuid: "npm:8.3.2" - ws: "npm:8.17.1" - xstate: "npm:4.30.6" - checksum: 10c0/721f9cafa26c610115b59e0bab6dbe7e8106796d468ed441af12dd6c301acdfd1f366851d27ea7046c59c9abd3c92e8d2e8768cf154b843016f492cb3ddbecf0 - languageName: node - linkType: hard - -"@webex/calling@npm:3.12.0-next.65": - version: 3.12.0-next.65 - resolution: "@webex/calling@npm:3.12.0-next.65" +"@webex/calling@npm:3.12.0-next.71": + version: 3.12.0-next.71 + resolution: "@webex/calling@npm:3.12.0-next.71" dependencies: "@types/platform": "npm:1.3.4" "@webex/common": "npm:3.12.0-next.5" "@webex/common-timers": "npm:3.12.0-next.5" - "@webex/internal-media-core": "npm:2.26.1" - "@webex/internal-plugin-device": "npm:3.12.0-next.26" - "@webex/internal-plugin-feature": "npm:3.12.0-next.26" - "@webex/internal-plugin-metrics": "npm:3.12.0-next.26" - "@webex/media-helpers": "npm:3.12.0-next.9" + "@webex/internal-media-core": "npm:2.26.2" + "@webex/internal-plugin-device": "npm:3.12.0-next.29" + "@webex/internal-plugin-feature": "npm:3.12.0-next.29" + "@webex/internal-plugin-metrics": "npm:3.12.0-next.29" + "@webex/media-helpers": "npm:3.12.0-next.10" async-mutex: "npm:0.4.0" backoff: "npm:2.5.0" buffer: "npm:6.0.3" @@ -9604,7 +9580,7 @@ __metadata: uuid: "npm:8.3.2" ws: "npm:8.17.1" xstate: "npm:4.30.6" - checksum: 10c0/78696f5d5a450ef6ae9282ee9ba9232fea180a7b60568f9c23fa825edd0705f14632820cf9f3af32c7e35fc49225c340814d44f4cdb88bc60fe3a978c0f807b1 + checksum: 10c0/7829fdbc71609949cab742e0f37b66f755e22ba46746e59eca4c2587c439aedc5736b2db4665011a691f331884de5e4457c5eca7b608767ed49b314fdf85e969 languageName: node linkType: hard @@ -9765,7 +9741,7 @@ __metadata: "@testing-library/react": "npm:16.0.1" "@types/jest": "npm:29.5.14" "@types/react-test-renderer": "npm:18" - "@webex/contact-center": "npm:3.12.0-next.74" + "@webex/contact-center": "npm:3.12.0-next.81" "@webex/event-dictionary-ts": "npm:^1.0.2191" "@webex/test-fixtures": "workspace:*" babel-jest: "npm:29.7.0" @@ -9991,13 +9967,6 @@ __metadata: languageName: node linkType: hard -"@webex/common-timers@npm:3.12.0-next.1": - version: 3.12.0-next.1 - resolution: "@webex/common-timers@npm:3.12.0-next.1" - checksum: 10c0/8181a7e7195df093db1ca649b32ada69b949b9f8f9d18c275120584f60ec912f5b2fa7772186f63c959eace900abb1ea4619db5bbac5839d6feda5c89758ae9a - languageName: node - linkType: hard - "@webex/common-timers@npm:3.12.0-next.5": version: 3.12.0-next.5 resolution: "@webex/common-timers@npm:3.12.0-next.5" @@ -10053,21 +10022,6 @@ __metadata: languageName: node linkType: hard -"@webex/common@npm:3.12.0-next.1": - version: 3.12.0-next.1 - resolution: "@webex/common@npm:3.12.0-next.1" - dependencies: - backoff: "npm:^2.5.0" - bowser: "npm:^2.11.0" - core-decorators: "npm:^0.20.0" - global: "npm:^4.4.0" - lodash: "npm:^4.17.21" - safe-buffer: "npm:^5.2.0" - urlsafe-base64: "npm:^1.0.0" - checksum: 10c0/6b604810d385f432671c3435211ddf097ac98d1d779acf418d1a53a9d8064e889d3381b2d48b0e43c008c806ce64387dccb1001b714acef2ab4b93f44fbb2bfe - languageName: node - linkType: hard - "@webex/common@npm:3.12.0-next.5": version: 3.12.0-next.5 resolution: "@webex/common@npm:3.12.0-next.5" @@ -10119,41 +10073,23 @@ __metadata: languageName: node linkType: hard -"@webex/contact-center@npm:3.12.0-next.42": - version: 3.12.0-next.42 - resolution: "@webex/contact-center@npm:3.12.0-next.42" +"@webex/contact-center@npm:3.12.0-next.81": + version: 3.12.0-next.81 + resolution: "@webex/contact-center@npm:3.12.0-next.81" dependencies: "@types/platform": "npm:1.3.4" - "@webex/calling": "npm:3.12.0-next.34" - "@webex/internal-plugin-mercury": "npm:3.12.0-next.16" - "@webex/internal-plugin-metrics": "npm:3.12.0-next.15" - "@webex/internal-plugin-support": "npm:3.12.0-next.17" - "@webex/plugin-authorization": "npm:3.12.0-next.15" - "@webex/plugin-logger": "npm:3.12.0-next.15" - "@webex/webex-core": "npm:3.12.0-next.15" - jest-html-reporters: "npm:3.0.11" - lodash: "npm:^4.17.21" - checksum: 10c0/7b4caa4a12dcd1070b7ed720fc56e7df357ae48126aa7a48c5ab2f854210093120891e2ab4d7cc8966539dbe204b0bf8e7d9aed22abab8216098c5dd72f760b4 - languageName: node - linkType: hard - -"@webex/contact-center@npm:3.12.0-next.74": - version: 3.12.0-next.74 - resolution: "@webex/contact-center@npm:3.12.0-next.74" - dependencies: - "@types/platform": "npm:1.3.4" - "@webex/calling": "npm:3.12.0-next.65" - "@webex/internal-plugin-mercury": "npm:3.12.0-next.27" - "@webex/internal-plugin-metrics": "npm:3.12.0-next.26" - "@webex/internal-plugin-support": "npm:3.12.0-next.28" - "@webex/plugin-authorization": "npm:3.12.0-next.26" - "@webex/plugin-logger": "npm:3.12.0-next.26" - "@webex/webex-core": "npm:3.12.0-next.26" + "@webex/calling": "npm:3.12.0-next.71" + "@webex/internal-plugin-mercury": "npm:3.12.0-next.30" + "@webex/internal-plugin-metrics": "npm:3.12.0-next.29" + "@webex/internal-plugin-support": "npm:3.12.0-next.31" + "@webex/plugin-authorization": "npm:3.12.0-next.29" + "@webex/plugin-logger": "npm:3.12.0-next.29" + "@webex/webex-core": "npm:3.12.0-next.29" jest-html-reporters: "npm:3.0.11" lodash: "npm:^4.17.21" uuid: "npm:^3.3.2" xstate: "npm:5.24.0" - checksum: 10c0/ba5eac7a197e352bd3b75c0290333310ed945500cb66ed12ec9a7937d25862da905ce1debfba9f9902eb23be9966c97e1791b5f42c1cef24d9753e898e845493 + checksum: 10c0/085269003737a9e2796838be2f68ea45ea810eaa26b68492576c7f70684a53a8fafee90e51e377ab987eec96f154e47394a69e18f12927e8e178961eb6695450 languageName: node linkType: hard @@ -10188,15 +10124,6 @@ __metadata: languageName: node linkType: hard -"@webex/helper-html@npm:3.12.0-next.1": - version: 3.12.0-next.1 - resolution: "@webex/helper-html@npm:3.12.0-next.1" - dependencies: - lodash: "npm:^4.17.21" - checksum: 10c0/cf325224593d4f2b52d0542828ef60bc22343721aa84047e97b3c12bc133f1e105db06c01ae9fb6f32bef6cae2a8f2e8b6c4aecae5083ffc72fad811dc6f88be - languageName: node - linkType: hard - "@webex/helper-html@npm:3.12.0-next.5": version: 3.12.0-next.5 resolution: "@webex/helper-html@npm:3.12.0-next.5" @@ -10240,23 +10167,6 @@ __metadata: languageName: node linkType: hard -"@webex/helper-image@npm:3.12.0-next.1": - version: 3.12.0-next.1 - resolution: "@webex/helper-image@npm:3.12.0-next.1" - dependencies: - "@webex/http-core": "npm:3.12.0-next.1" - "@webex/test-helper-chai": "npm:3.12.0-next.1" - "@webex/test-helper-file": "npm:3.12.0-next.1" - "@webex/test-helper-mocha": "npm:3.12.0-next.1" - exifr: "npm:^5.0.3" - gm: "npm:^1.23.1" - lodash: "npm:^4.17.21" - mime: "npm:^2.4.4" - safe-buffer: "npm:^5.2.0" - checksum: 10c0/1ab6b056cf208541876299b6ba984856c2f65ccf1f2f2c34b3e2eb0eb963b14eba4759178dbb99e86dfbbaa6e515ec958d591450ac16902339f138b86a78a461 - languageName: node - linkType: hard - "@webex/helper-image@npm:3.12.0-next.5": version: 3.12.0-next.5 resolution: "@webex/helper-image@npm:3.12.0-next.5" @@ -10333,24 +10243,6 @@ __metadata: languageName: node linkType: hard -"@webex/http-core@npm:3.12.0-next.1": - version: 3.12.0-next.1 - resolution: "@webex/http-core@npm:3.12.0-next.1" - dependencies: - "@webex/common": "npm:3.12.0-next.1" - file-type: "npm:^16.0.1" - global: "npm:^4.4.0" - is-function: "npm:^1.0.1" - lodash: "npm:^4.17.21" - parse-headers: "npm:^2.0.2" - qs: "npm:^6.7.3" - request: "npm:^2.88.0" - safe-buffer: "npm:^5.2.0" - xtend: "npm:^4.0.2" - checksum: 10c0/438aae8087f00da83b7a8790a74605167fc60ff5256bb79cd900803ee547b99046b8d2c7898f0c4d91092f28eee65ba8a8c2df591c3d784fff1b5e2b65af3876 - languageName: node - linkType: hard - "@webex/http-core@npm:3.12.0-next.5": version: 3.12.0-next.5 resolution: "@webex/http-core@npm:3.12.0-next.5" @@ -10386,29 +10278,9 @@ __metadata: languageName: node linkType: hard -"@webex/internal-media-core@npm:2.25.1": - version: 2.25.1 - resolution: "@webex/internal-media-core@npm:2.25.1" - dependencies: - "@babel/runtime": "npm:^7.18.9" - "@babel/runtime-corejs2": "npm:^7.25.0" - "@webex/rtcstats": "npm:^1.5.5" - "@webex/ts-sdp": "npm:1.8.2" - "@webex/web-capabilities": "npm:^1.10.0" - "@webex/web-client-media-engine": "npm:3.39.12" - events: "npm:^3.3.0" - ip-anonymize: "npm:^0.1.0" - typed-emitter: "npm:^2.1.0" - uuid: "npm:^8.3.2" - webrtc-adapter: "npm:^8.1.2" - xstate: "npm:^4.30.6" - checksum: 10c0/7a726f3226c1a0d4a1b60abccbeae8d5a60560bb11d5f0877c8411fbdb91f2f86f56ac83fad1bc5ace21d45afdbc20f810340e74b1986d785fd9bfb8cee04eb1 - languageName: node - linkType: hard - -"@webex/internal-media-core@npm:2.26.1": - version: 2.26.1 - resolution: "@webex/internal-media-core@npm:2.26.1" +"@webex/internal-media-core@npm:2.26.2": + version: 2.26.2 + resolution: "@webex/internal-media-core@npm:2.26.2" dependencies: "@babel/runtime": "npm:^7.18.9" "@babel/runtime-corejs2": "npm:^7.25.0" @@ -10422,7 +10294,7 @@ __metadata: uuid: "npm:^8.3.2" webrtc-adapter: "npm:^8.1.2" xstate: "npm:^4.30.6" - checksum: 10c0/b04e55649b06852d27d7d02254af66f9a2254e1ec755276bb233511bc902c5db475e32b148830f99b335b3496f9377f036dc1daabadefd8398d3483bcaf8f5c3 + checksum: 10c0/3755c93df231e0af3e5c4b145489fe4b051ac4d5e9832d85c346e633168c67e3f58d78d2dce2131a7a003afba99df772647ba94d00c41cddf311ef5fd4714c7a languageName: node linkType: hard @@ -10440,17 +10312,17 @@ __metadata: languageName: node linkType: hard -"@webex/internal-plugin-calendar@npm:3.12.0-next.17": - version: 3.12.0-next.17 - resolution: "@webex/internal-plugin-calendar@npm:3.12.0-next.17" +"@webex/internal-plugin-calendar@npm:3.12.0-next.31": + version: 3.12.0-next.31 + resolution: "@webex/internal-plugin-calendar@npm:3.12.0-next.31" dependencies: - "@webex/internal-plugin-conversation": "npm:3.12.0-next.17" - "@webex/internal-plugin-device": "npm:3.12.0-next.15" - "@webex/internal-plugin-encryption": "npm:3.12.0-next.16" - "@webex/webex-core": "npm:3.12.0-next.15" + "@webex/internal-plugin-conversation": "npm:3.12.0-next.31" + "@webex/internal-plugin-device": "npm:3.12.0-next.29" + "@webex/internal-plugin-encryption": "npm:3.12.0-next.30" + "@webex/webex-core": "npm:3.12.0-next.29" lodash: "npm:^4.17.21" uuid: "npm:^3.3.2" - checksum: 10c0/9461f0f5659a686da11686def1fdc1b265dd5185a50fe3e6e800f4dd6b5f5a88612e6a50d0ecfa39999ee79998e94f77f90f809f89543d1676ae353b727677d5 + checksum: 10c0/97fc8cb51d57cb6dd74d4b50691be4c8b2c9b6ff45be8abb80bb612c985c276ef43adff9df45059dbc85603bd051070bead80da6b6bd602370a2d82488338a06 languageName: node linkType: hard @@ -10490,39 +10362,21 @@ __metadata: languageName: node linkType: hard -"@webex/internal-plugin-conversation@npm:3.12.0-next.17": - version: 3.12.0-next.17 - resolution: "@webex/internal-plugin-conversation@npm:3.12.0-next.17" - dependencies: - "@webex/common": "npm:3.12.0-next.1" - "@webex/helper-html": "npm:3.12.0-next.1" - "@webex/helper-image": "npm:3.12.0-next.1" - "@webex/internal-plugin-encryption": "npm:3.12.0-next.16" - "@webex/internal-plugin-user": "npm:3.12.0-next.16" - "@webex/webex-core": "npm:3.12.0-next.15" - crypto-js: "npm:^4.1.1" - lodash: "npm:^4.17.21" - node-scr: "npm:^0.3.0" - uuid: "npm:^3.3.2" - checksum: 10c0/0d7345f75b13b905b6c3cc5a94bb37f36788a1750789b09a392e2046686916eceddc1d1913c34d850d3b2c5a33b8f3b5945415985c0d637b9879d4a57cb4065d - languageName: node - linkType: hard - -"@webex/internal-plugin-conversation@npm:3.12.0-next.28": - version: 3.12.0-next.28 - resolution: "@webex/internal-plugin-conversation@npm:3.12.0-next.28" +"@webex/internal-plugin-conversation@npm:3.12.0-next.31": + version: 3.12.0-next.31 + resolution: "@webex/internal-plugin-conversation@npm:3.12.0-next.31" dependencies: "@webex/common": "npm:3.12.0-next.5" "@webex/helper-html": "npm:3.12.0-next.5" "@webex/helper-image": "npm:3.12.0-next.5" - "@webex/internal-plugin-encryption": "npm:3.12.0-next.27" - "@webex/internal-plugin-user": "npm:3.12.0-next.27" - "@webex/webex-core": "npm:3.12.0-next.26" + "@webex/internal-plugin-encryption": "npm:3.12.0-next.30" + "@webex/internal-plugin-user": "npm:3.12.0-next.30" + "@webex/webex-core": "npm:3.12.0-next.29" crypto-js: "npm:^4.1.1" lodash: "npm:^4.17.21" node-scr: "npm:^0.3.0" uuid: "npm:^3.3.2" - checksum: 10c0/9a7983f0371878cdc50d58212b5c7586ec24a83765d19afbbc0f7a4aa2e22f7a902c167fc2c806c26947602c0b88f77cf14159cad57b13b925198d4eead301ea + checksum: 10c0/b7eb861bc3d2e08a05c9ded53f5fd404f8dc75474743d3cc7859f02b6a607382bc26b6531a5168d72aefa7da368b7c1dc688e431d7d16ba6824ba24f023df19a languageName: node linkType: hard @@ -10559,51 +10413,34 @@ __metadata: languageName: node linkType: hard -"@webex/internal-plugin-device@npm:3.12.0-next.15": - version: 3.12.0-next.15 - resolution: "@webex/internal-plugin-device@npm:3.12.0-next.15" - dependencies: - "@webex/common": "npm:3.12.0-next.1" - "@webex/common-timers": "npm:3.12.0-next.1" - "@webex/http-core": "npm:3.12.0-next.1" - "@webex/internal-plugin-metrics": "npm:3.12.0-next.15" - "@webex/webex-core": "npm:3.12.0-next.15" - ampersand-collection: "npm:^2.0.2" - ampersand-state: "npm:^5.0.3" - lodash: "npm:^4.17.21" - uuid: "npm:^3.3.2" - checksum: 10c0/c9bef045a01429c75ef55eee0fcdc08e6d647e4a35a81217963157a24c7e454093419a9a68203bdd3fb3f1081ba269d96d410b870e1eae5f4154691f40dfb8ed - languageName: node - linkType: hard - -"@webex/internal-plugin-device@npm:3.12.0-next.26": - version: 3.12.0-next.26 - resolution: "@webex/internal-plugin-device@npm:3.12.0-next.26" +"@webex/internal-plugin-device@npm:3.12.0-next.29": + version: 3.12.0-next.29 + resolution: "@webex/internal-plugin-device@npm:3.12.0-next.29" dependencies: "@webex/common": "npm:3.12.0-next.5" "@webex/common-timers": "npm:3.12.0-next.5" "@webex/http-core": "npm:3.12.0-next.5" - "@webex/internal-plugin-metrics": "npm:3.12.0-next.26" - "@webex/webex-core": "npm:3.12.0-next.26" + "@webex/internal-plugin-metrics": "npm:3.12.0-next.29" + "@webex/webex-core": "npm:3.12.0-next.29" ampersand-collection: "npm:^2.0.2" ampersand-state: "npm:^5.0.3" lodash: "npm:^4.17.21" uuid: "npm:^3.3.2" - checksum: 10c0/fa7c4950c569e3f0f6413de08b917ee1df57b00f9615861e3f1cea9e5f9075e6a51182fe6a63251a9925df236b43a0d739a9344072cec33a2f3fd13759862400 + checksum: 10c0/12da3a9c25af92ef0f06a085d9a5be4c43fa571314f13168fbddd37fedb0744d2daa0b122bc26b3f25695981b43805ba03ff66f6dce871b415bbd75aedb62ca5 languageName: node linkType: hard -"@webex/internal-plugin-dss@npm:3.12.0-next.16": - version: 3.12.0-next.16 - resolution: "@webex/internal-plugin-dss@npm:3.12.0-next.16" +"@webex/internal-plugin-dss@npm:3.12.0-next.30": + version: 3.12.0-next.30 + resolution: "@webex/internal-plugin-dss@npm:3.12.0-next.30" dependencies: - "@webex/common": "npm:3.12.0-next.1" - "@webex/common-timers": "npm:3.12.0-next.1" - "@webex/internal-plugin-mercury": "npm:3.12.0-next.16" - "@webex/webex-core": "npm:3.12.0-next.15" + "@webex/common": "npm:3.12.0-next.5" + "@webex/common-timers": "npm:3.12.0-next.5" + "@webex/internal-plugin-mercury": "npm:3.12.0-next.30" + "@webex/webex-core": "npm:3.12.0-next.29" lodash: "npm:^4.17.21" uuid: "npm:^3.3.2" - checksum: 10c0/bb7d8c041171235beb09df72731256824daa50e2a662b5aaad8af53105b6833371d75785613c9df851b658e337cc64cbb1c1f4868970124a7542dc5898a3817b + checksum: 10c0/eae4c134b85aea08f579bfd549bde3ca2a2e7613cdac13e1df2ee77fb860e47be135c21d43633d4fce5aa3156632e70e1526fd3e8f0b084195e77febd6040313 languageName: node linkType: hard @@ -10659,43 +10496,17 @@ __metadata: languageName: node linkType: hard -"@webex/internal-plugin-encryption@npm:3.12.0-next.16": - version: 3.12.0-next.16 - resolution: "@webex/internal-plugin-encryption@npm:3.12.0-next.16" - dependencies: - "@webex/common": "npm:3.12.0-next.1" - "@webex/common-timers": "npm:3.12.0-next.1" - "@webex/http-core": "npm:3.12.0-next.1" - "@webex/internal-plugin-device": "npm:3.12.0-next.15" - "@webex/internal-plugin-mercury": "npm:3.12.0-next.16" - "@webex/test-helper-file": "npm:3.12.0-next.1" - "@webex/webex-core": "npm:3.12.0-next.15" - asn1js: "npm:^2.0.26" - debug: "npm:^4.3.4" - isomorphic-webcrypto: "npm:^2.3.8" - lodash: "npm:^4.17.21" - node-jose: "npm:^2.2.0" - node-kms: "npm:^0.4.1" - node-scr: "npm:^0.3.0" - pkijs: "npm:^2.1.84" - safe-buffer: "npm:^5.2.0" - uuid: "npm:^3.3.2" - valid-url: "npm:^1.0.9" - checksum: 10c0/ed33bdfca34f2fe0bb81ef7b7ebcd4b336c7c36fdd6bc659941e56b5666448e7daf19fb1a0f7481d25ddb1924cbda8a536b87328196facccda0dca98e0942bc7 - languageName: node - linkType: hard - -"@webex/internal-plugin-encryption@npm:3.12.0-next.27": - version: 3.12.0-next.27 - resolution: "@webex/internal-plugin-encryption@npm:3.12.0-next.27" +"@webex/internal-plugin-encryption@npm:3.12.0-next.30": + version: 3.12.0-next.30 + resolution: "@webex/internal-plugin-encryption@npm:3.12.0-next.30" dependencies: "@webex/common": "npm:3.12.0-next.5" "@webex/common-timers": "npm:3.12.0-next.5" "@webex/http-core": "npm:3.12.0-next.5" - "@webex/internal-plugin-device": "npm:3.12.0-next.26" - "@webex/internal-plugin-mercury": "npm:3.12.0-next.27" + "@webex/internal-plugin-device": "npm:3.12.0-next.29" + "@webex/internal-plugin-mercury": "npm:3.12.0-next.30" "@webex/test-helper-file": "npm:3.12.0-next.5" - "@webex/webex-core": "npm:3.12.0-next.26" + "@webex/webex-core": "npm:3.12.0-next.29" asn1js: "npm:^2.0.26" debug: "npm:^4.3.4" isomorphic-webcrypto: "npm:^2.3.8" @@ -10707,7 +10518,7 @@ __metadata: safe-buffer: "npm:^5.2.0" uuid: "npm:^3.3.2" valid-url: "npm:^1.0.9" - checksum: 10c0/7019b7c39039b932773b71aec9990649e468bd2d50a19cf9d0fab2b5ee1c176b8d8e8bf28f60ac769baf96b9f9c6d236df894dff2cb7bf2d248762b3566c0b2a + checksum: 10c0/b2fff3f5a8e35675d46cc09dbc9658abab8b64d4889de99945d5cb8a28041890098dc5cdfa99147ac2aea9916571ef9d74b48430e3086dd2eaad1c205ed9efbd languageName: node linkType: hard @@ -10734,34 +10545,23 @@ __metadata: languageName: node linkType: hard -"@webex/internal-plugin-feature@npm:3.12.0-next.15": - version: 3.12.0-next.15 - resolution: "@webex/internal-plugin-feature@npm:3.12.0-next.15" +"@webex/internal-plugin-feature@npm:3.12.0-next.29": + version: 3.12.0-next.29 + resolution: "@webex/internal-plugin-feature@npm:3.12.0-next.29" dependencies: - "@webex/internal-plugin-device": "npm:3.12.0-next.15" - "@webex/webex-core": "npm:3.12.0-next.15" + "@webex/internal-plugin-device": "npm:3.12.0-next.29" + "@webex/webex-core": "npm:3.12.0-next.29" lodash: "npm:^4.17.21" - checksum: 10c0/c6d99e9b93ac300ec601c8d1da5a33e5249aadf49aec14166f5a14000045fcf07b28bf55890691680dab219f7f5291e9b2b74af56bf6473121eb862d402c439b + checksum: 10c0/4129694d08f9c5887ede7c8f2cdc8fd967aea52b8bd322da3537c497e0b82cd5473d750e62f4007b0c869729c4d3a722b4f7332d44b526687c833feaa3bfc6d1 languageName: node linkType: hard -"@webex/internal-plugin-feature@npm:3.12.0-next.26": - version: 3.12.0-next.26 - resolution: "@webex/internal-plugin-feature@npm:3.12.0-next.26" +"@webex/internal-plugin-llm@npm:3.12.0-next.33": + version: 3.12.0-next.33 + resolution: "@webex/internal-plugin-llm@npm:3.12.0-next.33" dependencies: - "@webex/internal-plugin-device": "npm:3.12.0-next.26" - "@webex/webex-core": "npm:3.12.0-next.26" - lodash: "npm:^4.17.21" - checksum: 10c0/44f1023e93cb4cc52be9cfb3c421a54bb85fa7009b65e0cc24262803835987c7afee97da67ad1eb63778e1623899a9212de451b4ba954210a3a0fb85f282d52c - languageName: node - linkType: hard - -"@webex/internal-plugin-llm@npm:3.12.0-next.18": - version: 3.12.0-next.18 - resolution: "@webex/internal-plugin-llm@npm:3.12.0-next.18" - dependencies: - "@webex/internal-plugin-mercury": "npm:3.12.0-next.16" - checksum: 10c0/a045c09cce44b6e3d6048dbb79bc3a4becd5206ba3c9c91529dc5eee707847d21b8bc57423290e055949b3dbed70d2db8fc3560867642000a5a888f1250e6a7b + "@webex/internal-plugin-mercury": "npm:3.12.0-next.30" + checksum: 10c0/9bf24edb69b07a1ecf36347bbce674a9c36f3f9717950c21a32cdd6017fe218ea23a3f71a391c33db6f9dc18cfdefe4d5b8b18edb40f9131b4a235a11fecf2ab languageName: node linkType: hard @@ -10779,17 +10579,17 @@ __metadata: languageName: node linkType: hard -"@webex/internal-plugin-locus@npm:3.12.0-next.16": - version: 3.12.0-next.16 - resolution: "@webex/internal-plugin-locus@npm:3.12.0-next.16" +"@webex/internal-plugin-locus@npm:3.12.0-next.30": + version: 3.12.0-next.30 + resolution: "@webex/internal-plugin-locus@npm:3.12.0-next.30" dependencies: - "@webex/internal-plugin-mercury": "npm:3.12.0-next.16" - "@webex/test-helper-chai": "npm:3.12.0-next.1" - "@webex/test-helper-mock-webex": "npm:3.12.0-next.1" - "@webex/webex-core": "npm:3.12.0-next.15" + "@webex/internal-plugin-mercury": "npm:3.12.0-next.30" + "@webex/test-helper-chai": "npm:3.12.0-next.5" + "@webex/test-helper-mock-webex": "npm:3.12.0-next.5" + "@webex/webex-core": "npm:3.12.0-next.29" lodash: "npm:^4.17.21" uuid: "npm:^3.3.2" - checksum: 10c0/7df66a258562ec27ced651a2be5dc002641414954df727f2d83dbc78e96b2d11bbe9209a0eb801a5bff2950796f100152ee28a60a245dd760b4aef70eadf3b2f + checksum: 10c0/f1520f2c97c90bdbe812de415aae747a7e49e0f985e779694555ff8bf4f4d30cb77ddf158622f9214b2863004b3e010d6f94ebbc03cc8f78c5c3c99b455c809a languageName: node linkType: hard @@ -10810,20 +10610,20 @@ __metadata: languageName: node linkType: hard -"@webex/internal-plugin-lyra@npm:3.12.0-next.17": - version: 3.12.0-next.17 - resolution: "@webex/internal-plugin-lyra@npm:3.12.0-next.17" +"@webex/internal-plugin-lyra@npm:3.12.0-next.31": + version: 3.12.0-next.31 + resolution: "@webex/internal-plugin-lyra@npm:3.12.0-next.31" dependencies: - "@webex/common": "npm:3.12.0-next.1" - "@webex/internal-plugin-conversation": "npm:3.12.0-next.17" - "@webex/internal-plugin-encryption": "npm:3.12.0-next.16" - "@webex/internal-plugin-feature": "npm:3.12.0-next.15" - "@webex/internal-plugin-locus": "npm:3.12.0-next.16" - "@webex/internal-plugin-mercury": "npm:3.12.0-next.16" - "@webex/webex-core": "npm:3.12.0-next.15" + "@webex/common": "npm:3.12.0-next.5" + "@webex/internal-plugin-conversation": "npm:3.12.0-next.31" + "@webex/internal-plugin-encryption": "npm:3.12.0-next.30" + "@webex/internal-plugin-feature": "npm:3.12.0-next.29" + "@webex/internal-plugin-locus": "npm:3.12.0-next.30" + "@webex/internal-plugin-mercury": "npm:3.12.0-next.30" + "@webex/webex-core": "npm:3.12.0-next.29" bowser: "npm:^2.11.0" uuid: "npm:^3.3.2" - checksum: 10c0/e94ed78b8f3bb50bcdbe68e7d4c0374a5ebe8ad19defb9a0cab90ceacc3ee4f6d2607d9ebc1ec751d1a4e4cfb55617973996ebc9e643da71054fe496b3a2bf04 + checksum: 10c0/8a6fdb6d7909806e644016a1f8f09a759d1ee15f34d7d4d8db3dac5708c9ef476d1233a0685859a27c795b3d4e4039342511937ca7347e8b11e72cec782656db languageName: node linkType: hard @@ -10875,51 +10675,27 @@ __metadata: languageName: node linkType: hard -"@webex/internal-plugin-mercury@npm:3.12.0-next.16": - version: 3.12.0-next.16 - resolution: "@webex/internal-plugin-mercury@npm:3.12.0-next.16" - dependencies: - "@webex/common": "npm:3.12.0-next.1" - "@webex/common-timers": "npm:3.12.0-next.1" - "@webex/internal-plugin-device": "npm:3.12.0-next.15" - "@webex/internal-plugin-feature": "npm:3.12.0-next.15" - "@webex/internal-plugin-metrics": "npm:3.12.0-next.15" - "@webex/test-helper-chai": "npm:3.12.0-next.1" - "@webex/test-helper-mocha": "npm:3.12.0-next.1" - "@webex/test-helper-mock-web-socket": "npm:3.12.0-next.1" - "@webex/test-helper-mock-webex": "npm:3.12.0-next.1" - "@webex/test-helper-refresh-callback": "npm:3.12.0-next.1" - "@webex/test-helper-test-users": "npm:3.12.0-next.1" - "@webex/webex-core": "npm:3.12.0-next.15" - backoff: "npm:^2.5.0" - lodash: "npm:^4.17.21" - uuid: "npm:^3.3.2" - ws: "npm:^8.17.1" - checksum: 10c0/0cfe3cb25a5b42513103ff147b850476c6f0d885e317a13dc2fe34f3a1d9e605edbd75b421ea667b1470c414b82998340e37d64bb33176e19844fb1466347ded - languageName: node - linkType: hard - -"@webex/internal-plugin-mercury@npm:3.12.0-next.27": - version: 3.12.0-next.27 - resolution: "@webex/internal-plugin-mercury@npm:3.12.0-next.27" +"@webex/internal-plugin-mercury@npm:3.12.0-next.30": + version: 3.12.0-next.30 + resolution: "@webex/internal-plugin-mercury@npm:3.12.0-next.30" dependencies: "@webex/common": "npm:3.12.0-next.5" "@webex/common-timers": "npm:3.12.0-next.5" - "@webex/internal-plugin-device": "npm:3.12.0-next.26" - "@webex/internal-plugin-feature": "npm:3.12.0-next.26" - "@webex/internal-plugin-metrics": "npm:3.12.0-next.26" + "@webex/internal-plugin-device": "npm:3.12.0-next.29" + "@webex/internal-plugin-feature": "npm:3.12.0-next.29" + "@webex/internal-plugin-metrics": "npm:3.12.0-next.29" "@webex/test-helper-chai": "npm:3.12.0-next.5" "@webex/test-helper-mocha": "npm:3.12.0-next.5" "@webex/test-helper-mock-web-socket": "npm:3.12.0-next.5" "@webex/test-helper-mock-webex": "npm:3.12.0-next.5" "@webex/test-helper-refresh-callback": "npm:3.12.0-next.5" "@webex/test-helper-test-users": "npm:3.12.0-next.5" - "@webex/webex-core": "npm:3.12.0-next.26" + "@webex/webex-core": "npm:3.12.0-next.29" backoff: "npm:^2.5.0" lodash: "npm:^4.17.21" uuid: "npm:^3.3.2" ws: "npm:^8.17.1" - checksum: 10c0/b7ac02db0d20f9945a8d96546c1bcdf508c7c285835fd6ccd543aab17b6704b221bd55084043874ce04bf3a42ee50a6d28547217b9e586e469f346ea2710f510 + checksum: 10c0/ae219d726d34f764c2b74eea51d2ad47f769ac5211e81ce89209cdd9a6bbc2fd0f9e01894591f55b7dad9396ec5a63fb5400e8d489f0ca6f5e843678d7ab806c languageName: node linkType: hard @@ -10952,35 +10728,19 @@ __metadata: languageName: node linkType: hard -"@webex/internal-plugin-metrics@npm:3.12.0-next.15": - version: 3.12.0-next.15 - resolution: "@webex/internal-plugin-metrics@npm:3.12.0-next.15" - dependencies: - "@webex/common": "npm:3.12.0-next.1" - "@webex/common-timers": "npm:3.12.0-next.1" - "@webex/test-helper-chai": "npm:3.12.0-next.1" - "@webex/test-helper-mock-webex": "npm:3.12.0-next.1" - "@webex/webex-core": "npm:3.12.0-next.15" - ip-anonymize: "npm:^0.1.0" - lodash: "npm:^4.17.21" - uuid: "npm:^3.3.2" - checksum: 10c0/d8b1d3f1b1f34589f57e9a30030cd8922a95d009f3e2d0b57dd57ade34c439a371d38bb4c0960ccab65498c55b9b0f3d12451e3b0d66a95a0f59757b23c55a83 - languageName: node - linkType: hard - -"@webex/internal-plugin-metrics@npm:3.12.0-next.26": - version: 3.12.0-next.26 - resolution: "@webex/internal-plugin-metrics@npm:3.12.0-next.26" +"@webex/internal-plugin-metrics@npm:3.12.0-next.29": + version: 3.12.0-next.29 + resolution: "@webex/internal-plugin-metrics@npm:3.12.0-next.29" dependencies: "@webex/common": "npm:3.12.0-next.5" "@webex/common-timers": "npm:3.12.0-next.5" "@webex/test-helper-chai": "npm:3.12.0-next.5" "@webex/test-helper-mock-webex": "npm:3.12.0-next.5" - "@webex/webex-core": "npm:3.12.0-next.26" + "@webex/webex-core": "npm:3.12.0-next.29" ip-anonymize: "npm:^0.1.0" lodash: "npm:^4.17.21" uuid: "npm:^3.3.2" - checksum: 10c0/3cdd157e2281c1816a150fc900bf4cc855fee47b7e10af06797b101e7c213ac7370d620e3a70395c4388089f68114d4f8ee78e4816f0459a91824b77f6bbfc02 + checksum: 10c0/4400e78b017df419a2052862dfdf7a738099cdbe6c1462277353e49526b3c45667f058db6c2d3b8924eb02cdaedec46db52852da695feb171dccdd90a4bdc547 languageName: node linkType: hard @@ -11000,19 +10760,19 @@ __metadata: languageName: node linkType: hard -"@webex/internal-plugin-presence@npm:3.12.0-next.16": - version: 3.12.0-next.16 - resolution: "@webex/internal-plugin-presence@npm:3.12.0-next.16" +"@webex/internal-plugin-presence@npm:3.12.0-next.30": + version: 3.12.0-next.30 + resolution: "@webex/internal-plugin-presence@npm:3.12.0-next.30" dependencies: - "@webex/internal-plugin-device": "npm:3.12.0-next.15" - "@webex/internal-plugin-mercury": "npm:3.12.0-next.16" - "@webex/test-helper-chai": "npm:3.12.0-next.1" - "@webex/test-helper-mocha": "npm:3.12.0-next.1" - "@webex/test-helper-mock-webex": "npm:3.12.0-next.1" - "@webex/test-helper-test-users": "npm:3.12.0-next.1" - "@webex/webex-core": "npm:3.12.0-next.15" + "@webex/internal-plugin-device": "npm:3.12.0-next.29" + "@webex/internal-plugin-mercury": "npm:3.12.0-next.30" + "@webex/test-helper-chai": "npm:3.12.0-next.5" + "@webex/test-helper-mocha": "npm:3.12.0-next.5" + "@webex/test-helper-mock-webex": "npm:3.12.0-next.5" + "@webex/test-helper-test-users": "npm:3.12.0-next.5" + "@webex/webex-core": "npm:3.12.0-next.29" lodash: "npm:^4.17.21" - checksum: 10c0/557276a3f1c64eb4026a9dcba35df70b7ad890109026582297085add6c970ad53d98ca46fee66862850a5f7ac2b2d8368c8aff5ddf0eac52f3efc1ff14198417 + checksum: 10c0/f501404c2ef446ffa80c3c0521c71ed2c166459b74c9c2845b9c762f11eccb13370755cd2f0dc97e4f5f09156676762f453baca2ebc0a2239a99e6625d1e1ff1 languageName: node linkType: hard @@ -11031,33 +10791,18 @@ __metadata: languageName: node linkType: hard -"@webex/internal-plugin-search@npm:3.12.0-next.17": - version: 3.12.0-next.17 - resolution: "@webex/internal-plugin-search@npm:3.12.0-next.17" - dependencies: - "@webex/common": "npm:3.12.0-next.1" - "@webex/internal-plugin-conversation": "npm:3.12.0-next.17" - "@webex/internal-plugin-device": "npm:3.12.0-next.15" - "@webex/internal-plugin-encryption": "npm:3.12.0-next.16" - "@webex/webex-core": "npm:3.12.0-next.15" - lodash: "npm:^4.17.21" - uuid: "npm:^3.3.2" - checksum: 10c0/b5e13de9b8312333a7ff48630ea0bb306da5b2d250626806e08d3cad5a43c20a8a8f119175687d9320a7ea2e8c13c2d8daa8aa8d1cc6e400f5aab89b501f553d - languageName: node - linkType: hard - -"@webex/internal-plugin-search@npm:3.12.0-next.28": - version: 3.12.0-next.28 - resolution: "@webex/internal-plugin-search@npm:3.12.0-next.28" +"@webex/internal-plugin-search@npm:3.12.0-next.31": + version: 3.12.0-next.31 + resolution: "@webex/internal-plugin-search@npm:3.12.0-next.31" dependencies: "@webex/common": "npm:3.12.0-next.5" - "@webex/internal-plugin-conversation": "npm:3.12.0-next.28" - "@webex/internal-plugin-device": "npm:3.12.0-next.26" - "@webex/internal-plugin-encryption": "npm:3.12.0-next.27" - "@webex/webex-core": "npm:3.12.0-next.26" + "@webex/internal-plugin-conversation": "npm:3.12.0-next.31" + "@webex/internal-plugin-device": "npm:3.12.0-next.29" + "@webex/internal-plugin-encryption": "npm:3.12.0-next.30" + "@webex/webex-core": "npm:3.12.0-next.29" lodash: "npm:^4.17.21" uuid: "npm:^3.3.2" - checksum: 10c0/00d1f328ce32c59d358e67ad3c4e23003af077082ba14c34c2ef34dcac1e90ad10a5b6da1d108bd546673d315fd470d96b95ee80600def05328a16284251b446 + checksum: 10c0/fc1ee21ba2b23a47f9a444ff96e87a2658c5b174a19ed0394eb63c077fe5ba339e91e05e05178987a7ffd4f6657d8fec80f24ef7ac5f2252af2b7d7a7ac67724 languageName: node linkType: hard @@ -11078,41 +10823,24 @@ __metadata: languageName: node linkType: hard -"@webex/internal-plugin-support@npm:3.12.0-next.17": - version: 3.12.0-next.17 - resolution: "@webex/internal-plugin-support@npm:3.12.0-next.17" - dependencies: - "@webex/internal-plugin-device": "npm:3.12.0-next.15" - "@webex/internal-plugin-search": "npm:3.12.0-next.17" - "@webex/test-helper-chai": "npm:3.12.0-next.1" - "@webex/test-helper-file": "npm:3.12.0-next.1" - "@webex/test-helper-mock-webex": "npm:3.12.0-next.1" - "@webex/test-helper-test-users": "npm:3.12.0-next.1" - "@webex/webex-core": "npm:3.12.0-next.15" - lodash: "npm:^4.17.21" - uuid: "npm:^3.3.2" - checksum: 10c0/e2d792681c264c646d47187a3a7a8f995fc63b1c724f78762d2e019db8798ae5b7ca6a7a516cf8938e66915f8c969c08a400806ffb51edf787397629bacc3c1c - languageName: node - linkType: hard - -"@webex/internal-plugin-support@npm:3.12.0-next.28": - version: 3.12.0-next.28 - resolution: "@webex/internal-plugin-support@npm:3.12.0-next.28" +"@webex/internal-plugin-support@npm:3.12.0-next.31": + version: 3.12.0-next.31 + resolution: "@webex/internal-plugin-support@npm:3.12.0-next.31" dependencies: - "@webex/internal-plugin-device": "npm:3.12.0-next.26" - "@webex/internal-plugin-search": "npm:3.12.0-next.28" + "@webex/internal-plugin-device": "npm:3.12.0-next.29" + "@webex/internal-plugin-search": "npm:3.12.0-next.31" "@webex/test-helper-chai": "npm:3.12.0-next.5" "@webex/test-helper-file": "npm:3.12.0-next.5" "@webex/test-helper-mock-webex": "npm:3.12.0-next.5" "@webex/test-helper-test-users": "npm:3.12.0-next.5" - "@webex/webex-core": "npm:3.12.0-next.26" + "@webex/webex-core": "npm:3.12.0-next.29" lodash: "npm:^4.17.21" uuid: "npm:^3.3.2" - checksum: 10c0/76bdb9ed2fc7128b75bae31c4fa35496bfe85789d482556e0c77da5500d153b733d08246e16580e752fa6405ab4cf0b0619d59604619c0d62d01fd5d9de14907 + checksum: 10c0/3d0e992c53a4465bebbb363ee23875e9484c64ea789dd6faa048f0addd687fd4fc84a96ceb4b38604278a26f1054872a5543386d8ab48121b66129a6f1fdb93a languageName: node linkType: hard -"@webex/internal-plugin-task@npm:^3.12.0-next.17": +"@webex/internal-plugin-task@npm:^3.12.0-next.31": version: 3.12.0 resolution: "@webex/internal-plugin-task@npm:3.12.0" dependencies: @@ -11158,47 +10886,31 @@ __metadata: languageName: node linkType: hard -"@webex/internal-plugin-user@npm:3.12.0-next.16": - version: 3.12.0-next.16 - resolution: "@webex/internal-plugin-user@npm:3.12.0-next.16" - dependencies: - "@webex/common": "npm:3.12.0-next.1" - "@webex/internal-plugin-device": "npm:3.12.0-next.15" - "@webex/test-helper-chai": "npm:3.12.0-next.1" - "@webex/test-helper-mock-webex": "npm:3.12.0-next.1" - "@webex/test-helper-test-users": "npm:3.12.0-next.1" - "@webex/webex-core": "npm:3.12.0-next.15" - lodash: "npm:^4.17.21" - uuid: "npm:^3.3.2" - checksum: 10c0/5f625971f05369f2ae1b22570383f5e6739291c66d1ac0dd44b7d2dfab19c844c0daafd6d5faa1afea3ef62656a27091a89604ab5e53479a9bc1663e5cc735ca - languageName: node - linkType: hard - -"@webex/internal-plugin-user@npm:3.12.0-next.27": - version: 3.12.0-next.27 - resolution: "@webex/internal-plugin-user@npm:3.12.0-next.27" +"@webex/internal-plugin-user@npm:3.12.0-next.30": + version: 3.12.0-next.30 + resolution: "@webex/internal-plugin-user@npm:3.12.0-next.30" dependencies: "@webex/common": "npm:3.12.0-next.5" - "@webex/internal-plugin-device": "npm:3.12.0-next.26" + "@webex/internal-plugin-device": "npm:3.12.0-next.29" "@webex/test-helper-chai": "npm:3.12.0-next.5" "@webex/test-helper-mock-webex": "npm:3.12.0-next.5" "@webex/test-helper-test-users": "npm:3.12.0-next.5" - "@webex/webex-core": "npm:3.12.0-next.26" + "@webex/webex-core": "npm:3.12.0-next.29" lodash: "npm:^4.17.21" uuid: "npm:^3.3.2" - checksum: 10c0/78a53f8dc41d16051410c205136d9088aa8eb88fbfce7356d2fc132692d216e51d3a88d345bfea088fc12c82fef885402b52356ade62d48538ccbb6bd28de501 + checksum: 10c0/2a9ac1306f685bc03becfb32e0bb8a4e749e131cb9843cc665c5fd12b4e424e05f5e8ab9464f3d167007f95f3f5a52be4ec005fd28cfb5f06876407ac63b441f languageName: node linkType: hard -"@webex/internal-plugin-voicea@npm:3.12.0-next.18": - version: 3.12.0-next.18 - resolution: "@webex/internal-plugin-voicea@npm:3.12.0-next.18" +"@webex/internal-plugin-voicea@npm:3.12.0-next.33": + version: 3.12.0-next.33 + resolution: "@webex/internal-plugin-voicea@npm:3.12.0-next.33" dependencies: - "@webex/internal-plugin-llm": "npm:3.12.0-next.18" - "@webex/internal-plugin-mercury": "npm:3.12.0-next.16" - "@webex/webex-core": "npm:3.12.0-next.15" + "@webex/internal-plugin-llm": "npm:3.12.0-next.33" + "@webex/internal-plugin-mercury": "npm:3.12.0-next.30" + "@webex/webex-core": "npm:3.12.0-next.29" uuid: "npm:^3.3.2" - checksum: 10c0/a95e8af210eb469d6501256216748ce7e066f567d02c3fab17430d825dd422068cc536ec88e9a06feac01d03b81aa6c5d799091cafb83ab54dc53398ac62903a + checksum: 10c0/aba67fc9bc3e0a958cdfefc73362901da2194441efddbc17032790e5de41ab51cc33785911746dab06c89380ce996156b60fbd0a053187a24615206b12a2ea96 languageName: node linkType: hard @@ -11222,25 +10934,14 @@ __metadata: languageName: node linkType: hard -"@webex/media-helpers@npm:3.12.0-next.4": - version: 3.12.0-next.4 - resolution: "@webex/media-helpers@npm:3.12.0-next.4" +"@webex/media-helpers@npm:3.12.0-next.10": + version: 3.12.0-next.10 + resolution: "@webex/media-helpers@npm:3.12.0-next.10" dependencies: - "@webex/internal-media-core": "npm:2.25.1" + "@webex/internal-media-core": "npm:2.26.2" "@webex/ts-events": "npm:^1.1.0" "@webex/web-media-effects": "npm:2.33.5" - checksum: 10c0/9ac15b19c7c450f4831b4d6afafc7873c86781670ea0fcd468f25b193f50d13d2c0b13a70b927a94b4d372ebe03f2ecad141df32cb0836dbcdb604c7de9f9799 - languageName: node - linkType: hard - -"@webex/media-helpers@npm:3.12.0-next.9": - version: 3.12.0-next.9 - resolution: "@webex/media-helpers@npm:3.12.0-next.9" - dependencies: - "@webex/internal-media-core": "npm:2.26.1" - "@webex/ts-events": "npm:^1.1.0" - "@webex/web-media-effects": "npm:2.33.5" - checksum: 10c0/5bc3121e4fbdf280938daf7c0734559f1374d09782be0488b37933afd27e6e7dffd53ce35be71df2f62d4501d176388963820b646057e220625a5fffdb3aeb0c + checksum: 10c0/75860e5409cfbfa0bcd5238c4fbdc1d8eecc793966156c81232bcb97d3afdc59606e61ae258da309c66d48845df90eae76fec306ee03d4e63ecf2b4dd8bc033f languageName: node linkType: hard @@ -11274,21 +10975,21 @@ __metadata: languageName: node linkType: hard -"@webex/plugin-attachment-actions@npm:3.12.0-next.17": - version: 3.12.0-next.17 - resolution: "@webex/plugin-attachment-actions@npm:3.12.0-next.17" +"@webex/plugin-attachment-actions@npm:3.12.0-next.31": + version: 3.12.0-next.31 + resolution: "@webex/plugin-attachment-actions@npm:3.12.0-next.31" dependencies: - "@webex/common": "npm:3.12.0-next.1" - "@webex/internal-plugin-conversation": "npm:3.12.0-next.17" - "@webex/internal-plugin-mercury": "npm:3.12.0-next.16" - "@webex/webex-core": "npm:3.12.0-next.15" + "@webex/common": "npm:3.12.0-next.5" + "@webex/internal-plugin-conversation": "npm:3.12.0-next.31" + "@webex/internal-plugin-mercury": "npm:3.12.0-next.30" + "@webex/webex-core": "npm:3.12.0-next.29" debug: "npm:^4.3.4" lodash: "npm:^4.17.21" peerDependencies: - "@webex/plugin-logger": 3.12.0-next.15 - "@webex/plugin-messages": 3.12.0-next.17 - "@webex/plugin-people": 3.12.0-next.16 - checksum: 10c0/2e615f347362c443c4931a0bfbed9c551df76319755abecde51b78ed53b269cec337325ea9e13ffe260a58bb165817d63e5ac56d0cb5b70bfdb7d041979f87bb + "@webex/plugin-logger": 3.12.0-next.29 + "@webex/plugin-messages": 3.12.0-next.31 + "@webex/plugin-people": 3.12.0-next.30 + checksum: 10c0/45e8929a2e4081be77b8ff547e2a0fde6469486721387226f72dccdc463c59926d0f7a9cb3af00ba27b2fc2dddfd930a79b8f8d20cd413bd3b8b80df283a623d languageName: node linkType: hard @@ -11309,37 +11010,20 @@ __metadata: languageName: node linkType: hard -"@webex/plugin-authorization-browser@npm:3.12.0-next.15": - version: 3.12.0-next.15 - resolution: "@webex/plugin-authorization-browser@npm:3.12.0-next.15" - dependencies: - "@webex/common": "npm:3.12.0-next.1" - "@webex/internal-plugin-device": "npm:3.12.0-next.15" - "@webex/plugin-authorization-node": "npm:3.12.0-next.15" - "@webex/storage-adapter-local-storage": "npm:3.12.0-next.15" - "@webex/storage-adapter-spec": "npm:3.12.0-next.1" - "@webex/webex-core": "npm:3.12.0-next.15" - jsonwebtoken: "npm:^9.0.2" - lodash: "npm:^4.17.21" - uuid: "npm:^3.3.2" - checksum: 10c0/04f15ad355e5431dcad6a545f2b1e5c6e99e734743a310107996319cbcd080f6a5d0e115e297fdf414a3aa6c421bba7f786abfbee1fd7aec60d0333796712313 - languageName: node - linkType: hard - -"@webex/plugin-authorization-browser@npm:3.12.0-next.26": - version: 3.12.0-next.26 - resolution: "@webex/plugin-authorization-browser@npm:3.12.0-next.26" +"@webex/plugin-authorization-browser@npm:3.12.0-next.29": + version: 3.12.0-next.29 + resolution: "@webex/plugin-authorization-browser@npm:3.12.0-next.29" dependencies: "@webex/common": "npm:3.12.0-next.5" - "@webex/internal-plugin-device": "npm:3.12.0-next.26" - "@webex/plugin-authorization-node": "npm:3.12.0-next.26" - "@webex/storage-adapter-local-storage": "npm:3.12.0-next.26" + "@webex/internal-plugin-device": "npm:3.12.0-next.29" + "@webex/plugin-authorization-node": "npm:3.12.0-next.29" + "@webex/storage-adapter-local-storage": "npm:3.12.0-next.29" "@webex/storage-adapter-spec": "npm:3.12.0-next.5" - "@webex/webex-core": "npm:3.12.0-next.26" + "@webex/webex-core": "npm:3.12.0-next.29" jsonwebtoken: "npm:^9.0.2" lodash: "npm:^4.17.21" uuid: "npm:^3.3.2" - checksum: 10c0/b8b6d6de65b236852b67511f367992f09fb28d9021d1f9502b16d0daf75bbc17774268860711b63796f1d4309840c758c036271b68b752f5e455daf0d964e26c + checksum: 10c0/6a51481a9881ae9760e9a2a2b845913765c5b72b896539b8acf1ffb22d48d60508ee39f0a8ea17b8efcd52b138ed68e00144f8783a03e04cac091089718b15b9 languageName: node linkType: hard @@ -11356,29 +11040,16 @@ __metadata: languageName: node linkType: hard -"@webex/plugin-authorization-node@npm:3.12.0-next.15": - version: 3.12.0-next.15 - resolution: "@webex/plugin-authorization-node@npm:3.12.0-next.15" - dependencies: - "@webex/common": "npm:3.12.0-next.1" - "@webex/internal-plugin-device": "npm:3.12.0-next.15" - "@webex/webex-core": "npm:3.12.0-next.15" - jsonwebtoken: "npm:^9.0.0" - uuid: "npm:^3.3.2" - checksum: 10c0/8bca668f4e7a17504674a02291ea699df1b350deea8cb9ab1c411e484e17ab765274227f5d1d4a9f4b2e3a4e7223739e38ec1b551780758e12900687960b87f3 - languageName: node - linkType: hard - -"@webex/plugin-authorization-node@npm:3.12.0-next.26": - version: 3.12.0-next.26 - resolution: "@webex/plugin-authorization-node@npm:3.12.0-next.26" +"@webex/plugin-authorization-node@npm:3.12.0-next.29": + version: 3.12.0-next.29 + resolution: "@webex/plugin-authorization-node@npm:3.12.0-next.29" dependencies: "@webex/common": "npm:3.12.0-next.5" - "@webex/internal-plugin-device": "npm:3.12.0-next.26" - "@webex/webex-core": "npm:3.12.0-next.26" + "@webex/internal-plugin-device": "npm:3.12.0-next.29" + "@webex/webex-core": "npm:3.12.0-next.29" jsonwebtoken: "npm:^9.0.0" uuid: "npm:^3.3.2" - checksum: 10c0/2bfd2d33de4d6f913f7c6c949a7649db05cd36441f7ed80d991ac6041fd2a57de209e8bc2a259a701be00e7fdf98facae0076e4376c84866665a1fcc0b581355 + checksum: 10c0/06b96949a8d231416519f4fee520cddac0a7c61f9ac3193a17f4e7763a24d7da19ba35c25681d3328b41aa78a56009ab84e03c0ec95e1932bd4e1cc97a4112af languageName: node linkType: hard @@ -11392,23 +11063,13 @@ __metadata: languageName: node linkType: hard -"@webex/plugin-authorization@npm:3.12.0-next.15": - version: 3.12.0-next.15 - resolution: "@webex/plugin-authorization@npm:3.12.0-next.15" +"@webex/plugin-authorization@npm:3.12.0-next.29": + version: 3.12.0-next.29 + resolution: "@webex/plugin-authorization@npm:3.12.0-next.29" dependencies: - "@webex/plugin-authorization-browser": "npm:3.12.0-next.15" - "@webex/plugin-authorization-node": "npm:3.12.0-next.15" - checksum: 10c0/97f50fb9b111628ea46be900e6f69ca61a7220c9dcc1b66a705f452178ecb9622ebdcb6f442139da09141fc34111b503035e38ce005d5ca5adff3cb86766148c - languageName: node - linkType: hard - -"@webex/plugin-authorization@npm:3.12.0-next.26": - version: 3.12.0-next.26 - resolution: "@webex/plugin-authorization@npm:3.12.0-next.26" - dependencies: - "@webex/plugin-authorization-browser": "npm:3.12.0-next.26" - "@webex/plugin-authorization-node": "npm:3.12.0-next.26" - checksum: 10c0/4238c60cb9ac2b805a04f4eb03b8d4fac3a63c812a6e273a5b6f24fbf505bf29171afef3404d0370ea0ef6f3726f03e93a28c27d416b8779fde58156706f7a76 + "@webex/plugin-authorization-browser": "npm:3.12.0-next.29" + "@webex/plugin-authorization-node": "npm:3.12.0-next.29" + checksum: 10c0/b5a090e95d79e29c2d706d5d00b15bf773eaa6ae4ef00f8a7437d3c8190abd2dbc5490a299df6bd83e369e8bdec6603022ac543439900739c9953bd1eabd840f languageName: node linkType: hard @@ -11430,31 +11091,31 @@ __metadata: languageName: node linkType: hard -"@webex/plugin-device-manager@npm:3.12.0-next.17": - version: 3.12.0-next.17 - resolution: "@webex/plugin-device-manager@npm:3.12.0-next.17" +"@webex/plugin-device-manager@npm:3.12.0-next.31": + version: 3.12.0-next.31 + resolution: "@webex/plugin-device-manager@npm:3.12.0-next.31" dependencies: - "@webex/internal-plugin-calendar": "npm:3.12.0-next.17" - "@webex/internal-plugin-device": "npm:3.12.0-next.15" - "@webex/internal-plugin-lyra": "npm:3.12.0-next.17" - "@webex/internal-plugin-search": "npm:3.12.0-next.17" - "@webex/plugin-authorization": "npm:3.12.0-next.15" - "@webex/plugin-logger": "npm:3.12.0-next.15" - "@webex/test-helper-chai": "npm:3.12.0-next.1" - "@webex/webex-core": "npm:3.12.0-next.15" + "@webex/internal-plugin-calendar": "npm:3.12.0-next.31" + "@webex/internal-plugin-device": "npm:3.12.0-next.29" + "@webex/internal-plugin-lyra": "npm:3.12.0-next.31" + "@webex/internal-plugin-search": "npm:3.12.0-next.31" + "@webex/plugin-authorization": "npm:3.12.0-next.29" + "@webex/plugin-logger": "npm:3.12.0-next.29" + "@webex/test-helper-chai": "npm:3.12.0-next.5" + "@webex/webex-core": "npm:3.12.0-next.29" lodash: "npm:^4.17.21" uuid: "npm:^3.3.2" - checksum: 10c0/4aaa87651b9666ab9b6824938ddf2f2c00839aff0b5c6a9e89699f75b587107e0eaa6852fe9dd2bdb99b8f5e1c411dc5c6022ce56c8b8bb9a7a2ef7544182cf2 + checksum: 10c0/32d72c835ea9d3b04fdb09d248ae1c01b1658568a1db0ad76629390e8f1a352a135a0631dc4e86ecd7f654ae8866a6f4ec58178519f02bf82dbf87a59bc26419 languageName: node linkType: hard -"@webex/plugin-encryption@npm:3.12.0-next.16": - version: 3.12.0-next.16 - resolution: "@webex/plugin-encryption@npm:3.12.0-next.16" +"@webex/plugin-encryption@npm:3.12.0-next.30": + version: 3.12.0-next.30 + resolution: "@webex/plugin-encryption@npm:3.12.0-next.30" dependencies: - "@webex/internal-plugin-encryption": "npm:3.12.0-next.16" - "@webex/webex-core": "npm:3.12.0-next.15" - checksum: 10c0/36e987ef8439913fdead76cdfec4184f1d39bb96a38bf3e2eacde4162a3e170d92593ba3649b08488e99244c9cec037c56e2ee0ce7d72fa2b9b264eb6679b1d8 + "@webex/internal-plugin-encryption": "npm:3.12.0-next.30" + "@webex/webex-core": "npm:3.12.0-next.29" + checksum: 10c0/50d25e97261b71f47f773da6a240f8880471eb1cb999bea1ec7f29a757018f4159e623b13d4de001ac348dd03a97f840667d30ca9e932a985c815410a03e1bb4 languageName: node linkType: hard @@ -11472,31 +11133,17 @@ __metadata: languageName: node linkType: hard -"@webex/plugin-logger@npm:3.12.0-next.15": - version: 3.12.0-next.15 - resolution: "@webex/plugin-logger@npm:3.12.0-next.15" - dependencies: - "@webex/common": "npm:3.12.0-next.1" - "@webex/test-helper-chai": "npm:3.12.0-next.1" - "@webex/test-helper-mocha": "npm:3.12.0-next.1" - "@webex/test-helper-mock-webex": "npm:3.12.0-next.1" - "@webex/webex-core": "npm:3.12.0-next.15" - lodash: "npm:^4.17.21" - checksum: 10c0/3db9830fb48a66caa3ed0c9a2a3548f6bf9c3b9b07cfd0eeda0cb090c71db42399bbbfc5747848a7069a1d75cf34f072a08adf277caa76e50fc490afeeb833af - languageName: node - linkType: hard - -"@webex/plugin-logger@npm:3.12.0-next.26": - version: 3.12.0-next.26 - resolution: "@webex/plugin-logger@npm:3.12.0-next.26" +"@webex/plugin-logger@npm:3.12.0-next.29": + version: 3.12.0-next.29 + resolution: "@webex/plugin-logger@npm:3.12.0-next.29" dependencies: "@webex/common": "npm:3.12.0-next.5" "@webex/test-helper-chai": "npm:3.12.0-next.5" "@webex/test-helper-mocha": "npm:3.12.0-next.5" "@webex/test-helper-mock-webex": "npm:3.12.0-next.5" - "@webex/webex-core": "npm:3.12.0-next.26" + "@webex/webex-core": "npm:3.12.0-next.29" lodash: "npm:^4.17.21" - checksum: 10c0/42d73401f35ac2c2ee40bd6e44c8a386c423c1220b4ec9ebf0ee557cca1164ae432077dc72acc4270997bc29e875f75f63b8ad804789510c27f37d9749bb51dd + checksum: 10c0/5ed3fe5bda657d5b6db16de9c1abd86970ee7eab730a4de63f251d7c5280931ee72c5aedfc8f2e74294e1389af29048a9fa3227e98d7f746281be0aa7ed2e199 languageName: node linkType: hard @@ -11527,26 +11174,26 @@ __metadata: languageName: node linkType: hard -"@webex/plugin-meetings@npm:3.12.0-next.62": - version: 3.12.0-next.62 - resolution: "@webex/plugin-meetings@npm:3.12.0-next.62" - dependencies: - "@webex/common": "npm:3.12.0-next.1" - "@webex/internal-media-core": "npm:2.25.1" - "@webex/internal-plugin-conversation": "npm:3.12.0-next.17" - "@webex/internal-plugin-device": "npm:3.12.0-next.15" - "@webex/internal-plugin-llm": "npm:3.12.0-next.18" - "@webex/internal-plugin-mercury": "npm:3.12.0-next.16" - "@webex/internal-plugin-metrics": "npm:3.12.0-next.15" - "@webex/internal-plugin-support": "npm:3.12.0-next.17" - "@webex/internal-plugin-user": "npm:3.12.0-next.16" - "@webex/internal-plugin-voicea": "npm:3.12.0-next.18" - "@webex/media-helpers": "npm:3.12.0-next.4" - "@webex/plugin-people": "npm:3.12.0-next.16" - "@webex/plugin-rooms": "npm:3.12.0-next.17" +"@webex/plugin-meetings@npm:3.12.0-next.93": + version: 3.12.0-next.93 + resolution: "@webex/plugin-meetings@npm:3.12.0-next.93" + dependencies: + "@webex/common": "npm:3.12.0-next.5" + "@webex/internal-media-core": "npm:2.26.2" + "@webex/internal-plugin-conversation": "npm:3.12.0-next.31" + "@webex/internal-plugin-device": "npm:3.12.0-next.29" + "@webex/internal-plugin-llm": "npm:3.12.0-next.33" + "@webex/internal-plugin-mercury": "npm:3.12.0-next.30" + "@webex/internal-plugin-metrics": "npm:3.12.0-next.29" + "@webex/internal-plugin-support": "npm:3.12.0-next.31" + "@webex/internal-plugin-user": "npm:3.12.0-next.30" + "@webex/internal-plugin-voicea": "npm:3.12.0-next.33" + "@webex/media-helpers": "npm:3.12.0-next.10" + "@webex/plugin-people": "npm:3.12.0-next.30" + "@webex/plugin-rooms": "npm:3.12.0-next.31" "@webex/ts-sdp": "npm:^1.8.1" "@webex/web-capabilities": "npm:^1.10.0" - "@webex/webex-core": "npm:3.12.0-next.15" + "@webex/webex-core": "npm:3.12.0-next.29" ampersand-collection: "npm:^2.0.2" bowser: "npm:^2.11.0" btoa: "npm:^1.2.1" @@ -11560,7 +11207,7 @@ __metadata: uuid: "npm:^3.3.2" webrtc-adapter: "npm:^8.1.2" xxh3-ts: "npm:^2.0.1" - checksum: 10c0/65e30bde464ac5e2da09a6485d663323daee2ec26c528d761dd80b51566c277c338c80f949c28555e7329a5107534250692068c08ce546a5c4cfe15ff47e5ea4 + checksum: 10c0/b4d423b8c6055bd71045cc2071f9d80dd00c72ed27ba602faa5cd523943464332517e663192e4caa037532ec1b5f1d27e89e7c3d57ef55521ae5ab99529ea9ea languageName: node linkType: hard @@ -11582,22 +11229,22 @@ __metadata: languageName: node linkType: hard -"@webex/plugin-memberships@npm:3.12.0-next.17": - version: 3.12.0-next.17 - resolution: "@webex/plugin-memberships@npm:3.12.0-next.17" +"@webex/plugin-memberships@npm:3.12.0-next.31": + version: 3.12.0-next.31 + resolution: "@webex/plugin-memberships@npm:3.12.0-next.31" dependencies: - "@webex/common": "npm:3.12.0-next.1" - "@webex/internal-plugin-conversation": "npm:3.12.0-next.17" - "@webex/internal-plugin-mercury": "npm:3.12.0-next.16" - "@webex/webex-core": "npm:3.12.0-next.15" + "@webex/common": "npm:3.12.0-next.5" + "@webex/internal-plugin-conversation": "npm:3.12.0-next.31" + "@webex/internal-plugin-mercury": "npm:3.12.0-next.30" + "@webex/webex-core": "npm:3.12.0-next.29" debug: "npm:^4.3.4" lodash: "npm:^4.17.21" peerDependencies: - "@webex/plugin-logger": 3.12.0-next.15 - "@webex/plugin-messages": 3.12.0-next.17 - "@webex/plugin-people": 3.12.0-next.16 - "@webex/plugin-rooms": 3.12.0-next.17 - checksum: 10c0/617e1eae28b9de947cd6ea33a779e88a1442b9a0de44290f28c9aae8a0b907bbbf14fe071cf373dce4dcb4f6d1368450269da2617a2b6ab330b240ac3b5ecbf6 + "@webex/plugin-logger": 3.12.0-next.29 + "@webex/plugin-messages": 3.12.0-next.31 + "@webex/plugin-people": 3.12.0-next.30 + "@webex/plugin-rooms": 3.12.0-next.31 + checksum: 10c0/6affaea58af92c7497befa5b2879ce18ce662c6dca65f53f6101ed2154e469d1991e41524c7aeeb5251792676520861a434ded7d50fcfb106ba71bdd74207e09 languageName: node linkType: hard @@ -11619,22 +11266,22 @@ __metadata: languageName: node linkType: hard -"@webex/plugin-messages@npm:3.12.0-next.17": - version: 3.12.0-next.17 - resolution: "@webex/plugin-messages@npm:3.12.0-next.17" +"@webex/plugin-messages@npm:3.12.0-next.31": + version: 3.12.0-next.31 + resolution: "@webex/plugin-messages@npm:3.12.0-next.31" dependencies: - "@webex/common": "npm:3.12.0-next.1" - "@webex/internal-plugin-conversation": "npm:3.12.0-next.17" - "@webex/internal-plugin-device": "npm:3.12.0-next.15" - "@webex/internal-plugin-mercury": "npm:3.12.0-next.16" - "@webex/webex-core": "npm:3.12.0-next.15" + "@webex/common": "npm:3.12.0-next.5" + "@webex/internal-plugin-conversation": "npm:3.12.0-next.31" + "@webex/internal-plugin-device": "npm:3.12.0-next.29" + "@webex/internal-plugin-mercury": "npm:3.12.0-next.30" + "@webex/webex-core": "npm:3.12.0-next.29" debug: "npm:^4.3.4" lodash: "npm:^4.17.21" peerDependencies: - "@webex/plugin-logger": 3.12.0-next.15 - "@webex/plugin-people": 3.12.0-next.16 - "@webex/plugin-rooms": 3.12.0-next.17 - checksum: 10c0/74015a95b1d35d9c360a16cffa712fcd5261405b7b5c4955ad1d2fd4b9bff0a5d196f5ea343edeb1aeb0a98042afc3f414911eb0dd84dc7db67077fb70fb724c + "@webex/plugin-logger": 3.12.0-next.29 + "@webex/plugin-people": 3.12.0-next.30 + "@webex/plugin-rooms": 3.12.0-next.31 + checksum: 10c0/b5aaf24c5abce295b8b35748cf6e6e19fbe4618e98ce8158f7cf0e20529a7b773ec4bdbc8f3fed5f5ed3b53b214d1bdae3dea79cc51b2f2c39744d3500a0d2a6 languageName: node linkType: hard @@ -11649,14 +11296,14 @@ __metadata: languageName: node linkType: hard -"@webex/plugin-people@npm:3.12.0-next.16": - version: 3.12.0-next.16 - resolution: "@webex/plugin-people@npm:3.12.0-next.16" +"@webex/plugin-people@npm:3.12.0-next.30": + version: 3.12.0-next.30 + resolution: "@webex/plugin-people@npm:3.12.0-next.30" dependencies: - "@webex/common": "npm:3.12.0-next.1" - "@webex/internal-plugin-mercury": "npm:3.12.0-next.16" - "@webex/webex-core": "npm:3.12.0-next.15" - checksum: 10c0/328aedf95ae0cd8ebbd91b8424f0279551b6a66fdb8fde83d8bf4e130ecdc5e49aeb516e9d6a365c6b3a67e3e916b3352ff33ab5e1ed1c7622f4842508d055d4 + "@webex/common": "npm:3.12.0-next.5" + "@webex/internal-plugin-mercury": "npm:3.12.0-next.30" + "@webex/webex-core": "npm:3.12.0-next.29" + checksum: 10c0/590ceaa4185da7eb89b2fb5f631c1ea1bcd5ad8dc72cd5975842298ee95bd7f30237e8cb6fd3e534d4dcfba79d973ed67c4180e6cc220a7650c5e0f2e14f7ca2 languageName: node linkType: hard @@ -11678,20 +11325,20 @@ __metadata: languageName: node linkType: hard -"@webex/plugin-rooms@npm:3.12.0-next.17": - version: 3.12.0-next.17 - resolution: "@webex/plugin-rooms@npm:3.12.0-next.17" +"@webex/plugin-rooms@npm:3.12.0-next.31": + version: 3.12.0-next.31 + resolution: "@webex/plugin-rooms@npm:3.12.0-next.31" dependencies: - "@webex/common": "npm:3.12.0-next.1" - "@webex/internal-plugin-conversation": "npm:3.12.0-next.17" - "@webex/internal-plugin-mercury": "npm:3.12.0-next.16" - "@webex/webex-core": "npm:3.12.0-next.15" + "@webex/common": "npm:3.12.0-next.5" + "@webex/internal-plugin-conversation": "npm:3.12.0-next.31" + "@webex/internal-plugin-mercury": "npm:3.12.0-next.30" + "@webex/webex-core": "npm:3.12.0-next.29" debug: "npm:^4.3.4" lodash: "npm:^4.17.21" peerDependencies: - "@webex/plugin-logger": 3.12.0-next.15 - "@webex/plugin-people": 3.12.0-next.16 - checksum: 10c0/384483ea056ff2d2f7470521fc32743cb106264f0f830b3f79edce5f01dc7d47244e71189296ad2d15c76303e2b9428488f55ac0065a28a525aa02f5b3ad0525 + "@webex/plugin-logger": 3.12.0-next.29 + "@webex/plugin-people": 3.12.0-next.30 + checksum: 10c0/f4ce630d3d9108e974b75d5f3b6c87a5de3760811e354a24fd9d6f728704cc7666f71006fcbb745b4408caa1c399989de5efdf793fb3c39f693627cdfe0681cc languageName: node linkType: hard @@ -11708,17 +11355,17 @@ __metadata: languageName: node linkType: hard -"@webex/plugin-team-memberships@npm:3.12.0-next.15": - version: 3.12.0-next.15 - resolution: "@webex/plugin-team-memberships@npm:3.12.0-next.15" +"@webex/plugin-team-memberships@npm:3.12.0-next.29": + version: 3.12.0-next.29 + resolution: "@webex/plugin-team-memberships@npm:3.12.0-next.29" dependencies: - "@webex/internal-plugin-device": "npm:3.12.0-next.15" - "@webex/webex-core": "npm:3.12.0-next.15" + "@webex/internal-plugin-device": "npm:3.12.0-next.29" + "@webex/webex-core": "npm:3.12.0-next.29" peerDependencies: - "@webex/plugin-logger": 3.12.0-next.15 - "@webex/plugin-rooms": 3.12.0-next.17 - "@webex/plugin-teams": 3.12.0-next.15 - checksum: 10c0/dd6c8720f9daabad5e455b2782012559f605733e578e94b5d923d63380bc0ec6403d20c80c8ef05ba8fdd16937b76318c9a83c9dc7084cded0021985b109e603 + "@webex/plugin-logger": 3.12.0-next.29 + "@webex/plugin-rooms": 3.12.0-next.31 + "@webex/plugin-teams": 3.12.0-next.29 + checksum: 10c0/477fd5edae889fc1070e89a1a54d20b096848d3f1f550451a0f2e357dba128452d77a14b2de60271a278caa9cb91d0d0a14f51633b2561fb759a525633bcd39d languageName: node linkType: hard @@ -11738,18 +11385,18 @@ __metadata: languageName: node linkType: hard -"@webex/plugin-teams@npm:3.12.0-next.15": - version: 3.12.0-next.15 - resolution: "@webex/plugin-teams@npm:3.12.0-next.15" +"@webex/plugin-teams@npm:3.12.0-next.29": + version: 3.12.0-next.29 + resolution: "@webex/plugin-teams@npm:3.12.0-next.29" dependencies: - "@webex/internal-plugin-device": "npm:3.12.0-next.15" - "@webex/webex-core": "npm:3.12.0-next.15" + "@webex/internal-plugin-device": "npm:3.12.0-next.29" + "@webex/webex-core": "npm:3.12.0-next.29" lodash: "npm:^4.17.21" peerDependencies: - "@webex/plugin-logger": 3.12.0-next.15 - "@webex/plugin-memberships": 3.12.0-next.17 - "@webex/plugin-rooms": 3.12.0-next.17 - checksum: 10c0/b37c81368bdc3079da07c8bfc0f6e9c5818b34fab02a05003cc5ede264e82a951e4d343cb2317d087b64cf58ec4dff4030d86b56c2b76c1b62e28078f06eda5a + "@webex/plugin-logger": 3.12.0-next.29 + "@webex/plugin-memberships": 3.12.0-next.31 + "@webex/plugin-rooms": 3.12.0-next.31 + checksum: 10c0/3b9d08cbe521dffac19a303913ae8fe99456851c90a6e9f0be041fd07a0b91028388b01591f13508d7450e9302fa8df7669fa9d7dee71cfca7fedfb7c3b1f5e8 languageName: node linkType: hard @@ -11765,16 +11412,16 @@ __metadata: languageName: node linkType: hard -"@webex/plugin-webhooks@npm:3.12.0-next.15": - version: 3.12.0-next.15 - resolution: "@webex/plugin-webhooks@npm:3.12.0-next.15" +"@webex/plugin-webhooks@npm:3.12.0-next.29": + version: 3.12.0-next.29 + resolution: "@webex/plugin-webhooks@npm:3.12.0-next.29" dependencies: - "@webex/internal-plugin-device": "npm:3.12.0-next.15" - "@webex/webex-core": "npm:3.12.0-next.15" + "@webex/internal-plugin-device": "npm:3.12.0-next.29" + "@webex/webex-core": "npm:3.12.0-next.29" peerDependencies: - "@webex/plugin-logger": 3.12.0-next.15 - "@webex/plugin-rooms": 3.12.0-next.17 - checksum: 10c0/dd50f24056870b667686e96d7e578170eae030bd7cff69ca5c3ab3aaa5d54fd7e51d0c60903506b6f6980c7cfad720188a98fd6b6cb5459b46f359b11111e810 + "@webex/plugin-logger": 3.12.0-next.29 + "@webex/plugin-rooms": 3.12.0-next.31 + checksum: 10c0/2daed1db75aec82a23442305d60adcf424210aaf32ddfbaca00e590703fc61d651fb9c2255327133af08e59e6f0125ce05d516513efb4fe51f9df8bfe0817487 languageName: node linkType: hard @@ -11814,25 +11461,14 @@ __metadata: languageName: node linkType: hard -"@webex/storage-adapter-local-storage@npm:3.12.0-next.15": - version: 3.12.0-next.15 - resolution: "@webex/storage-adapter-local-storage@npm:3.12.0-next.15" - dependencies: - "@webex/storage-adapter-spec": "npm:3.12.0-next.1" - "@webex/test-helper-mocha": "npm:3.12.0-next.1" - "@webex/webex-core": "npm:3.12.0-next.15" - checksum: 10c0/211a82fcf5d06d2eb7c47c08538e4d23334354767d442b03219382aaef0d8537186850345d1b128b1f8014df82c7e06eee3bea88edeea97125fa113f83c4c5e2 - languageName: node - linkType: hard - -"@webex/storage-adapter-local-storage@npm:3.12.0-next.26": - version: 3.12.0-next.26 - resolution: "@webex/storage-adapter-local-storage@npm:3.12.0-next.26" +"@webex/storage-adapter-local-storage@npm:3.12.0-next.29": + version: 3.12.0-next.29 + resolution: "@webex/storage-adapter-local-storage@npm:3.12.0-next.29" dependencies: "@webex/storage-adapter-spec": "npm:3.12.0-next.5" "@webex/test-helper-mocha": "npm:3.12.0-next.5" - "@webex/webex-core": "npm:3.12.0-next.26" - checksum: 10c0/37fe28086c11241b432596d21477322ae4ce2ee894b64be5024a9c9c98f20e06a0ea6d5abfd6eb6dadcdf70e941fc8d5059d1b9fa3ac58f39875585590c80abf + "@webex/webex-core": "npm:3.12.0-next.29" + checksum: 10c0/fc71bf834baeda8ca205fc104c184e0f1111ab5437e780b644d1bada771f57f151574a81ada2ccb0f3cd14e95bde03c36f96abf51652294de7b623f8ad47a582 languageName: node linkType: hard @@ -11854,15 +11490,6 @@ __metadata: languageName: node linkType: hard -"@webex/storage-adapter-spec@npm:3.12.0-next.1": - version: 3.12.0-next.1 - resolution: "@webex/storage-adapter-spec@npm:3.12.0-next.1" - dependencies: - "@webex/test-helper-chai": "npm:3.12.0-next.1" - checksum: 10c0/daaf7fa3eeae4749485d1f64eba9f2c20bbdaec1870873f6b393ec0f3dc6a8728c0dbf46c8969bb49c34208ad0cf56cbc5fde0575d3e1a786e8a744be2c57ea5 - languageName: node - linkType: hard - "@webex/storage-adapter-spec@npm:3.12.0-next.5": version: 3.12.0-next.5 resolution: "@webex/storage-adapter-spec@npm:3.12.0-next.5" @@ -11925,17 +11552,6 @@ __metadata: languageName: node linkType: hard -"@webex/test-helper-chai@npm:3.12.0-next.1": - version: 3.12.0-next.1 - resolution: "@webex/test-helper-chai@npm:3.12.0-next.1" - dependencies: - "@webex/test-helper-file": "npm:3.12.0-next.1" - check-error: "npm:^1.0.2" - lodash: "npm:^4.17.21" - checksum: 10c0/5d8139752365ed2dc3cd4ae790b5ca542d2c689108df414082a907a8bdad674293fffe49858b7cec64bac1085579b6d55f7c3aea40edefc4d6d23f17b3b46bd1 - languageName: node - linkType: hard - "@webex/test-helper-chai@npm:3.12.0-next.5": version: 3.12.0-next.5 resolution: "@webex/test-helper-chai@npm:3.12.0-next.5" @@ -11973,19 +11589,6 @@ __metadata: languageName: node linkType: hard -"@webex/test-helper-file@npm:3.12.0-next.1": - version: 3.12.0-next.1 - resolution: "@webex/test-helper-file@npm:3.12.0-next.1" - dependencies: - "@webex/common": "npm:3.12.0-next.1" - "@webex/test-helper-make-local-url": "npm:3.12.0-next.1" - es6-promise: "npm:^4.2.8" - file-type: "npm:^16.0.1" - xhr: "npm:^2.5.0" - checksum: 10c0/6ed8abaec3268ac09dafbb9da2e639c3b3f10f01ffe619c10e78b9834caf36b1fba55d4cab031ff55f6b159be40f1c61a3f5ac6e782f1ba626e6782392485643 - languageName: node - linkType: hard - "@webex/test-helper-file@npm:3.12.0-next.5": version: 3.12.0-next.5 resolution: "@webex/test-helper-file@npm:3.12.0-next.5" @@ -12013,13 +11616,6 @@ __metadata: languageName: node linkType: hard -"@webex/test-helper-make-local-url@npm:3.12.0-next.1": - version: 3.12.0-next.1 - resolution: "@webex/test-helper-make-local-url@npm:3.12.0-next.1" - checksum: 10c0/13dd73a17c3d241b02affcabf831828dcef0df8850240142159ce15b0970a66ca2caf5d4c40cf54d9fde27b61abcf86d40a0895069e3f8d0f79f769f5c952ab4 - languageName: node - linkType: hard - "@webex/test-helper-make-local-url@npm:3.12.0-next.5": version: 3.12.0-next.5 resolution: "@webex/test-helper-make-local-url@npm:3.12.0-next.5" @@ -12045,15 +11641,6 @@ __metadata: languageName: node linkType: hard -"@webex/test-helper-mocha@npm:3.12.0-next.1": - version: 3.12.0-next.1 - resolution: "@webex/test-helper-mocha@npm:3.12.0-next.1" - dependencies: - bowser: "npm:^2.11.0" - checksum: 10c0/3df1caa81e06216abcfeee71b340684dc43c85660c6f4b1ecf7db692cd8901496aac8e9572acdd20b1b443f4330e4a1ed7f1a0ae9f94e9f7f6123011762b42f1 - languageName: node - linkType: hard - "@webex/test-helper-mocha@npm:3.12.0-next.5": version: 3.12.0-next.5 resolution: "@webex/test-helper-mocha@npm:3.12.0-next.5" @@ -12077,13 +11664,6 @@ __metadata: languageName: node linkType: hard -"@webex/test-helper-mock-web-socket@npm:3.12.0-next.1": - version: 3.12.0-next.1 - resolution: "@webex/test-helper-mock-web-socket@npm:3.12.0-next.1" - checksum: 10c0/a572fa41d1420a36d57fc57b9705297dff5c62fb479ef6b7433ce56102b3e331f894dad679a04ec98cf4c45f8007cbc4129a07036dc9d4f5a11b0f40a18adc5c - languageName: node - linkType: hard - "@webex/test-helper-mock-web-socket@npm:3.12.0-next.5": version: 3.12.0-next.5 resolution: "@webex/test-helper-mock-web-socket@npm:3.12.0-next.5" @@ -12113,17 +11693,6 @@ __metadata: languageName: node linkType: hard -"@webex/test-helper-mock-webex@npm:3.12.0-next.1": - version: 3.12.0-next.1 - resolution: "@webex/test-helper-mock-webex@npm:3.12.0-next.1" - dependencies: - ampersand-state: "npm:^5.0.3" - es6-promise: "npm:^4.2.8" - lodash: "npm:^4.17.21" - checksum: 10c0/d422edd71d6ac2031215ab479e409686526324698e96fb0baaf588047779e33fa138273c213716c4937f78b254646b74aa8fc08d663b0aeebb02439e4804f88a - languageName: node - linkType: hard - "@webex/test-helper-mock-webex@npm:3.12.0-next.5": version: 3.12.0-next.5 resolution: "@webex/test-helper-mock-webex@npm:3.12.0-next.5" @@ -12149,13 +11718,6 @@ __metadata: languageName: node linkType: hard -"@webex/test-helper-refresh-callback@npm:3.12.0-next.1": - version: 3.12.0-next.1 - resolution: "@webex/test-helper-refresh-callback@npm:3.12.0-next.1" - checksum: 10c0/3144709abb36533e76891052e8d805c8377129a099a5972f2f2dad99cff163b555d683babc5a5cb1316fe92d6ba9588e890121c67df3355e47137dab5d7829a0 - languageName: node - linkType: hard - "@webex/test-helper-refresh-callback@npm:3.12.0-next.5": version: 3.12.0-next.5 resolution: "@webex/test-helper-refresh-callback@npm:3.12.0-next.5" @@ -12181,15 +11743,6 @@ __metadata: languageName: node linkType: hard -"@webex/test-helper-retry@npm:3.12.0-next.1": - version: 3.12.0-next.1 - resolution: "@webex/test-helper-retry@npm:3.12.0-next.1" - dependencies: - es6-promise: "npm:^4.2.8" - checksum: 10c0/0a94cef8493b53a43c42187ca4cfa6e4fefd569203c97018b59f93c99225f9ff07e24ecce150f231e552c948789925c82abc7aa68a9a96e8aa59967f162126fc - languageName: node - linkType: hard - "@webex/test-helper-retry@npm:3.12.0-next.5": version: 3.12.0-next.5 resolution: "@webex/test-helper-retry@npm:3.12.0-next.5" @@ -12225,17 +11778,6 @@ __metadata: languageName: node linkType: hard -"@webex/test-helper-test-users@npm:3.12.0-next.1": - version: 3.12.0-next.1 - resolution: "@webex/test-helper-test-users@npm:3.12.0-next.1" - dependencies: - "@webex/test-helper-retry": "npm:3.12.0-next.1" - "@webex/test-users": "npm:3.12.0-next.1" - lodash: "npm:^4.17.21" - checksum: 10c0/e4f9f44c1cc88042fa6b8b5d1f0b8379158fac09cb5cdc4abd2d5c656b0f4bc863b16baea885f0f4aaea9aa7aa8e10663268490d85528f9e0fa1d3e819d11198 - languageName: node - linkType: hard - "@webex/test-helper-test-users@npm:3.12.0-next.5": version: 3.12.0-next.5 resolution: "@webex/test-helper-test-users@npm:3.12.0-next.5" @@ -12275,20 +11817,6 @@ __metadata: languageName: node linkType: hard -"@webex/test-users@npm:3.12.0-next.1": - version: 3.12.0-next.1 - resolution: "@webex/test-users@npm:3.12.0-next.1" - dependencies: - "@webex/http-core": "npm:3.12.0-next.1" - "@webex/test-helper-mocha": "npm:3.12.0-next.1" - btoa: "npm:^1.2.1" - lodash: "npm:^4.17.21" - node-random-name: "npm:^1.0.1" - uuid: "npm:^3.3.2" - checksum: 10c0/29ba5766b6c150ec3d1ec908dc4078816dedae21a773b056102fafe7ff474856573dfb53885e63475fdac3a35900827224578e24588d75127ebde5f00405ba68 - languageName: node - linkType: hard - "@webex/test-users@npm:3.12.0-next.5": version: 3.12.0-next.5 resolution: "@webex/test-users@npm:3.12.0-next.5" @@ -12377,25 +11905,6 @@ __metadata: languageName: node linkType: hard -"@webex/web-client-media-engine@npm:3.39.12": - version: 3.39.12 - resolution: "@webex/web-client-media-engine@npm:3.39.12" - dependencies: - "@webex/json-multistream": "npm:^2.4.3" - "@webex/rtcstats": "npm:^1.5.5" - "@webex/ts-events": "npm:^1.2.1" - "@webex/ts-sdp": "npm:1.8.2" - "@webex/web-capabilities": "npm:^1.10.0" - "@webex/web-media-effects": "npm:2.33.5" - "@webex/webrtc-core": "npm:2.13.7" - async: "npm:^3.2.4" - js-logger: "npm:^1.6.1" - typed-emitter: "npm:^2.1.0" - uuid: "npm:^8.3.2" - checksum: 10c0/16d7d144ff7f2a95c5d407481d2c9871910c1d2cd20350837e019f86c36be2fe27fe88ee5b182c003274777b6298b898193d88c5e8df5309379ec8b9edd306f7 - languageName: node - linkType: hard - "@webex/web-client-media-engine@npm:3.40.2": version: 3.40.2 resolution: "@webex/web-client-media-engine@npm:3.40.2" @@ -12472,29 +11981,9 @@ __metadata: languageName: node linkType: hard -"@webex/webex-core@npm:3.12.0-next.15": - version: 3.12.0-next.15 - resolution: "@webex/webex-core@npm:3.12.0-next.15" - dependencies: - "@webex/common": "npm:3.12.0-next.1" - "@webex/common-timers": "npm:3.12.0-next.1" - "@webex/http-core": "npm:3.12.0-next.1" - "@webex/storage-adapter-spec": "npm:3.12.0-next.1" - ampersand-collection: "npm:^2.0.2" - ampersand-events: "npm:^2.0.2" - ampersand-state: "npm:^5.0.3" - core-decorators: "npm:^0.20.0" - crypto-js: "npm:^4.1.1" - jsonwebtoken: "npm:^9.0.0" - lodash: "npm:^4.17.21" - uuid: "npm:^3.3.2" - checksum: 10c0/295175e66e511f3d63649baba8cbf325a10b4f6adeddbfbfb2a7a483ee68c25f0730ea13a5c6e3c90ec5540d4cb94cc8d90083bbff14ac7d201b5217a60dbe80 - languageName: node - linkType: hard - -"@webex/webex-core@npm:3.12.0-next.26": - version: 3.12.0-next.26 - resolution: "@webex/webex-core@npm:3.12.0-next.26" +"@webex/webex-core@npm:3.12.0-next.29": + version: 3.12.0-next.29 + resolution: "@webex/webex-core@npm:3.12.0-next.29" dependencies: "@webex/common": "npm:3.12.0-next.5" "@webex/common-timers": "npm:3.12.0-next.5" @@ -12508,22 +11997,7 @@ __metadata: jsonwebtoken: "npm:^9.0.0" lodash: "npm:^4.17.21" uuid: "npm:^3.3.2" - checksum: 10c0/c9ece3ff64fb0ceb31fba55d34648d24d682d9d4350a3b4e730331d845c30a931b39510e4e447f04ecd30f59c82cb54d70807088da9f34fd001434ddbb1ca1d2 - languageName: node - linkType: hard - -"@webex/webrtc-core@npm:2.13.7": - version: 2.13.7 - resolution: "@webex/webrtc-core@npm:2.13.7" - dependencies: - "@webex/ts-events": "npm:^1.2.1" - "@webex/web-capabilities": "npm:^1.6.1" - "@webex/web-media-effects": "npm:2.33.5" - events: "npm:^3.3.0" - js-logger: "npm:^1.6.1" - typed-emitter: "npm:^2.1.0" - webrtc-adapter: "npm:^8.1.2" - checksum: 10c0/48f781ca9d57330b6498e155e2b46bd914773dfc8ce21df80d0d79bf902a224ca4961f848999a3d1b779b49aef8ce7e797374e9c42eec016f88cd2fdc433895a + checksum: 10c0/65d2bde0e17be552fc00e8fdb75529d3bc24750bb84e78e7295f45cd48e1f599be47af043d9b2725bf35d5ab356db07fe2ca15ba4536bfe768541a373b930836 languageName: node linkType: hard @@ -31169,7 +30643,7 @@ __metadata: ts-loader: "npm:^9.5.1" typescript: "npm:^5.6.3" typescript-eslint: "npm:^8.24.1" - webex: "npm:3.12.0-next.84" + webex: "npm:3.12.0-next.138" webpack: "npm:^5.94.0" webpack-cli: "npm:^5.1.4" webpack-dev-server: "npm:^5.1.0" @@ -35296,42 +34770,42 @@ __metadata: languageName: node linkType: hard -"webex@npm:3.12.0-next.84": - version: 3.12.0-next.84 - resolution: "webex@npm:3.12.0-next.84" +"webex@npm:3.12.0-next.138": + version: 3.12.0-next.138 + resolution: "webex@npm:3.12.0-next.138" dependencies: "@babel/polyfill": "npm:^7.12.1" "@babel/runtime-corejs2": "npm:^7.14.8" - "@webex/calling": "npm:3.12.0-next.34" - "@webex/common": "npm:3.12.0-next.1" - "@webex/contact-center": "npm:3.12.0-next.42" - "@webex/internal-plugin-calendar": "npm:3.12.0-next.17" - "@webex/internal-plugin-device": "npm:3.12.0-next.15" - "@webex/internal-plugin-dss": "npm:3.12.0-next.16" - "@webex/internal-plugin-llm": "npm:3.12.0-next.18" - "@webex/internal-plugin-mercury": "npm:3.12.0-next.16" - "@webex/internal-plugin-presence": "npm:3.12.0-next.16" - "@webex/internal-plugin-support": "npm:3.12.0-next.17" - "@webex/internal-plugin-task": "npm:^3.12.0-next.17" - "@webex/internal-plugin-voicea": "npm:3.12.0-next.18" - "@webex/plugin-attachment-actions": "npm:3.12.0-next.17" - "@webex/plugin-authorization": "npm:3.12.0-next.15" - "@webex/plugin-device-manager": "npm:3.12.0-next.17" - "@webex/plugin-encryption": "npm:3.12.0-next.16" - "@webex/plugin-logger": "npm:3.12.0-next.15" - "@webex/plugin-meetings": "npm:3.12.0-next.62" - "@webex/plugin-memberships": "npm:3.12.0-next.17" - "@webex/plugin-messages": "npm:3.12.0-next.17" - "@webex/plugin-people": "npm:3.12.0-next.16" - "@webex/plugin-rooms": "npm:3.12.0-next.17" - "@webex/plugin-team-memberships": "npm:3.12.0-next.15" - "@webex/plugin-teams": "npm:3.12.0-next.15" - "@webex/plugin-webhooks": "npm:3.12.0-next.15" - "@webex/storage-adapter-local-storage": "npm:3.12.0-next.15" - "@webex/webex-core": "npm:3.12.0-next.15" + "@webex/calling": "npm:3.12.0-next.71" + "@webex/common": "npm:3.12.0-next.5" + "@webex/contact-center": "npm:3.12.0-next.81" + "@webex/internal-plugin-calendar": "npm:3.12.0-next.31" + "@webex/internal-plugin-device": "npm:3.12.0-next.29" + "@webex/internal-plugin-dss": "npm:3.12.0-next.30" + "@webex/internal-plugin-llm": "npm:3.12.0-next.33" + "@webex/internal-plugin-mercury": "npm:3.12.0-next.30" + "@webex/internal-plugin-presence": "npm:3.12.0-next.30" + "@webex/internal-plugin-support": "npm:3.12.0-next.31" + "@webex/internal-plugin-task": "npm:^3.12.0-next.31" + "@webex/internal-plugin-voicea": "npm:3.12.0-next.33" + "@webex/plugin-attachment-actions": "npm:3.12.0-next.31" + "@webex/plugin-authorization": "npm:3.12.0-next.29" + "@webex/plugin-device-manager": "npm:3.12.0-next.31" + "@webex/plugin-encryption": "npm:3.12.0-next.30" + "@webex/plugin-logger": "npm:3.12.0-next.29" + "@webex/plugin-meetings": "npm:3.12.0-next.93" + "@webex/plugin-memberships": "npm:3.12.0-next.31" + "@webex/plugin-messages": "npm:3.12.0-next.31" + "@webex/plugin-people": "npm:3.12.0-next.30" + "@webex/plugin-rooms": "npm:3.12.0-next.31" + "@webex/plugin-team-memberships": "npm:3.12.0-next.29" + "@webex/plugin-teams": "npm:3.12.0-next.29" + "@webex/plugin-webhooks": "npm:3.12.0-next.29" + "@webex/storage-adapter-local-storage": "npm:3.12.0-next.29" + "@webex/webex-core": "npm:3.12.0-next.29" lodash: "npm:^4.17.21" safe-buffer: "npm:^5.2.0" - checksum: 10c0/2498002ee3e82e639209852eabb942bdabfae305d63e9cc0f5d3b2a015110c18f1e8edd198a6b095a7058eac96c947ee8df8d997d8d8434334ea0fa066be57c3 + checksum: 10c0/8b1fc29e12b0af440c11555813443c103c56b34165439181c18ec0cef160f4efa9452942cebd902b1981e6596cd9e822963424baf8b2321f231315a488cc8402 languageName: node linkType: hard From 5e205c18ad41e9f4cceeccc12e6d61e4e698fe0a Mon Sep 17 00:00:00 2001 From: Matthew Olker Date: Wed, 15 Jul 2026 10:39:23 -0500 Subject: [PATCH 03/14] fix(cc-components): update E911Modal test import path - Fix import path in e911-modal.test.tsx to match renamed component file - Update yarn.lock --- .../E911Modal/e911-modal.test.tsx | 2 +- yarn.lock | 938 ++++++++++++++---- 2 files changed, 733 insertions(+), 207 deletions(-) diff --git a/packages/contact-center/cc-components/tests/components/StationLogin/E911Modal/e911-modal.test.tsx b/packages/contact-center/cc-components/tests/components/StationLogin/E911Modal/e911-modal.test.tsx index 2517eea6d..9bb64fecc 100644 --- a/packages/contact-center/cc-components/tests/components/StationLogin/E911Modal/e911-modal.test.tsx +++ b/packages/contact-center/cc-components/tests/components/StationLogin/E911Modal/e911-modal.test.tsx @@ -1,7 +1,7 @@ import React from 'react'; import {render, screen, fireEvent, waitFor} from '@testing-library/react'; import '@testing-library/jest-dom'; -import E911Modal from '../../../../src/components/StationLogin/E911Modal/E911Modal'; +import E911Modal from '../../../../src/components/StationLogin/E911Modal/e911-modal'; import {E911ModalLabels} from '../../../../src/components/StationLogin/E911Modal/e911-modal.constants'; jest.mock('@webex/cc-ui-logging', () => ({ diff --git a/yarn.lock b/yarn.lock index 2576c81b7..911729fe7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9560,6 +9560,30 @@ __metadata: languageName: node linkType: hard +"@webex/calling@npm:3.12.0-next.34": + version: 3.12.0-next.34 + resolution: "@webex/calling@npm:3.12.0-next.34" + dependencies: + "@types/platform": "npm:1.3.4" + "@webex/common": "npm:3.12.0-next.1" + "@webex/common-timers": "npm:3.12.0-next.1" + "@webex/internal-media-core": "npm:2.25.1" + "@webex/internal-plugin-device": "npm:3.12.0-next.15" + "@webex/internal-plugin-feature": "npm:3.12.0-next.15" + "@webex/internal-plugin-metrics": "npm:3.12.0-next.15" + "@webex/media-helpers": "npm:3.12.0-next.4" + async-mutex: "npm:0.4.0" + backoff: "npm:2.5.0" + buffer: "npm:6.0.3" + lodash: "npm:^4.17.21" + platform: "npm:1.3.6" + uuid: "npm:8.3.2" + ws: "npm:8.17.1" + xstate: "npm:4.30.6" + checksum: 10c0/721f9cafa26c610115b59e0bab6dbe7e8106796d468ed441af12dd6c301acdfd1f366851d27ea7046c59c9abd3c92e8d2e8768cf154b843016f492cb3ddbecf0 + languageName: node + linkType: hard + "@webex/calling@npm:3.12.0-next.71": version: 3.12.0-next.71 resolution: "@webex/calling@npm:3.12.0-next.71" @@ -9967,6 +9991,13 @@ __metadata: languageName: node linkType: hard +"@webex/common-timers@npm:3.12.0-next.1": + version: 3.12.0-next.1 + resolution: "@webex/common-timers@npm:3.12.0-next.1" + checksum: 10c0/8181a7e7195df093db1ca649b32ada69b949b9f8f9d18c275120584f60ec912f5b2fa7772186f63c959eace900abb1ea4619db5bbac5839d6feda5c89758ae9a + languageName: node + linkType: hard + "@webex/common-timers@npm:3.12.0-next.5": version: 3.12.0-next.5 resolution: "@webex/common-timers@npm:3.12.0-next.5" @@ -10022,6 +10053,21 @@ __metadata: languageName: node linkType: hard +"@webex/common@npm:3.12.0-next.1": + version: 3.12.0-next.1 + resolution: "@webex/common@npm:3.12.0-next.1" + dependencies: + backoff: "npm:^2.5.0" + bowser: "npm:^2.11.0" + core-decorators: "npm:^0.20.0" + global: "npm:^4.4.0" + lodash: "npm:^4.17.21" + safe-buffer: "npm:^5.2.0" + urlsafe-base64: "npm:^1.0.0" + checksum: 10c0/6b604810d385f432671c3435211ddf097ac98d1d779acf418d1a53a9d8064e889d3381b2d48b0e43c008c806ce64387dccb1001b714acef2ab4b93f44fbb2bfe + languageName: node + linkType: hard + "@webex/common@npm:3.12.0-next.5": version: 3.12.0-next.5 resolution: "@webex/common@npm:3.12.0-next.5" @@ -10073,6 +10119,24 @@ __metadata: languageName: node linkType: hard +"@webex/contact-center@npm:3.12.0-next.42": + version: 3.12.0-next.42 + resolution: "@webex/contact-center@npm:3.12.0-next.42" + dependencies: + "@types/platform": "npm:1.3.4" + "@webex/calling": "npm:3.12.0-next.34" + "@webex/internal-plugin-mercury": "npm:3.12.0-next.16" + "@webex/internal-plugin-metrics": "npm:3.12.0-next.15" + "@webex/internal-plugin-support": "npm:3.12.0-next.17" + "@webex/plugin-authorization": "npm:3.12.0-next.15" + "@webex/plugin-logger": "npm:3.12.0-next.15" + "@webex/webex-core": "npm:3.12.0-next.15" + jest-html-reporters: "npm:3.0.11" + lodash: "npm:^4.17.21" + checksum: 10c0/7b4caa4a12dcd1070b7ed720fc56e7df357ae48126aa7a48c5ab2f854210093120891e2ab4d7cc8966539dbe204b0bf8e7d9aed22abab8216098c5dd72f760b4 + languageName: node + linkType: hard + "@webex/contact-center@npm:3.12.0-next.81": version: 3.12.0-next.81 resolution: "@webex/contact-center@npm:3.12.0-next.81" @@ -10124,6 +10188,15 @@ __metadata: languageName: node linkType: hard +"@webex/helper-html@npm:3.12.0-next.1": + version: 3.12.0-next.1 + resolution: "@webex/helper-html@npm:3.12.0-next.1" + dependencies: + lodash: "npm:^4.17.21" + checksum: 10c0/cf325224593d4f2b52d0542828ef60bc22343721aa84047e97b3c12bc133f1e105db06c01ae9fb6f32bef6cae2a8f2e8b6c4aecae5083ffc72fad811dc6f88be + languageName: node + linkType: hard + "@webex/helper-html@npm:3.12.0-next.5": version: 3.12.0-next.5 resolution: "@webex/helper-html@npm:3.12.0-next.5" @@ -10167,6 +10240,23 @@ __metadata: languageName: node linkType: hard +"@webex/helper-image@npm:3.12.0-next.1": + version: 3.12.0-next.1 + resolution: "@webex/helper-image@npm:3.12.0-next.1" + dependencies: + "@webex/http-core": "npm:3.12.0-next.1" + "@webex/test-helper-chai": "npm:3.12.0-next.1" + "@webex/test-helper-file": "npm:3.12.0-next.1" + "@webex/test-helper-mocha": "npm:3.12.0-next.1" + exifr: "npm:^5.0.3" + gm: "npm:^1.23.1" + lodash: "npm:^4.17.21" + mime: "npm:^2.4.4" + safe-buffer: "npm:^5.2.0" + checksum: 10c0/1ab6b056cf208541876299b6ba984856c2f65ccf1f2f2c34b3e2eb0eb963b14eba4759178dbb99e86dfbbaa6e515ec958d591450ac16902339f138b86a78a461 + languageName: node + linkType: hard + "@webex/helper-image@npm:3.12.0-next.5": version: 3.12.0-next.5 resolution: "@webex/helper-image@npm:3.12.0-next.5" @@ -10243,6 +10333,24 @@ __metadata: languageName: node linkType: hard +"@webex/http-core@npm:3.12.0-next.1": + version: 3.12.0-next.1 + resolution: "@webex/http-core@npm:3.12.0-next.1" + dependencies: + "@webex/common": "npm:3.12.0-next.1" + file-type: "npm:^16.0.1" + global: "npm:^4.4.0" + is-function: "npm:^1.0.1" + lodash: "npm:^4.17.21" + parse-headers: "npm:^2.0.2" + qs: "npm:^6.7.3" + request: "npm:^2.88.0" + safe-buffer: "npm:^5.2.0" + xtend: "npm:^4.0.2" + checksum: 10c0/438aae8087f00da83b7a8790a74605167fc60ff5256bb79cd900803ee547b99046b8d2c7898f0c4d91092f28eee65ba8a8c2df591c3d784fff1b5e2b65af3876 + languageName: node + linkType: hard + "@webex/http-core@npm:3.12.0-next.5": version: 3.12.0-next.5 resolution: "@webex/http-core@npm:3.12.0-next.5" @@ -10278,6 +10386,26 @@ __metadata: languageName: node linkType: hard +"@webex/internal-media-core@npm:2.25.1": + version: 2.25.1 + resolution: "@webex/internal-media-core@npm:2.25.1" + dependencies: + "@babel/runtime": "npm:^7.18.9" + "@babel/runtime-corejs2": "npm:^7.25.0" + "@webex/rtcstats": "npm:^1.5.5" + "@webex/ts-sdp": "npm:1.8.2" + "@webex/web-capabilities": "npm:^1.10.0" + "@webex/web-client-media-engine": "npm:3.39.12" + events: "npm:^3.3.0" + ip-anonymize: "npm:^0.1.0" + typed-emitter: "npm:^2.1.0" + uuid: "npm:^8.3.2" + webrtc-adapter: "npm:^8.1.2" + xstate: "npm:^4.30.6" + checksum: 10c0/7a726f3226c1a0d4a1b60abccbeae8d5a60560bb11d5f0877c8411fbdb91f2f86f56ac83fad1bc5ace21d45afdbc20f810340e74b1986d785fd9bfb8cee04eb1 + languageName: node + linkType: hard + "@webex/internal-media-core@npm:2.26.2": version: 2.26.2 resolution: "@webex/internal-media-core@npm:2.26.2" @@ -10312,17 +10440,17 @@ __metadata: languageName: node linkType: hard -"@webex/internal-plugin-calendar@npm:3.12.0-next.31": - version: 3.12.0-next.31 - resolution: "@webex/internal-plugin-calendar@npm:3.12.0-next.31" +"@webex/internal-plugin-calendar@npm:3.12.0-next.17": + version: 3.12.0-next.17 + resolution: "@webex/internal-plugin-calendar@npm:3.12.0-next.17" dependencies: - "@webex/internal-plugin-conversation": "npm:3.12.0-next.31" - "@webex/internal-plugin-device": "npm:3.12.0-next.29" - "@webex/internal-plugin-encryption": "npm:3.12.0-next.30" - "@webex/webex-core": "npm:3.12.0-next.29" + "@webex/internal-plugin-conversation": "npm:3.12.0-next.17" + "@webex/internal-plugin-device": "npm:3.12.0-next.15" + "@webex/internal-plugin-encryption": "npm:3.12.0-next.16" + "@webex/webex-core": "npm:3.12.0-next.15" lodash: "npm:^4.17.21" uuid: "npm:^3.3.2" - checksum: 10c0/97fc8cb51d57cb6dd74d4b50691be4c8b2c9b6ff45be8abb80bb612c985c276ef43adff9df45059dbc85603bd051070bead80da6b6bd602370a2d82488338a06 + checksum: 10c0/9461f0f5659a686da11686def1fdc1b265dd5185a50fe3e6e800f4dd6b5f5a88612e6a50d0ecfa39999ee79998e94f77f90f809f89543d1676ae353b727677d5 languageName: node linkType: hard @@ -10362,6 +10490,24 @@ __metadata: languageName: node linkType: hard +"@webex/internal-plugin-conversation@npm:3.12.0-next.17": + version: 3.12.0-next.17 + resolution: "@webex/internal-plugin-conversation@npm:3.12.0-next.17" + dependencies: + "@webex/common": "npm:3.12.0-next.1" + "@webex/helper-html": "npm:3.12.0-next.1" + "@webex/helper-image": "npm:3.12.0-next.1" + "@webex/internal-plugin-encryption": "npm:3.12.0-next.16" + "@webex/internal-plugin-user": "npm:3.12.0-next.16" + "@webex/webex-core": "npm:3.12.0-next.15" + crypto-js: "npm:^4.1.1" + lodash: "npm:^4.17.21" + node-scr: "npm:^0.3.0" + uuid: "npm:^3.3.2" + checksum: 10c0/0d7345f75b13b905b6c3cc5a94bb37f36788a1750789b09a392e2046686916eceddc1d1913c34d850d3b2c5a33b8f3b5945415985c0d637b9879d4a57cb4065d + languageName: node + linkType: hard + "@webex/internal-plugin-conversation@npm:3.12.0-next.31": version: 3.12.0-next.31 resolution: "@webex/internal-plugin-conversation@npm:3.12.0-next.31" @@ -10413,6 +10559,23 @@ __metadata: languageName: node linkType: hard +"@webex/internal-plugin-device@npm:3.12.0-next.15": + version: 3.12.0-next.15 + resolution: "@webex/internal-plugin-device@npm:3.12.0-next.15" + dependencies: + "@webex/common": "npm:3.12.0-next.1" + "@webex/common-timers": "npm:3.12.0-next.1" + "@webex/http-core": "npm:3.12.0-next.1" + "@webex/internal-plugin-metrics": "npm:3.12.0-next.15" + "@webex/webex-core": "npm:3.12.0-next.15" + ampersand-collection: "npm:^2.0.2" + ampersand-state: "npm:^5.0.3" + lodash: "npm:^4.17.21" + uuid: "npm:^3.3.2" + checksum: 10c0/c9bef045a01429c75ef55eee0fcdc08e6d647e4a35a81217963157a24c7e454093419a9a68203bdd3fb3f1081ba269d96d410b870e1eae5f4154691f40dfb8ed + languageName: node + linkType: hard + "@webex/internal-plugin-device@npm:3.12.0-next.29": version: 3.12.0-next.29 resolution: "@webex/internal-plugin-device@npm:3.12.0-next.29" @@ -10430,17 +10593,17 @@ __metadata: languageName: node linkType: hard -"@webex/internal-plugin-dss@npm:3.12.0-next.30": - version: 3.12.0-next.30 - resolution: "@webex/internal-plugin-dss@npm:3.12.0-next.30" +"@webex/internal-plugin-dss@npm:3.12.0-next.16": + version: 3.12.0-next.16 + resolution: "@webex/internal-plugin-dss@npm:3.12.0-next.16" dependencies: - "@webex/common": "npm:3.12.0-next.5" - "@webex/common-timers": "npm:3.12.0-next.5" - "@webex/internal-plugin-mercury": "npm:3.12.0-next.30" - "@webex/webex-core": "npm:3.12.0-next.29" + "@webex/common": "npm:3.12.0-next.1" + "@webex/common-timers": "npm:3.12.0-next.1" + "@webex/internal-plugin-mercury": "npm:3.12.0-next.16" + "@webex/webex-core": "npm:3.12.0-next.15" lodash: "npm:^4.17.21" uuid: "npm:^3.3.2" - checksum: 10c0/eae4c134b85aea08f579bfd549bde3ca2a2e7613cdac13e1df2ee77fb860e47be135c21d43633d4fce5aa3156632e70e1526fd3e8f0b084195e77febd6040313 + checksum: 10c0/bb7d8c041171235beb09df72731256824daa50e2a662b5aaad8af53105b6833371d75785613c9df851b658e337cc64cbb1c1f4868970124a7542dc5898a3817b languageName: node linkType: hard @@ -10496,6 +10659,32 @@ __metadata: languageName: node linkType: hard +"@webex/internal-plugin-encryption@npm:3.12.0-next.16": + version: 3.12.0-next.16 + resolution: "@webex/internal-plugin-encryption@npm:3.12.0-next.16" + dependencies: + "@webex/common": "npm:3.12.0-next.1" + "@webex/common-timers": "npm:3.12.0-next.1" + "@webex/http-core": "npm:3.12.0-next.1" + "@webex/internal-plugin-device": "npm:3.12.0-next.15" + "@webex/internal-plugin-mercury": "npm:3.12.0-next.16" + "@webex/test-helper-file": "npm:3.12.0-next.1" + "@webex/webex-core": "npm:3.12.0-next.15" + asn1js: "npm:^2.0.26" + debug: "npm:^4.3.4" + isomorphic-webcrypto: "npm:^2.3.8" + lodash: "npm:^4.17.21" + node-jose: "npm:^2.2.0" + node-kms: "npm:^0.4.1" + node-scr: "npm:^0.3.0" + pkijs: "npm:^2.1.84" + safe-buffer: "npm:^5.2.0" + uuid: "npm:^3.3.2" + valid-url: "npm:^1.0.9" + checksum: 10c0/ed33bdfca34f2fe0bb81ef7b7ebcd4b336c7c36fdd6bc659941e56b5666448e7daf19fb1a0f7481d25ddb1924cbda8a536b87328196facccda0dca98e0942bc7 + languageName: node + linkType: hard + "@webex/internal-plugin-encryption@npm:3.12.0-next.30": version: 3.12.0-next.30 resolution: "@webex/internal-plugin-encryption@npm:3.12.0-next.30" @@ -10545,6 +10734,17 @@ __metadata: languageName: node linkType: hard +"@webex/internal-plugin-feature@npm:3.12.0-next.15": + version: 3.12.0-next.15 + resolution: "@webex/internal-plugin-feature@npm:3.12.0-next.15" + dependencies: + "@webex/internal-plugin-device": "npm:3.12.0-next.15" + "@webex/webex-core": "npm:3.12.0-next.15" + lodash: "npm:^4.17.21" + checksum: 10c0/c6d99e9b93ac300ec601c8d1da5a33e5249aadf49aec14166f5a14000045fcf07b28bf55890691680dab219f7f5291e9b2b74af56bf6473121eb862d402c439b + languageName: node + linkType: hard + "@webex/internal-plugin-feature@npm:3.12.0-next.29": version: 3.12.0-next.29 resolution: "@webex/internal-plugin-feature@npm:3.12.0-next.29" @@ -10556,12 +10756,12 @@ __metadata: languageName: node linkType: hard -"@webex/internal-plugin-llm@npm:3.12.0-next.33": - version: 3.12.0-next.33 - resolution: "@webex/internal-plugin-llm@npm:3.12.0-next.33" +"@webex/internal-plugin-llm@npm:3.12.0-next.18": + version: 3.12.0-next.18 + resolution: "@webex/internal-plugin-llm@npm:3.12.0-next.18" dependencies: - "@webex/internal-plugin-mercury": "npm:3.12.0-next.30" - checksum: 10c0/9bf24edb69b07a1ecf36347bbce674a9c36f3f9717950c21a32cdd6017fe218ea23a3f71a391c33db6f9dc18cfdefe4d5b8b18edb40f9131b4a235a11fecf2ab + "@webex/internal-plugin-mercury": "npm:3.12.0-next.16" + checksum: 10c0/a045c09cce44b6e3d6048dbb79bc3a4becd5206ba3c9c91529dc5eee707847d21b8bc57423290e055949b3dbed70d2db8fc3560867642000a5a888f1250e6a7b languageName: node linkType: hard @@ -10579,17 +10779,17 @@ __metadata: languageName: node linkType: hard -"@webex/internal-plugin-locus@npm:3.12.0-next.30": - version: 3.12.0-next.30 - resolution: "@webex/internal-plugin-locus@npm:3.12.0-next.30" +"@webex/internal-plugin-locus@npm:3.12.0-next.16": + version: 3.12.0-next.16 + resolution: "@webex/internal-plugin-locus@npm:3.12.0-next.16" dependencies: - "@webex/internal-plugin-mercury": "npm:3.12.0-next.30" - "@webex/test-helper-chai": "npm:3.12.0-next.5" - "@webex/test-helper-mock-webex": "npm:3.12.0-next.5" - "@webex/webex-core": "npm:3.12.0-next.29" + "@webex/internal-plugin-mercury": "npm:3.12.0-next.16" + "@webex/test-helper-chai": "npm:3.12.0-next.1" + "@webex/test-helper-mock-webex": "npm:3.12.0-next.1" + "@webex/webex-core": "npm:3.12.0-next.15" lodash: "npm:^4.17.21" uuid: "npm:^3.3.2" - checksum: 10c0/f1520f2c97c90bdbe812de415aae747a7e49e0f985e779694555ff8bf4f4d30cb77ddf158622f9214b2863004b3e010d6f94ebbc03cc8f78c5c3c99b455c809a + checksum: 10c0/7df66a258562ec27ced651a2be5dc002641414954df727f2d83dbc78e96b2d11bbe9209a0eb801a5bff2950796f100152ee28a60a245dd760b4aef70eadf3b2f languageName: node linkType: hard @@ -10610,20 +10810,20 @@ __metadata: languageName: node linkType: hard -"@webex/internal-plugin-lyra@npm:3.12.0-next.31": - version: 3.12.0-next.31 - resolution: "@webex/internal-plugin-lyra@npm:3.12.0-next.31" +"@webex/internal-plugin-lyra@npm:3.12.0-next.17": + version: 3.12.0-next.17 + resolution: "@webex/internal-plugin-lyra@npm:3.12.0-next.17" dependencies: - "@webex/common": "npm:3.12.0-next.5" - "@webex/internal-plugin-conversation": "npm:3.12.0-next.31" - "@webex/internal-plugin-encryption": "npm:3.12.0-next.30" - "@webex/internal-plugin-feature": "npm:3.12.0-next.29" - "@webex/internal-plugin-locus": "npm:3.12.0-next.30" - "@webex/internal-plugin-mercury": "npm:3.12.0-next.30" - "@webex/webex-core": "npm:3.12.0-next.29" + "@webex/common": "npm:3.12.0-next.1" + "@webex/internal-plugin-conversation": "npm:3.12.0-next.17" + "@webex/internal-plugin-encryption": "npm:3.12.0-next.16" + "@webex/internal-plugin-feature": "npm:3.12.0-next.15" + "@webex/internal-plugin-locus": "npm:3.12.0-next.16" + "@webex/internal-plugin-mercury": "npm:3.12.0-next.16" + "@webex/webex-core": "npm:3.12.0-next.15" bowser: "npm:^2.11.0" uuid: "npm:^3.3.2" - checksum: 10c0/8a6fdb6d7909806e644016a1f8f09a759d1ee15f34d7d4d8db3dac5708c9ef476d1233a0685859a27c795b3d4e4039342511937ca7347e8b11e72cec782656db + checksum: 10c0/e94ed78b8f3bb50bcdbe68e7d4c0374a5ebe8ad19defb9a0cab90ceacc3ee4f6d2607d9ebc1ec751d1a4e4cfb55617973996ebc9e643da71054fe496b3a2bf04 languageName: node linkType: hard @@ -10675,6 +10875,30 @@ __metadata: languageName: node linkType: hard +"@webex/internal-plugin-mercury@npm:3.12.0-next.16": + version: 3.12.0-next.16 + resolution: "@webex/internal-plugin-mercury@npm:3.12.0-next.16" + dependencies: + "@webex/common": "npm:3.12.0-next.1" + "@webex/common-timers": "npm:3.12.0-next.1" + "@webex/internal-plugin-device": "npm:3.12.0-next.15" + "@webex/internal-plugin-feature": "npm:3.12.0-next.15" + "@webex/internal-plugin-metrics": "npm:3.12.0-next.15" + "@webex/test-helper-chai": "npm:3.12.0-next.1" + "@webex/test-helper-mocha": "npm:3.12.0-next.1" + "@webex/test-helper-mock-web-socket": "npm:3.12.0-next.1" + "@webex/test-helper-mock-webex": "npm:3.12.0-next.1" + "@webex/test-helper-refresh-callback": "npm:3.12.0-next.1" + "@webex/test-helper-test-users": "npm:3.12.0-next.1" + "@webex/webex-core": "npm:3.12.0-next.15" + backoff: "npm:^2.5.0" + lodash: "npm:^4.17.21" + uuid: "npm:^3.3.2" + ws: "npm:^8.17.1" + checksum: 10c0/0cfe3cb25a5b42513103ff147b850476c6f0d885e317a13dc2fe34f3a1d9e605edbd75b421ea667b1470c414b82998340e37d64bb33176e19844fb1466347ded + languageName: node + linkType: hard + "@webex/internal-plugin-mercury@npm:3.12.0-next.30": version: 3.12.0-next.30 resolution: "@webex/internal-plugin-mercury@npm:3.12.0-next.30" @@ -10728,6 +10952,22 @@ __metadata: languageName: node linkType: hard +"@webex/internal-plugin-metrics@npm:3.12.0-next.15": + version: 3.12.0-next.15 + resolution: "@webex/internal-plugin-metrics@npm:3.12.0-next.15" + dependencies: + "@webex/common": "npm:3.12.0-next.1" + "@webex/common-timers": "npm:3.12.0-next.1" + "@webex/test-helper-chai": "npm:3.12.0-next.1" + "@webex/test-helper-mock-webex": "npm:3.12.0-next.1" + "@webex/webex-core": "npm:3.12.0-next.15" + ip-anonymize: "npm:^0.1.0" + lodash: "npm:^4.17.21" + uuid: "npm:^3.3.2" + checksum: 10c0/d8b1d3f1b1f34589f57e9a30030cd8922a95d009f3e2d0b57dd57ade34c439a371d38bb4c0960ccab65498c55b9b0f3d12451e3b0d66a95a0f59757b23c55a83 + languageName: node + linkType: hard + "@webex/internal-plugin-metrics@npm:3.12.0-next.29": version: 3.12.0-next.29 resolution: "@webex/internal-plugin-metrics@npm:3.12.0-next.29" @@ -10760,19 +11000,19 @@ __metadata: languageName: node linkType: hard -"@webex/internal-plugin-presence@npm:3.12.0-next.30": - version: 3.12.0-next.30 - resolution: "@webex/internal-plugin-presence@npm:3.12.0-next.30" +"@webex/internal-plugin-presence@npm:3.12.0-next.16": + version: 3.12.0-next.16 + resolution: "@webex/internal-plugin-presence@npm:3.12.0-next.16" dependencies: - "@webex/internal-plugin-device": "npm:3.12.0-next.29" - "@webex/internal-plugin-mercury": "npm:3.12.0-next.30" - "@webex/test-helper-chai": "npm:3.12.0-next.5" - "@webex/test-helper-mocha": "npm:3.12.0-next.5" - "@webex/test-helper-mock-webex": "npm:3.12.0-next.5" - "@webex/test-helper-test-users": "npm:3.12.0-next.5" - "@webex/webex-core": "npm:3.12.0-next.29" + "@webex/internal-plugin-device": "npm:3.12.0-next.15" + "@webex/internal-plugin-mercury": "npm:3.12.0-next.16" + "@webex/test-helper-chai": "npm:3.12.0-next.1" + "@webex/test-helper-mocha": "npm:3.12.0-next.1" + "@webex/test-helper-mock-webex": "npm:3.12.0-next.1" + "@webex/test-helper-test-users": "npm:3.12.0-next.1" + "@webex/webex-core": "npm:3.12.0-next.15" lodash: "npm:^4.17.21" - checksum: 10c0/f501404c2ef446ffa80c3c0521c71ed2c166459b74c9c2845b9c762f11eccb13370755cd2f0dc97e4f5f09156676762f453baca2ebc0a2239a99e6625d1e1ff1 + checksum: 10c0/557276a3f1c64eb4026a9dcba35df70b7ad890109026582297085add6c970ad53d98ca46fee66862850a5f7ac2b2d8368c8aff5ddf0eac52f3efc1ff14198417 languageName: node linkType: hard @@ -10791,6 +11031,21 @@ __metadata: languageName: node linkType: hard +"@webex/internal-plugin-search@npm:3.12.0-next.17": + version: 3.12.0-next.17 + resolution: "@webex/internal-plugin-search@npm:3.12.0-next.17" + dependencies: + "@webex/common": "npm:3.12.0-next.1" + "@webex/internal-plugin-conversation": "npm:3.12.0-next.17" + "@webex/internal-plugin-device": "npm:3.12.0-next.15" + "@webex/internal-plugin-encryption": "npm:3.12.0-next.16" + "@webex/webex-core": "npm:3.12.0-next.15" + lodash: "npm:^4.17.21" + uuid: "npm:^3.3.2" + checksum: 10c0/b5e13de9b8312333a7ff48630ea0bb306da5b2d250626806e08d3cad5a43c20a8a8f119175687d9320a7ea2e8c13c2d8daa8aa8d1cc6e400f5aab89b501f553d + languageName: node + linkType: hard + "@webex/internal-plugin-search@npm:3.12.0-next.31": version: 3.12.0-next.31 resolution: "@webex/internal-plugin-search@npm:3.12.0-next.31" @@ -10823,6 +11078,23 @@ __metadata: languageName: node linkType: hard +"@webex/internal-plugin-support@npm:3.12.0-next.17": + version: 3.12.0-next.17 + resolution: "@webex/internal-plugin-support@npm:3.12.0-next.17" + dependencies: + "@webex/internal-plugin-device": "npm:3.12.0-next.15" + "@webex/internal-plugin-search": "npm:3.12.0-next.17" + "@webex/test-helper-chai": "npm:3.12.0-next.1" + "@webex/test-helper-file": "npm:3.12.0-next.1" + "@webex/test-helper-mock-webex": "npm:3.12.0-next.1" + "@webex/test-helper-test-users": "npm:3.12.0-next.1" + "@webex/webex-core": "npm:3.12.0-next.15" + lodash: "npm:^4.17.21" + uuid: "npm:^3.3.2" + checksum: 10c0/e2d792681c264c646d47187a3a7a8f995fc63b1c724f78762d2e019db8798ae5b7ca6a7a516cf8938e66915f8c969c08a400806ffb51edf787397629bacc3c1c + languageName: node + linkType: hard + "@webex/internal-plugin-support@npm:3.12.0-next.31": version: 3.12.0-next.31 resolution: "@webex/internal-plugin-support@npm:3.12.0-next.31" @@ -10840,7 +11112,7 @@ __metadata: languageName: node linkType: hard -"@webex/internal-plugin-task@npm:^3.12.0-next.31": +"@webex/internal-plugin-task@npm:^3.12.0-next.17": version: 3.12.0 resolution: "@webex/internal-plugin-task@npm:3.12.0" dependencies: @@ -10886,6 +11158,22 @@ __metadata: languageName: node linkType: hard +"@webex/internal-plugin-user@npm:3.12.0-next.16": + version: 3.12.0-next.16 + resolution: "@webex/internal-plugin-user@npm:3.12.0-next.16" + dependencies: + "@webex/common": "npm:3.12.0-next.1" + "@webex/internal-plugin-device": "npm:3.12.0-next.15" + "@webex/test-helper-chai": "npm:3.12.0-next.1" + "@webex/test-helper-mock-webex": "npm:3.12.0-next.1" + "@webex/test-helper-test-users": "npm:3.12.0-next.1" + "@webex/webex-core": "npm:3.12.0-next.15" + lodash: "npm:^4.17.21" + uuid: "npm:^3.3.2" + checksum: 10c0/5f625971f05369f2ae1b22570383f5e6739291c66d1ac0dd44b7d2dfab19c844c0daafd6d5faa1afea3ef62656a27091a89604ab5e53479a9bc1663e5cc735ca + languageName: node + linkType: hard + "@webex/internal-plugin-user@npm:3.12.0-next.30": version: 3.12.0-next.30 resolution: "@webex/internal-plugin-user@npm:3.12.0-next.30" @@ -10902,15 +11190,15 @@ __metadata: languageName: node linkType: hard -"@webex/internal-plugin-voicea@npm:3.12.0-next.33": - version: 3.12.0-next.33 - resolution: "@webex/internal-plugin-voicea@npm:3.12.0-next.33" +"@webex/internal-plugin-voicea@npm:3.12.0-next.18": + version: 3.12.0-next.18 + resolution: "@webex/internal-plugin-voicea@npm:3.12.0-next.18" dependencies: - "@webex/internal-plugin-llm": "npm:3.12.0-next.33" - "@webex/internal-plugin-mercury": "npm:3.12.0-next.30" - "@webex/webex-core": "npm:3.12.0-next.29" + "@webex/internal-plugin-llm": "npm:3.12.0-next.18" + "@webex/internal-plugin-mercury": "npm:3.12.0-next.16" + "@webex/webex-core": "npm:3.12.0-next.15" uuid: "npm:^3.3.2" - checksum: 10c0/aba67fc9bc3e0a958cdfefc73362901da2194441efddbc17032790e5de41ab51cc33785911746dab06c89380ce996156b60fbd0a053187a24615206b12a2ea96 + checksum: 10c0/a95e8af210eb469d6501256216748ce7e066f567d02c3fab17430d825dd422068cc536ec88e9a06feac01d03b81aa6c5d799091cafb83ab54dc53398ac62903a languageName: node linkType: hard @@ -10945,6 +11233,17 @@ __metadata: languageName: node linkType: hard +"@webex/media-helpers@npm:3.12.0-next.4": + version: 3.12.0-next.4 + resolution: "@webex/media-helpers@npm:3.12.0-next.4" + dependencies: + "@webex/internal-media-core": "npm:2.25.1" + "@webex/ts-events": "npm:^1.1.0" + "@webex/web-media-effects": "npm:2.33.5" + checksum: 10c0/9ac15b19c7c450f4831b4d6afafc7873c86781670ea0fcd468f25b193f50d13d2c0b13a70b927a94b4d372ebe03f2ecad141df32cb0836dbcdb604c7de9f9799 + languageName: node + linkType: hard + "@webex/package-tools@npm:0.0.0-next.6": version: 0.0.0-next.6 resolution: "@webex/package-tools@npm:0.0.0-next.6" @@ -10975,21 +11274,21 @@ __metadata: languageName: node linkType: hard -"@webex/plugin-attachment-actions@npm:3.12.0-next.31": - version: 3.12.0-next.31 - resolution: "@webex/plugin-attachment-actions@npm:3.12.0-next.31" +"@webex/plugin-attachment-actions@npm:3.12.0-next.17": + version: 3.12.0-next.17 + resolution: "@webex/plugin-attachment-actions@npm:3.12.0-next.17" dependencies: - "@webex/common": "npm:3.12.0-next.5" - "@webex/internal-plugin-conversation": "npm:3.12.0-next.31" - "@webex/internal-plugin-mercury": "npm:3.12.0-next.30" - "@webex/webex-core": "npm:3.12.0-next.29" + "@webex/common": "npm:3.12.0-next.1" + "@webex/internal-plugin-conversation": "npm:3.12.0-next.17" + "@webex/internal-plugin-mercury": "npm:3.12.0-next.16" + "@webex/webex-core": "npm:3.12.0-next.15" debug: "npm:^4.3.4" lodash: "npm:^4.17.21" peerDependencies: - "@webex/plugin-logger": 3.12.0-next.29 - "@webex/plugin-messages": 3.12.0-next.31 - "@webex/plugin-people": 3.12.0-next.30 - checksum: 10c0/45e8929a2e4081be77b8ff547e2a0fde6469486721387226f72dccdc463c59926d0f7a9cb3af00ba27b2fc2dddfd930a79b8f8d20cd413bd3b8b80df283a623d + "@webex/plugin-logger": 3.12.0-next.15 + "@webex/plugin-messages": 3.12.0-next.17 + "@webex/plugin-people": 3.12.0-next.16 + checksum: 10c0/2e615f347362c443c4931a0bfbed9c551df76319755abecde51b78ed53b269cec337325ea9e13ffe260a58bb165817d63e5ac56d0cb5b70bfdb7d041979f87bb languageName: node linkType: hard @@ -11010,6 +11309,23 @@ __metadata: languageName: node linkType: hard +"@webex/plugin-authorization-browser@npm:3.12.0-next.15": + version: 3.12.0-next.15 + resolution: "@webex/plugin-authorization-browser@npm:3.12.0-next.15" + dependencies: + "@webex/common": "npm:3.12.0-next.1" + "@webex/internal-plugin-device": "npm:3.12.0-next.15" + "@webex/plugin-authorization-node": "npm:3.12.0-next.15" + "@webex/storage-adapter-local-storage": "npm:3.12.0-next.15" + "@webex/storage-adapter-spec": "npm:3.12.0-next.1" + "@webex/webex-core": "npm:3.12.0-next.15" + jsonwebtoken: "npm:^9.0.2" + lodash: "npm:^4.17.21" + uuid: "npm:^3.3.2" + checksum: 10c0/04f15ad355e5431dcad6a545f2b1e5c6e99e734743a310107996319cbcd080f6a5d0e115e297fdf414a3aa6c421bba7f786abfbee1fd7aec60d0333796712313 + languageName: node + linkType: hard + "@webex/plugin-authorization-browser@npm:3.12.0-next.29": version: 3.12.0-next.29 resolution: "@webex/plugin-authorization-browser@npm:3.12.0-next.29" @@ -11040,6 +11356,19 @@ __metadata: languageName: node linkType: hard +"@webex/plugin-authorization-node@npm:3.12.0-next.15": + version: 3.12.0-next.15 + resolution: "@webex/plugin-authorization-node@npm:3.12.0-next.15" + dependencies: + "@webex/common": "npm:3.12.0-next.1" + "@webex/internal-plugin-device": "npm:3.12.0-next.15" + "@webex/webex-core": "npm:3.12.0-next.15" + jsonwebtoken: "npm:^9.0.0" + uuid: "npm:^3.3.2" + checksum: 10c0/8bca668f4e7a17504674a02291ea699df1b350deea8cb9ab1c411e484e17ab765274227f5d1d4a9f4b2e3a4e7223739e38ec1b551780758e12900687960b87f3 + languageName: node + linkType: hard + "@webex/plugin-authorization-node@npm:3.12.0-next.29": version: 3.12.0-next.29 resolution: "@webex/plugin-authorization-node@npm:3.12.0-next.29" @@ -11063,6 +11392,16 @@ __metadata: languageName: node linkType: hard +"@webex/plugin-authorization@npm:3.12.0-next.15": + version: 3.12.0-next.15 + resolution: "@webex/plugin-authorization@npm:3.12.0-next.15" + dependencies: + "@webex/plugin-authorization-browser": "npm:3.12.0-next.15" + "@webex/plugin-authorization-node": "npm:3.12.0-next.15" + checksum: 10c0/97f50fb9b111628ea46be900e6f69ca61a7220c9dcc1b66a705f452178ecb9622ebdcb6f442139da09141fc34111b503035e38ce005d5ca5adff3cb86766148c + languageName: node + linkType: hard + "@webex/plugin-authorization@npm:3.12.0-next.29": version: 3.12.0-next.29 resolution: "@webex/plugin-authorization@npm:3.12.0-next.29" @@ -11091,31 +11430,31 @@ __metadata: languageName: node linkType: hard -"@webex/plugin-device-manager@npm:3.12.0-next.31": - version: 3.12.0-next.31 - resolution: "@webex/plugin-device-manager@npm:3.12.0-next.31" +"@webex/plugin-device-manager@npm:3.12.0-next.17": + version: 3.12.0-next.17 + resolution: "@webex/plugin-device-manager@npm:3.12.0-next.17" dependencies: - "@webex/internal-plugin-calendar": "npm:3.12.0-next.31" - "@webex/internal-plugin-device": "npm:3.12.0-next.29" - "@webex/internal-plugin-lyra": "npm:3.12.0-next.31" - "@webex/internal-plugin-search": "npm:3.12.0-next.31" - "@webex/plugin-authorization": "npm:3.12.0-next.29" - "@webex/plugin-logger": "npm:3.12.0-next.29" - "@webex/test-helper-chai": "npm:3.12.0-next.5" - "@webex/webex-core": "npm:3.12.0-next.29" + "@webex/internal-plugin-calendar": "npm:3.12.0-next.17" + "@webex/internal-plugin-device": "npm:3.12.0-next.15" + "@webex/internal-plugin-lyra": "npm:3.12.0-next.17" + "@webex/internal-plugin-search": "npm:3.12.0-next.17" + "@webex/plugin-authorization": "npm:3.12.0-next.15" + "@webex/plugin-logger": "npm:3.12.0-next.15" + "@webex/test-helper-chai": "npm:3.12.0-next.1" + "@webex/webex-core": "npm:3.12.0-next.15" lodash: "npm:^4.17.21" uuid: "npm:^3.3.2" - checksum: 10c0/32d72c835ea9d3b04fdb09d248ae1c01b1658568a1db0ad76629390e8f1a352a135a0631dc4e86ecd7f654ae8866a6f4ec58178519f02bf82dbf87a59bc26419 + checksum: 10c0/4aaa87651b9666ab9b6824938ddf2f2c00839aff0b5c6a9e89699f75b587107e0eaa6852fe9dd2bdb99b8f5e1c411dc5c6022ce56c8b8bb9a7a2ef7544182cf2 languageName: node linkType: hard -"@webex/plugin-encryption@npm:3.12.0-next.30": - version: 3.12.0-next.30 - resolution: "@webex/plugin-encryption@npm:3.12.0-next.30" +"@webex/plugin-encryption@npm:3.12.0-next.16": + version: 3.12.0-next.16 + resolution: "@webex/plugin-encryption@npm:3.12.0-next.16" dependencies: - "@webex/internal-plugin-encryption": "npm:3.12.0-next.30" - "@webex/webex-core": "npm:3.12.0-next.29" - checksum: 10c0/50d25e97261b71f47f773da6a240f8880471eb1cb999bea1ec7f29a757018f4159e623b13d4de001ac348dd03a97f840667d30ca9e932a985c815410a03e1bb4 + "@webex/internal-plugin-encryption": "npm:3.12.0-next.16" + "@webex/webex-core": "npm:3.12.0-next.15" + checksum: 10c0/36e987ef8439913fdead76cdfec4184f1d39bb96a38bf3e2eacde4162a3e170d92593ba3649b08488e99244c9cec037c56e2ee0ce7d72fa2b9b264eb6679b1d8 languageName: node linkType: hard @@ -11133,6 +11472,20 @@ __metadata: languageName: node linkType: hard +"@webex/plugin-logger@npm:3.12.0-next.15": + version: 3.12.0-next.15 + resolution: "@webex/plugin-logger@npm:3.12.0-next.15" + dependencies: + "@webex/common": "npm:3.12.0-next.1" + "@webex/test-helper-chai": "npm:3.12.0-next.1" + "@webex/test-helper-mocha": "npm:3.12.0-next.1" + "@webex/test-helper-mock-webex": "npm:3.12.0-next.1" + "@webex/webex-core": "npm:3.12.0-next.15" + lodash: "npm:^4.17.21" + checksum: 10c0/3db9830fb48a66caa3ed0c9a2a3548f6bf9c3b9b07cfd0eeda0cb090c71db42399bbbfc5747848a7069a1d75cf34f072a08adf277caa76e50fc490afeeb833af + languageName: node + linkType: hard + "@webex/plugin-logger@npm:3.12.0-next.29": version: 3.12.0-next.29 resolution: "@webex/plugin-logger@npm:3.12.0-next.29" @@ -11174,26 +11527,26 @@ __metadata: languageName: node linkType: hard -"@webex/plugin-meetings@npm:3.12.0-next.93": - version: 3.12.0-next.93 - resolution: "@webex/plugin-meetings@npm:3.12.0-next.93" - dependencies: - "@webex/common": "npm:3.12.0-next.5" - "@webex/internal-media-core": "npm:2.26.2" - "@webex/internal-plugin-conversation": "npm:3.12.0-next.31" - "@webex/internal-plugin-device": "npm:3.12.0-next.29" - "@webex/internal-plugin-llm": "npm:3.12.0-next.33" - "@webex/internal-plugin-mercury": "npm:3.12.0-next.30" - "@webex/internal-plugin-metrics": "npm:3.12.0-next.29" - "@webex/internal-plugin-support": "npm:3.12.0-next.31" - "@webex/internal-plugin-user": "npm:3.12.0-next.30" - "@webex/internal-plugin-voicea": "npm:3.12.0-next.33" - "@webex/media-helpers": "npm:3.12.0-next.10" - "@webex/plugin-people": "npm:3.12.0-next.30" - "@webex/plugin-rooms": "npm:3.12.0-next.31" +"@webex/plugin-meetings@npm:3.12.0-next.62": + version: 3.12.0-next.62 + resolution: "@webex/plugin-meetings@npm:3.12.0-next.62" + dependencies: + "@webex/common": "npm:3.12.0-next.1" + "@webex/internal-media-core": "npm:2.25.1" + "@webex/internal-plugin-conversation": "npm:3.12.0-next.17" + "@webex/internal-plugin-device": "npm:3.12.0-next.15" + "@webex/internal-plugin-llm": "npm:3.12.0-next.18" + "@webex/internal-plugin-mercury": "npm:3.12.0-next.16" + "@webex/internal-plugin-metrics": "npm:3.12.0-next.15" + "@webex/internal-plugin-support": "npm:3.12.0-next.17" + "@webex/internal-plugin-user": "npm:3.12.0-next.16" + "@webex/internal-plugin-voicea": "npm:3.12.0-next.18" + "@webex/media-helpers": "npm:3.12.0-next.4" + "@webex/plugin-people": "npm:3.12.0-next.16" + "@webex/plugin-rooms": "npm:3.12.0-next.17" "@webex/ts-sdp": "npm:^1.8.1" "@webex/web-capabilities": "npm:^1.10.0" - "@webex/webex-core": "npm:3.12.0-next.29" + "@webex/webex-core": "npm:3.12.0-next.15" ampersand-collection: "npm:^2.0.2" bowser: "npm:^2.11.0" btoa: "npm:^1.2.1" @@ -11207,7 +11560,7 @@ __metadata: uuid: "npm:^3.3.2" webrtc-adapter: "npm:^8.1.2" xxh3-ts: "npm:^2.0.1" - checksum: 10c0/b4d423b8c6055bd71045cc2071f9d80dd00c72ed27ba602faa5cd523943464332517e663192e4caa037532ec1b5f1d27e89e7c3d57ef55521ae5ab99529ea9ea + checksum: 10c0/65e30bde464ac5e2da09a6485d663323daee2ec26c528d761dd80b51566c277c338c80f949c28555e7329a5107534250692068c08ce546a5c4cfe15ff47e5ea4 languageName: node linkType: hard @@ -11229,22 +11582,22 @@ __metadata: languageName: node linkType: hard -"@webex/plugin-memberships@npm:3.12.0-next.31": - version: 3.12.0-next.31 - resolution: "@webex/plugin-memberships@npm:3.12.0-next.31" +"@webex/plugin-memberships@npm:3.12.0-next.17": + version: 3.12.0-next.17 + resolution: "@webex/plugin-memberships@npm:3.12.0-next.17" dependencies: - "@webex/common": "npm:3.12.0-next.5" - "@webex/internal-plugin-conversation": "npm:3.12.0-next.31" - "@webex/internal-plugin-mercury": "npm:3.12.0-next.30" - "@webex/webex-core": "npm:3.12.0-next.29" + "@webex/common": "npm:3.12.0-next.1" + "@webex/internal-plugin-conversation": "npm:3.12.0-next.17" + "@webex/internal-plugin-mercury": "npm:3.12.0-next.16" + "@webex/webex-core": "npm:3.12.0-next.15" debug: "npm:^4.3.4" lodash: "npm:^4.17.21" peerDependencies: - "@webex/plugin-logger": 3.12.0-next.29 - "@webex/plugin-messages": 3.12.0-next.31 - "@webex/plugin-people": 3.12.0-next.30 - "@webex/plugin-rooms": 3.12.0-next.31 - checksum: 10c0/6affaea58af92c7497befa5b2879ce18ce662c6dca65f53f6101ed2154e469d1991e41524c7aeeb5251792676520861a434ded7d50fcfb106ba71bdd74207e09 + "@webex/plugin-logger": 3.12.0-next.15 + "@webex/plugin-messages": 3.12.0-next.17 + "@webex/plugin-people": 3.12.0-next.16 + "@webex/plugin-rooms": 3.12.0-next.17 + checksum: 10c0/617e1eae28b9de947cd6ea33a779e88a1442b9a0de44290f28c9aae8a0b907bbbf14fe071cf373dce4dcb4f6d1368450269da2617a2b6ab330b240ac3b5ecbf6 languageName: node linkType: hard @@ -11266,22 +11619,22 @@ __metadata: languageName: node linkType: hard -"@webex/plugin-messages@npm:3.12.0-next.31": - version: 3.12.0-next.31 - resolution: "@webex/plugin-messages@npm:3.12.0-next.31" +"@webex/plugin-messages@npm:3.12.0-next.17": + version: 3.12.0-next.17 + resolution: "@webex/plugin-messages@npm:3.12.0-next.17" dependencies: - "@webex/common": "npm:3.12.0-next.5" - "@webex/internal-plugin-conversation": "npm:3.12.0-next.31" - "@webex/internal-plugin-device": "npm:3.12.0-next.29" - "@webex/internal-plugin-mercury": "npm:3.12.0-next.30" - "@webex/webex-core": "npm:3.12.0-next.29" + "@webex/common": "npm:3.12.0-next.1" + "@webex/internal-plugin-conversation": "npm:3.12.0-next.17" + "@webex/internal-plugin-device": "npm:3.12.0-next.15" + "@webex/internal-plugin-mercury": "npm:3.12.0-next.16" + "@webex/webex-core": "npm:3.12.0-next.15" debug: "npm:^4.3.4" lodash: "npm:^4.17.21" peerDependencies: - "@webex/plugin-logger": 3.12.0-next.29 - "@webex/plugin-people": 3.12.0-next.30 - "@webex/plugin-rooms": 3.12.0-next.31 - checksum: 10c0/b5aaf24c5abce295b8b35748cf6e6e19fbe4618e98ce8158f7cf0e20529a7b773ec4bdbc8f3fed5f5ed3b53b214d1bdae3dea79cc51b2f2c39744d3500a0d2a6 + "@webex/plugin-logger": 3.12.0-next.15 + "@webex/plugin-people": 3.12.0-next.16 + "@webex/plugin-rooms": 3.12.0-next.17 + checksum: 10c0/74015a95b1d35d9c360a16cffa712fcd5261405b7b5c4955ad1d2fd4b9bff0a5d196f5ea343edeb1aeb0a98042afc3f414911eb0dd84dc7db67077fb70fb724c languageName: node linkType: hard @@ -11296,14 +11649,14 @@ __metadata: languageName: node linkType: hard -"@webex/plugin-people@npm:3.12.0-next.30": - version: 3.12.0-next.30 - resolution: "@webex/plugin-people@npm:3.12.0-next.30" +"@webex/plugin-people@npm:3.12.0-next.16": + version: 3.12.0-next.16 + resolution: "@webex/plugin-people@npm:3.12.0-next.16" dependencies: - "@webex/common": "npm:3.12.0-next.5" - "@webex/internal-plugin-mercury": "npm:3.12.0-next.30" - "@webex/webex-core": "npm:3.12.0-next.29" - checksum: 10c0/590ceaa4185da7eb89b2fb5f631c1ea1bcd5ad8dc72cd5975842298ee95bd7f30237e8cb6fd3e534d4dcfba79d973ed67c4180e6cc220a7650c5e0f2e14f7ca2 + "@webex/common": "npm:3.12.0-next.1" + "@webex/internal-plugin-mercury": "npm:3.12.0-next.16" + "@webex/webex-core": "npm:3.12.0-next.15" + checksum: 10c0/328aedf95ae0cd8ebbd91b8424f0279551b6a66fdb8fde83d8bf4e130ecdc5e49aeb516e9d6a365c6b3a67e3e916b3352ff33ab5e1ed1c7622f4842508d055d4 languageName: node linkType: hard @@ -11325,20 +11678,20 @@ __metadata: languageName: node linkType: hard -"@webex/plugin-rooms@npm:3.12.0-next.31": - version: 3.12.0-next.31 - resolution: "@webex/plugin-rooms@npm:3.12.0-next.31" +"@webex/plugin-rooms@npm:3.12.0-next.17": + version: 3.12.0-next.17 + resolution: "@webex/plugin-rooms@npm:3.12.0-next.17" dependencies: - "@webex/common": "npm:3.12.0-next.5" - "@webex/internal-plugin-conversation": "npm:3.12.0-next.31" - "@webex/internal-plugin-mercury": "npm:3.12.0-next.30" - "@webex/webex-core": "npm:3.12.0-next.29" + "@webex/common": "npm:3.12.0-next.1" + "@webex/internal-plugin-conversation": "npm:3.12.0-next.17" + "@webex/internal-plugin-mercury": "npm:3.12.0-next.16" + "@webex/webex-core": "npm:3.12.0-next.15" debug: "npm:^4.3.4" lodash: "npm:^4.17.21" peerDependencies: - "@webex/plugin-logger": 3.12.0-next.29 - "@webex/plugin-people": 3.12.0-next.30 - checksum: 10c0/f4ce630d3d9108e974b75d5f3b6c87a5de3760811e354a24fd9d6f728704cc7666f71006fcbb745b4408caa1c399989de5efdf793fb3c39f693627cdfe0681cc + "@webex/plugin-logger": 3.12.0-next.15 + "@webex/plugin-people": 3.12.0-next.16 + checksum: 10c0/384483ea056ff2d2f7470521fc32743cb106264f0f830b3f79edce5f01dc7d47244e71189296ad2d15c76303e2b9428488f55ac0065a28a525aa02f5b3ad0525 languageName: node linkType: hard @@ -11355,17 +11708,17 @@ __metadata: languageName: node linkType: hard -"@webex/plugin-team-memberships@npm:3.12.0-next.29": - version: 3.12.0-next.29 - resolution: "@webex/plugin-team-memberships@npm:3.12.0-next.29" +"@webex/plugin-team-memberships@npm:3.12.0-next.15": + version: 3.12.0-next.15 + resolution: "@webex/plugin-team-memberships@npm:3.12.0-next.15" dependencies: - "@webex/internal-plugin-device": "npm:3.12.0-next.29" - "@webex/webex-core": "npm:3.12.0-next.29" + "@webex/internal-plugin-device": "npm:3.12.0-next.15" + "@webex/webex-core": "npm:3.12.0-next.15" peerDependencies: - "@webex/plugin-logger": 3.12.0-next.29 - "@webex/plugin-rooms": 3.12.0-next.31 - "@webex/plugin-teams": 3.12.0-next.29 - checksum: 10c0/477fd5edae889fc1070e89a1a54d20b096848d3f1f550451a0f2e357dba128452d77a14b2de60271a278caa9cb91d0d0a14f51633b2561fb759a525633bcd39d + "@webex/plugin-logger": 3.12.0-next.15 + "@webex/plugin-rooms": 3.12.0-next.17 + "@webex/plugin-teams": 3.12.0-next.15 + checksum: 10c0/dd6c8720f9daabad5e455b2782012559f605733e578e94b5d923d63380bc0ec6403d20c80c8ef05ba8fdd16937b76318c9a83c9dc7084cded0021985b109e603 languageName: node linkType: hard @@ -11385,18 +11738,18 @@ __metadata: languageName: node linkType: hard -"@webex/plugin-teams@npm:3.12.0-next.29": - version: 3.12.0-next.29 - resolution: "@webex/plugin-teams@npm:3.12.0-next.29" +"@webex/plugin-teams@npm:3.12.0-next.15": + version: 3.12.0-next.15 + resolution: "@webex/plugin-teams@npm:3.12.0-next.15" dependencies: - "@webex/internal-plugin-device": "npm:3.12.0-next.29" - "@webex/webex-core": "npm:3.12.0-next.29" + "@webex/internal-plugin-device": "npm:3.12.0-next.15" + "@webex/webex-core": "npm:3.12.0-next.15" lodash: "npm:^4.17.21" peerDependencies: - "@webex/plugin-logger": 3.12.0-next.29 - "@webex/plugin-memberships": 3.12.0-next.31 - "@webex/plugin-rooms": 3.12.0-next.31 - checksum: 10c0/3b9d08cbe521dffac19a303913ae8fe99456851c90a6e9f0be041fd07a0b91028388b01591f13508d7450e9302fa8df7669fa9d7dee71cfca7fedfb7c3b1f5e8 + "@webex/plugin-logger": 3.12.0-next.15 + "@webex/plugin-memberships": 3.12.0-next.17 + "@webex/plugin-rooms": 3.12.0-next.17 + checksum: 10c0/b37c81368bdc3079da07c8bfc0f6e9c5818b34fab02a05003cc5ede264e82a951e4d343cb2317d087b64cf58ec4dff4030d86b56c2b76c1b62e28078f06eda5a languageName: node linkType: hard @@ -11412,16 +11765,16 @@ __metadata: languageName: node linkType: hard -"@webex/plugin-webhooks@npm:3.12.0-next.29": - version: 3.12.0-next.29 - resolution: "@webex/plugin-webhooks@npm:3.12.0-next.29" +"@webex/plugin-webhooks@npm:3.12.0-next.15": + version: 3.12.0-next.15 + resolution: "@webex/plugin-webhooks@npm:3.12.0-next.15" dependencies: - "@webex/internal-plugin-device": "npm:3.12.0-next.29" - "@webex/webex-core": "npm:3.12.0-next.29" + "@webex/internal-plugin-device": "npm:3.12.0-next.15" + "@webex/webex-core": "npm:3.12.0-next.15" peerDependencies: - "@webex/plugin-logger": 3.12.0-next.29 - "@webex/plugin-rooms": 3.12.0-next.31 - checksum: 10c0/2daed1db75aec82a23442305d60adcf424210aaf32ddfbaca00e590703fc61d651fb9c2255327133af08e59e6f0125ce05d516513efb4fe51f9df8bfe0817487 + "@webex/plugin-logger": 3.12.0-next.15 + "@webex/plugin-rooms": 3.12.0-next.17 + checksum: 10c0/dd50f24056870b667686e96d7e578170eae030bd7cff69ca5c3ab3aaa5d54fd7e51d0c60903506b6f6980c7cfad720188a98fd6b6cb5459b46f359b11111e810 languageName: node linkType: hard @@ -11461,6 +11814,17 @@ __metadata: languageName: node linkType: hard +"@webex/storage-adapter-local-storage@npm:3.12.0-next.15": + version: 3.12.0-next.15 + resolution: "@webex/storage-adapter-local-storage@npm:3.12.0-next.15" + dependencies: + "@webex/storage-adapter-spec": "npm:3.12.0-next.1" + "@webex/test-helper-mocha": "npm:3.12.0-next.1" + "@webex/webex-core": "npm:3.12.0-next.15" + checksum: 10c0/211a82fcf5d06d2eb7c47c08538e4d23334354767d442b03219382aaef0d8537186850345d1b128b1f8014df82c7e06eee3bea88edeea97125fa113f83c4c5e2 + languageName: node + linkType: hard + "@webex/storage-adapter-local-storage@npm:3.12.0-next.29": version: 3.12.0-next.29 resolution: "@webex/storage-adapter-local-storage@npm:3.12.0-next.29" @@ -11490,6 +11854,15 @@ __metadata: languageName: node linkType: hard +"@webex/storage-adapter-spec@npm:3.12.0-next.1": + version: 3.12.0-next.1 + resolution: "@webex/storage-adapter-spec@npm:3.12.0-next.1" + dependencies: + "@webex/test-helper-chai": "npm:3.12.0-next.1" + checksum: 10c0/daaf7fa3eeae4749485d1f64eba9f2c20bbdaec1870873f6b393ec0f3dc6a8728c0dbf46c8969bb49c34208ad0cf56cbc5fde0575d3e1a786e8a744be2c57ea5 + languageName: node + linkType: hard + "@webex/storage-adapter-spec@npm:3.12.0-next.5": version: 3.12.0-next.5 resolution: "@webex/storage-adapter-spec@npm:3.12.0-next.5" @@ -11552,6 +11925,17 @@ __metadata: languageName: node linkType: hard +"@webex/test-helper-chai@npm:3.12.0-next.1": + version: 3.12.0-next.1 + resolution: "@webex/test-helper-chai@npm:3.12.0-next.1" + dependencies: + "@webex/test-helper-file": "npm:3.12.0-next.1" + check-error: "npm:^1.0.2" + lodash: "npm:^4.17.21" + checksum: 10c0/5d8139752365ed2dc3cd4ae790b5ca542d2c689108df414082a907a8bdad674293fffe49858b7cec64bac1085579b6d55f7c3aea40edefc4d6d23f17b3b46bd1 + languageName: node + linkType: hard + "@webex/test-helper-chai@npm:3.12.0-next.5": version: 3.12.0-next.5 resolution: "@webex/test-helper-chai@npm:3.12.0-next.5" @@ -11589,6 +11973,19 @@ __metadata: languageName: node linkType: hard +"@webex/test-helper-file@npm:3.12.0-next.1": + version: 3.12.0-next.1 + resolution: "@webex/test-helper-file@npm:3.12.0-next.1" + dependencies: + "@webex/common": "npm:3.12.0-next.1" + "@webex/test-helper-make-local-url": "npm:3.12.0-next.1" + es6-promise: "npm:^4.2.8" + file-type: "npm:^16.0.1" + xhr: "npm:^2.5.0" + checksum: 10c0/6ed8abaec3268ac09dafbb9da2e639c3b3f10f01ffe619c10e78b9834caf36b1fba55d4cab031ff55f6b159be40f1c61a3f5ac6e782f1ba626e6782392485643 + languageName: node + linkType: hard + "@webex/test-helper-file@npm:3.12.0-next.5": version: 3.12.0-next.5 resolution: "@webex/test-helper-file@npm:3.12.0-next.5" @@ -11616,6 +12013,13 @@ __metadata: languageName: node linkType: hard +"@webex/test-helper-make-local-url@npm:3.12.0-next.1": + version: 3.12.0-next.1 + resolution: "@webex/test-helper-make-local-url@npm:3.12.0-next.1" + checksum: 10c0/13dd73a17c3d241b02affcabf831828dcef0df8850240142159ce15b0970a66ca2caf5d4c40cf54d9fde27b61abcf86d40a0895069e3f8d0f79f769f5c952ab4 + languageName: node + linkType: hard + "@webex/test-helper-make-local-url@npm:3.12.0-next.5": version: 3.12.0-next.5 resolution: "@webex/test-helper-make-local-url@npm:3.12.0-next.5" @@ -11641,6 +12045,15 @@ __metadata: languageName: node linkType: hard +"@webex/test-helper-mocha@npm:3.12.0-next.1": + version: 3.12.0-next.1 + resolution: "@webex/test-helper-mocha@npm:3.12.0-next.1" + dependencies: + bowser: "npm:^2.11.0" + checksum: 10c0/3df1caa81e06216abcfeee71b340684dc43c85660c6f4b1ecf7db692cd8901496aac8e9572acdd20b1b443f4330e4a1ed7f1a0ae9f94e9f7f6123011762b42f1 + languageName: node + linkType: hard + "@webex/test-helper-mocha@npm:3.12.0-next.5": version: 3.12.0-next.5 resolution: "@webex/test-helper-mocha@npm:3.12.0-next.5" @@ -11664,6 +12077,13 @@ __metadata: languageName: node linkType: hard +"@webex/test-helper-mock-web-socket@npm:3.12.0-next.1": + version: 3.12.0-next.1 + resolution: "@webex/test-helper-mock-web-socket@npm:3.12.0-next.1" + checksum: 10c0/a572fa41d1420a36d57fc57b9705297dff5c62fb479ef6b7433ce56102b3e331f894dad679a04ec98cf4c45f8007cbc4129a07036dc9d4f5a11b0f40a18adc5c + languageName: node + linkType: hard + "@webex/test-helper-mock-web-socket@npm:3.12.0-next.5": version: 3.12.0-next.5 resolution: "@webex/test-helper-mock-web-socket@npm:3.12.0-next.5" @@ -11693,6 +12113,17 @@ __metadata: languageName: node linkType: hard +"@webex/test-helper-mock-webex@npm:3.12.0-next.1": + version: 3.12.0-next.1 + resolution: "@webex/test-helper-mock-webex@npm:3.12.0-next.1" + dependencies: + ampersand-state: "npm:^5.0.3" + es6-promise: "npm:^4.2.8" + lodash: "npm:^4.17.21" + checksum: 10c0/d422edd71d6ac2031215ab479e409686526324698e96fb0baaf588047779e33fa138273c213716c4937f78b254646b74aa8fc08d663b0aeebb02439e4804f88a + languageName: node + linkType: hard + "@webex/test-helper-mock-webex@npm:3.12.0-next.5": version: 3.12.0-next.5 resolution: "@webex/test-helper-mock-webex@npm:3.12.0-next.5" @@ -11718,6 +12149,13 @@ __metadata: languageName: node linkType: hard +"@webex/test-helper-refresh-callback@npm:3.12.0-next.1": + version: 3.12.0-next.1 + resolution: "@webex/test-helper-refresh-callback@npm:3.12.0-next.1" + checksum: 10c0/3144709abb36533e76891052e8d805c8377129a099a5972f2f2dad99cff163b555d683babc5a5cb1316fe92d6ba9588e890121c67df3355e47137dab5d7829a0 + languageName: node + linkType: hard + "@webex/test-helper-refresh-callback@npm:3.12.0-next.5": version: 3.12.0-next.5 resolution: "@webex/test-helper-refresh-callback@npm:3.12.0-next.5" @@ -11743,6 +12181,15 @@ __metadata: languageName: node linkType: hard +"@webex/test-helper-retry@npm:3.12.0-next.1": + version: 3.12.0-next.1 + resolution: "@webex/test-helper-retry@npm:3.12.0-next.1" + dependencies: + es6-promise: "npm:^4.2.8" + checksum: 10c0/0a94cef8493b53a43c42187ca4cfa6e4fefd569203c97018b59f93c99225f9ff07e24ecce150f231e552c948789925c82abc7aa68a9a96e8aa59967f162126fc + languageName: node + linkType: hard + "@webex/test-helper-retry@npm:3.12.0-next.5": version: 3.12.0-next.5 resolution: "@webex/test-helper-retry@npm:3.12.0-next.5" @@ -11778,6 +12225,17 @@ __metadata: languageName: node linkType: hard +"@webex/test-helper-test-users@npm:3.12.0-next.1": + version: 3.12.0-next.1 + resolution: "@webex/test-helper-test-users@npm:3.12.0-next.1" + dependencies: + "@webex/test-helper-retry": "npm:3.12.0-next.1" + "@webex/test-users": "npm:3.12.0-next.1" + lodash: "npm:^4.17.21" + checksum: 10c0/e4f9f44c1cc88042fa6b8b5d1f0b8379158fac09cb5cdc4abd2d5c656b0f4bc863b16baea885f0f4aaea9aa7aa8e10663268490d85528f9e0fa1d3e819d11198 + languageName: node + linkType: hard + "@webex/test-helper-test-users@npm:3.12.0-next.5": version: 3.12.0-next.5 resolution: "@webex/test-helper-test-users@npm:3.12.0-next.5" @@ -11817,6 +12275,20 @@ __metadata: languageName: node linkType: hard +"@webex/test-users@npm:3.12.0-next.1": + version: 3.12.0-next.1 + resolution: "@webex/test-users@npm:3.12.0-next.1" + dependencies: + "@webex/http-core": "npm:3.12.0-next.1" + "@webex/test-helper-mocha": "npm:3.12.0-next.1" + btoa: "npm:^1.2.1" + lodash: "npm:^4.17.21" + node-random-name: "npm:^1.0.1" + uuid: "npm:^3.3.2" + checksum: 10c0/29ba5766b6c150ec3d1ec908dc4078816dedae21a773b056102fafe7ff474856573dfb53885e63475fdac3a35900827224578e24588d75127ebde5f00405ba68 + languageName: node + linkType: hard + "@webex/test-users@npm:3.12.0-next.5": version: 3.12.0-next.5 resolution: "@webex/test-users@npm:3.12.0-next.5" @@ -11905,6 +12377,25 @@ __metadata: languageName: node linkType: hard +"@webex/web-client-media-engine@npm:3.39.12": + version: 3.39.12 + resolution: "@webex/web-client-media-engine@npm:3.39.12" + dependencies: + "@webex/json-multistream": "npm:^2.4.3" + "@webex/rtcstats": "npm:^1.5.5" + "@webex/ts-events": "npm:^1.2.1" + "@webex/ts-sdp": "npm:1.8.2" + "@webex/web-capabilities": "npm:^1.10.0" + "@webex/web-media-effects": "npm:2.33.5" + "@webex/webrtc-core": "npm:2.13.7" + async: "npm:^3.2.4" + js-logger: "npm:^1.6.1" + typed-emitter: "npm:^2.1.0" + uuid: "npm:^8.3.2" + checksum: 10c0/16d7d144ff7f2a95c5d407481d2c9871910c1d2cd20350837e019f86c36be2fe27fe88ee5b182c003274777b6298b898193d88c5e8df5309379ec8b9edd306f7 + languageName: node + linkType: hard + "@webex/web-client-media-engine@npm:3.40.2": version: 3.40.2 resolution: "@webex/web-client-media-engine@npm:3.40.2" @@ -11981,6 +12472,26 @@ __metadata: languageName: node linkType: hard +"@webex/webex-core@npm:3.12.0-next.15": + version: 3.12.0-next.15 + resolution: "@webex/webex-core@npm:3.12.0-next.15" + dependencies: + "@webex/common": "npm:3.12.0-next.1" + "@webex/common-timers": "npm:3.12.0-next.1" + "@webex/http-core": "npm:3.12.0-next.1" + "@webex/storage-adapter-spec": "npm:3.12.0-next.1" + ampersand-collection: "npm:^2.0.2" + ampersand-events: "npm:^2.0.2" + ampersand-state: "npm:^5.0.3" + core-decorators: "npm:^0.20.0" + crypto-js: "npm:^4.1.1" + jsonwebtoken: "npm:^9.0.0" + lodash: "npm:^4.17.21" + uuid: "npm:^3.3.2" + checksum: 10c0/295175e66e511f3d63649baba8cbf325a10b4f6adeddbfbfb2a7a483ee68c25f0730ea13a5c6e3c90ec5540d4cb94cc8d90083bbff14ac7d201b5217a60dbe80 + languageName: node + linkType: hard + "@webex/webex-core@npm:3.12.0-next.29": version: 3.12.0-next.29 resolution: "@webex/webex-core@npm:3.12.0-next.29" @@ -12001,6 +12512,21 @@ __metadata: languageName: node linkType: hard +"@webex/webrtc-core@npm:2.13.7": + version: 2.13.7 + resolution: "@webex/webrtc-core@npm:2.13.7" + dependencies: + "@webex/ts-events": "npm:^1.2.1" + "@webex/web-capabilities": "npm:^1.6.1" + "@webex/web-media-effects": "npm:2.33.5" + events: "npm:^3.3.0" + js-logger: "npm:^1.6.1" + typed-emitter: "npm:^2.1.0" + webrtc-adapter: "npm:^8.1.2" + checksum: 10c0/48f781ca9d57330b6498e155e2b46bd914773dfc8ce21df80d0d79bf902a224ca4961f848999a3d1b779b49aef8ce7e797374e9c42eec016f88cd2fdc433895a + languageName: node + linkType: hard + "@webex/webrtc-core@npm:2.14.0": version: 2.14.0 resolution: "@webex/webrtc-core@npm:2.14.0" @@ -30643,7 +31169,7 @@ __metadata: ts-loader: "npm:^9.5.1" typescript: "npm:^5.6.3" typescript-eslint: "npm:^8.24.1" - webex: "npm:3.12.0-next.138" + webex: "npm:3.12.0-next.84" webpack: "npm:^5.94.0" webpack-cli: "npm:^5.1.4" webpack-dev-server: "npm:^5.1.0" @@ -34770,42 +35296,42 @@ __metadata: languageName: node linkType: hard -"webex@npm:3.12.0-next.138": - version: 3.12.0-next.138 - resolution: "webex@npm:3.12.0-next.138" +"webex@npm:3.12.0-next.84": + version: 3.12.0-next.84 + resolution: "webex@npm:3.12.0-next.84" dependencies: "@babel/polyfill": "npm:^7.12.1" "@babel/runtime-corejs2": "npm:^7.14.8" - "@webex/calling": "npm:3.12.0-next.71" - "@webex/common": "npm:3.12.0-next.5" - "@webex/contact-center": "npm:3.12.0-next.81" - "@webex/internal-plugin-calendar": "npm:3.12.0-next.31" - "@webex/internal-plugin-device": "npm:3.12.0-next.29" - "@webex/internal-plugin-dss": "npm:3.12.0-next.30" - "@webex/internal-plugin-llm": "npm:3.12.0-next.33" - "@webex/internal-plugin-mercury": "npm:3.12.0-next.30" - "@webex/internal-plugin-presence": "npm:3.12.0-next.30" - "@webex/internal-plugin-support": "npm:3.12.0-next.31" - "@webex/internal-plugin-task": "npm:^3.12.0-next.31" - "@webex/internal-plugin-voicea": "npm:3.12.0-next.33" - "@webex/plugin-attachment-actions": "npm:3.12.0-next.31" - "@webex/plugin-authorization": "npm:3.12.0-next.29" - "@webex/plugin-device-manager": "npm:3.12.0-next.31" - "@webex/plugin-encryption": "npm:3.12.0-next.30" - "@webex/plugin-logger": "npm:3.12.0-next.29" - "@webex/plugin-meetings": "npm:3.12.0-next.93" - "@webex/plugin-memberships": "npm:3.12.0-next.31" - "@webex/plugin-messages": "npm:3.12.0-next.31" - "@webex/plugin-people": "npm:3.12.0-next.30" - "@webex/plugin-rooms": "npm:3.12.0-next.31" - "@webex/plugin-team-memberships": "npm:3.12.0-next.29" - "@webex/plugin-teams": "npm:3.12.0-next.29" - "@webex/plugin-webhooks": "npm:3.12.0-next.29" - "@webex/storage-adapter-local-storage": "npm:3.12.0-next.29" - "@webex/webex-core": "npm:3.12.0-next.29" + "@webex/calling": "npm:3.12.0-next.34" + "@webex/common": "npm:3.12.0-next.1" + "@webex/contact-center": "npm:3.12.0-next.42" + "@webex/internal-plugin-calendar": "npm:3.12.0-next.17" + "@webex/internal-plugin-device": "npm:3.12.0-next.15" + "@webex/internal-plugin-dss": "npm:3.12.0-next.16" + "@webex/internal-plugin-llm": "npm:3.12.0-next.18" + "@webex/internal-plugin-mercury": "npm:3.12.0-next.16" + "@webex/internal-plugin-presence": "npm:3.12.0-next.16" + "@webex/internal-plugin-support": "npm:3.12.0-next.17" + "@webex/internal-plugin-task": "npm:^3.12.0-next.17" + "@webex/internal-plugin-voicea": "npm:3.12.0-next.18" + "@webex/plugin-attachment-actions": "npm:3.12.0-next.17" + "@webex/plugin-authorization": "npm:3.12.0-next.15" + "@webex/plugin-device-manager": "npm:3.12.0-next.17" + "@webex/plugin-encryption": "npm:3.12.0-next.16" + "@webex/plugin-logger": "npm:3.12.0-next.15" + "@webex/plugin-meetings": "npm:3.12.0-next.62" + "@webex/plugin-memberships": "npm:3.12.0-next.17" + "@webex/plugin-messages": "npm:3.12.0-next.17" + "@webex/plugin-people": "npm:3.12.0-next.16" + "@webex/plugin-rooms": "npm:3.12.0-next.17" + "@webex/plugin-team-memberships": "npm:3.12.0-next.15" + "@webex/plugin-teams": "npm:3.12.0-next.15" + "@webex/plugin-webhooks": "npm:3.12.0-next.15" + "@webex/storage-adapter-local-storage": "npm:3.12.0-next.15" + "@webex/webex-core": "npm:3.12.0-next.15" lodash: "npm:^4.17.21" safe-buffer: "npm:^5.2.0" - checksum: 10c0/8b1fc29e12b0af440c11555813443c103c56b34165439181c18ec0cef160f4efa9452942cebd902b1981e6596cd9e822963424baf8b2321f231315a488cc8402 + checksum: 10c0/2498002ee3e82e639209852eabb942bdabfae305d63e9cc0f5d3b2a015110c18f1e8edd198a6b095a7058eac96c947ee8df8d997d8d8434334ea0fa066be57c3 languageName: node linkType: hard From 440fa9aef626d9025fb9a67749d0242698c27071 Mon Sep 17 00:00:00 2001 From: Matthew Olker Date: Wed, 15 Jul 2026 11:13:30 -0500 Subject: [PATCH 04/14] fix(e911-modal): address Codex review feedback - Wire Momentum Checkbox via 'onchange' prop instead of React 'onChange' (custom element does not fire React's synthetic change event, leaving Save & Continue permanently disabled) - Sync native dialog 'cancel' event (Escape key) back to onCancel so store state (showE911Modal) stays consistent with dialog visibility - Reset isEmergencyModalAlreadyDisplayed to false whenever desktopPreference is missing/unparsable, instead of leaving the singleton's stale value, which could cause the modal to be skipped for a new agent/session - Throw instead of silently resolving in fetchUserPreferences and updateEmergencyModalAcknowledgment when userPreference service is unavailable, so failures are surfaced instead of leaving the modal stuck open with no way to persist acknowledgment - Add regression tests for native cancel handling and preference reset Addresses review comments on PR #719 --- .../StationLogin/E911Modal/e911-modal.tsx | 30 +++++++++++++------ .../E911Modal/e911-modal.test.tsx | 11 +++++++ .../store/src/storeEventsWrapper.ts | 14 +++++---- .../store/tests/storeEventsWrapper.ts | 27 ++++++++++++++--- 4 files changed, 64 insertions(+), 18 deletions(-) diff --git a/packages/contact-center/cc-components/src/components/StationLogin/E911Modal/e911-modal.tsx b/packages/contact-center/cc-components/src/components/StationLogin/E911Modal/e911-modal.tsx index 1db99c012..0ad5b4062 100644 --- a/packages/contact-center/cc-components/src/components/StationLogin/E911Modal/e911-modal.tsx +++ b/packages/contact-center/cc-components/src/components/StationLogin/E911Modal/e911-modal.tsx @@ -8,6 +8,15 @@ const E911Modal: React.FC = ({isOpen, onSaveAndContinue, onCance const dialogRef = useRef(null); const [isChecked, setIsChecked] = useState(false); + const handleCheckboxChange = () => { + setIsChecked(!isChecked); + }; + + const handleCancel = () => { + setIsChecked(false); + onCancel(); + }; + useEffect(() => { const dialog = dialogRef.current; if (!dialog) return; @@ -22,16 +31,18 @@ const E911Modal: React.FC = ({isOpen, onSaveAndContinue, onCance } setIsChecked(false); } - }, [isOpen]); - const handleCheckboxChange = () => { - setIsChecked(!isChecked); - }; + const handleNativeCancel = (event: Event) => { + event.preventDefault(); + handleCancel(); + }; - const handleCancel = () => { - setIsChecked(false); - onCancel(); - }; + dialog.addEventListener('cancel', handleNativeCancel); + + return () => { + dialog.removeEventListener('cancel', handleNativeCancel); + }; + }, [isOpen, onCancel]); const handleSaveAndContinue = () => { if (isChecked) { @@ -88,7 +99,8 @@ const E911Modal: React.FC = ({isOpen, onSaveAndContinue, onCance diff --git a/packages/contact-center/cc-components/tests/components/StationLogin/E911Modal/e911-modal.test.tsx b/packages/contact-center/cc-components/tests/components/StationLogin/E911Modal/e911-modal.test.tsx index 9bb64fecc..3d5e743f6 100644 --- a/packages/contact-center/cc-components/tests/components/StationLogin/E911Modal/e911-modal.test.tsx +++ b/packages/contact-center/cc-components/tests/components/StationLogin/E911Modal/e911-modal.test.tsx @@ -126,4 +126,15 @@ describe('E911Modal', () => { expect(screen.getByTestId('e911-help-link')).toBeInTheDocument(); expect(screen.getByText(E911ModalLabels.HELP_LINK_TEXT)).toBeInTheDocument(); }); + + it('should call onCancel when the native dialog is dismissed via the cancel event (e.g. Escape key)', () => { + render(); + const dialog = screen.getByTestId('e911-modal'); + + const cancelEvent = new Event('cancel', {cancelable: true}); + dialog.dispatchEvent(cancelEvent); + + expect(cancelEvent.defaultPrevented).toBe(true); + expect(defaultProps.onCancel).toHaveBeenCalledTimes(1); + }); }); diff --git a/packages/contact-center/store/src/storeEventsWrapper.ts b/packages/contact-center/store/src/storeEventsWrapper.ts index d21e88542..f5a9a00b6 100644 --- a/packages/contact-center/store/src/storeEventsWrapper.ts +++ b/packages/contact-center/store/src/storeEventsWrapper.ts @@ -608,18 +608,18 @@ class StoreWrapper implements IStoreWrapper { module: 'storeEventsWrapper.ts', method: 'fetchUserPreferences', }); - return; + throw new Error('userPreference service not available'); } const response = await this.store.cc.userPreference.getUserPreference(); const desktopPrefString = response?.desktopPreference; + let isEmergencyModalAlreadyDisplayed = false; + if (desktopPrefString) { try { const desktopPref = JSON.parse(desktopPrefString); - runInAction(() => { - this.store.isEmergencyModalAlreadyDisplayed = desktopPref.isEmergencyModalAlreadyDisplayed ?? false; - }); + isEmergencyModalAlreadyDisplayed = desktopPref.isEmergencyModalAlreadyDisplayed ?? false; } catch (parseError) { this.store.logger.error('CC-Widgets: fetchUserPreferences(): failed to parse desktopPreference', { module: 'storeEventsWrapper.ts', @@ -628,6 +628,10 @@ class StoreWrapper implements IStoreWrapper { }); } } + + runInAction(() => { + this.store.isEmergencyModalAlreadyDisplayed = isEmergencyModalAlreadyDisplayed; + }); } catch (error) { this.store.logger.error('CC-Widgets: fetchUserPreferences(): failed to fetch user preferences', { module: 'storeEventsWrapper.ts', @@ -648,7 +652,7 @@ class StoreWrapper implements IStoreWrapper { method: 'updateEmergencyModalAcknowledgment', } ); - return; + throw new Error('userPreference service not available'); } const desktopPreference = JSON.stringify({ diff --git a/packages/contact-center/store/tests/storeEventsWrapper.ts b/packages/contact-center/store/tests/storeEventsWrapper.ts index 2107b1d33..8abe3f8e5 100644 --- a/packages/contact-center/store/tests/storeEventsWrapper.ts +++ b/packages/contact-center/store/tests/storeEventsWrapper.ts @@ -2572,10 +2572,10 @@ describe('storeEventsWrapper', () => { }); describe('fetchUserPreferences', () => { - it('should return early if userPreference service is not available', async () => { + it('should throw and warn if userPreference service is not available', async () => { storeWrapper['store'].cc.userPreference = undefined; - await storeWrapper.fetchUserPreferences(); + await expect(storeWrapper.fetchUserPreferences()).rejects.toThrow('userPreference service not available'); expect(storeWrapper['store'].logger.warn).toHaveBeenCalledWith( 'CC-Widgets: fetchUserPreferences(): userPreference service not available', @@ -2610,6 +2610,22 @@ describe('storeEventsWrapper', () => { await storeWrapper.fetchUserPreferences(); expect(mockUserPreference.getUserPreference).toHaveBeenCalled(); + expect(storeWrapper['store'].isEmergencyModalAlreadyDisplayed).toBe(false); + }); + + it('should reset isEmergencyModalAlreadyDisplayed to false when desktopPreference is missing, even if previously true', async () => { + storeWrapper['store'].isEmergencyModalAlreadyDisplayed = true; + const mockUserPreference = { + getUserPreference: jest.fn().mockResolvedValue({ + desktopPreference: null, + }), + updateUserPreference: jest.fn(), + }; + storeWrapper['store'].cc.userPreference = mockUserPreference; + + await storeWrapper.fetchUserPreferences(); + + expect(storeWrapper['store'].isEmergencyModalAlreadyDisplayed).toBe(false); }); it('should handle parse error gracefully', async () => { @@ -2627,6 +2643,7 @@ describe('storeEventsWrapper', () => { 'CC-Widgets: fetchUserPreferences(): failed to parse desktopPreference', expect.any(Object) ); + expect(storeWrapper['store'].isEmergencyModalAlreadyDisplayed).toBe(false); }); it('should throw error on API failure', async () => { @@ -2642,10 +2659,12 @@ describe('storeEventsWrapper', () => { }); describe('updateEmergencyModalAcknowledgment', () => { - it('should return early if userPreference service is not available', async () => { + it('should throw and warn if userPreference service is not available', async () => { storeWrapper['store'].cc.userPreference = undefined; - await storeWrapper.updateEmergencyModalAcknowledgment(); + await expect(storeWrapper.updateEmergencyModalAcknowledgment()).rejects.toThrow( + 'userPreference service not available' + ); expect(storeWrapper['store'].logger.warn).toHaveBeenCalledWith( 'CC-Widgets: updateEmergencyModalAcknowledgment(): userPreference service not available', From f0d949d35531a3878bc72200f4cb04183a19e522 Mon Sep 17 00:00:00 2001 From: Matthew Olker Date: Mon, 20 Jul 2026 09:12:26 -0500 Subject: [PATCH 05/14] fix(e911-modal): address remaining PR #719 review feedback - Merge E911 acknowledgment flag into existing desktopPreference instead of overwriting it (was deleting sibling preference fields) - Use rem units and Momentum theme color tokens in e911-modal.style.scss instead of hardcoded px/hex; drop redundant font-size/weight overrides already handled by the Text component's type prop - Extract 'BROWSER' string literal to the shared DEVICE_TYPE_BROWSER constant from @webex/cc-store - Remove placeholder help link (href="#") until a real destination exists - Add unit tests for checkE911ModalDisplay() covering BROWSER/non-BROWSER device types, already-acknowledged preference, and fetchUserPreferences failure handling - Add regression tests for desktopPreference merge behavior --- ai-docs/RULES.md | 49 +++++++-- ai-docs/rules/css-rem-units.md | 78 +++++++++++++ .../E911Modal/e911-modal.constants.ts | 1 - .../E911Modal/e911-modal.style.scss | 64 ++++------- .../StationLogin/E911Modal/e911-modal.tsx | 3 - .../E911Modal/e911-modal.test.tsx | 6 - .../station-login/src/helper.ts | 8 +- .../station-login/tests/helper.ts | 104 ++++++++++++++++++ .../store/src/storeEventsWrapper.ts | 20 ++++ .../store/tests/storeEventsWrapper.ts | 38 +++++++ 10 files changed, 304 insertions(+), 67 deletions(-) create mode 100644 ai-docs/rules/css-rem-units.md diff --git a/ai-docs/RULES.md b/ai-docs/RULES.md index d82984c4c..a1f52dc6f 100644 --- a/ai-docs/RULES.md +++ b/ai-docs/RULES.md @@ -10,53 +10,72 @@ Coverage state is mirrored from `.sdd/manifest.json`. Every module is currently `DRAFT`: specs were generated fresh during the SDLC migration (2026-06-29) and have not been validated. **Cross-check code before relying on any spec claim** (drift tolerance ≤ 25%, see below). -| Module | Manifest coverage state | What it means here | -|---|---|---| -| `store` (`@webex/cc-store`, `packages/contact-center/store`) | DRAFT | Tier-1, sole SDK access point (`store.ts` `getInstance`). Spec `store/ai-docs/store-spec.md` is unvalidated — verify observables/SDK proxying against `store/src/store.ts` and `store/src/storeEventsWrapper.ts`. | -| `cc-components` (`@webex/cc-components`, `packages/contact-center/cc-components`) | DRAFT | Tier-1 presentational primitives. Verify prop contracts against `cc-components/src/` before trusting the spec. | -| `cc-widgets` (`@webex/cc-widgets`, `packages/contact-center/cc-widgets`) | DRAFT | Tier-1 r2wc aggregator. The custom-element registry lives in `cc-widgets/src/wc.ts` — cross-check element names/attrs there. | -| `cc-digital-channels` (`@webex/cc-digital-channels`, `packages/contact-center/cc-digital-channels`) | DRAFT | Tier-1 digital-channels widget (`widget-cc-digital-channels`). Verify against `cc-digital-channels/src/`. | -| `station-login` (`@webex/cc-station-login`, `packages/contact-center/station-login`) | DRAFT | Tier-1 widget. Verify against `station-login/src/`. | -| `user-state` (`@webex/cc-user-state`, `packages/contact-center/user-state`) | DRAFT | Tier-1 widget. Verify state/idle-code logic against `user-state/src/helper.ts`. | -| `task` (`@webex/cc-task`, `packages/contact-center/task`) | DRAFT | Tier-1 bundle of sub-widgets CallControl, CallControlCAD, IncomingTask, OutdialCall, TaskList. Verify per-widget behavior against `task/src/{Widget}/index.tsx` and `task/src/helper.ts`. | -| `ui-logging` (`@webex/cc-ui-logging`, `packages/contact-center/ui-logging`) | DRAFT | Tier-2 telemetry. `withMetrics` HOC + `metricsLogger` in `ui-logging/src/`. | -| `test-fixtures` (`@webex/test-fixtures`, `packages/contact-center/test-fixtures`) | DRAFT | Tier-2 shared mocks/helpers in `test-fixtures/src/`. | -| `meetings-widgets` (`@webex/widgets`, `packages/@webex/widgets`) | DRAFT | Tier-2 legacy meetings family, separate from the CC widget family. Out of scope for CC rules below unless explicitly named. | +| Module | Manifest coverage state | What it means here | +| --------------------------------------------------------------------------------------------------- | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `store` (`@webex/cc-store`, `packages/contact-center/store`) | DRAFT | Tier-1, sole SDK access point (`store.ts` `getInstance`). Spec `store/ai-docs/store-spec.md` is unvalidated — verify observables/SDK proxying against `store/src/store.ts` and `store/src/storeEventsWrapper.ts`. | +| `cc-components` (`@webex/cc-components`, `packages/contact-center/cc-components`) | DRAFT | Tier-1 presentational primitives. Verify prop contracts against `cc-components/src/` before trusting the spec. | +| `cc-widgets` (`@webex/cc-widgets`, `packages/contact-center/cc-widgets`) | DRAFT | Tier-1 r2wc aggregator. The custom-element registry lives in `cc-widgets/src/wc.ts` — cross-check element names/attrs there. | +| `cc-digital-channels` (`@webex/cc-digital-channels`, `packages/contact-center/cc-digital-channels`) | DRAFT | Tier-1 digital-channels widget (`widget-cc-digital-channels`). Verify against `cc-digital-channels/src/`. | +| `station-login` (`@webex/cc-station-login`, `packages/contact-center/station-login`) | DRAFT | Tier-1 widget. Verify against `station-login/src/`. | +| `user-state` (`@webex/cc-user-state`, `packages/contact-center/user-state`) | DRAFT | Tier-1 widget. Verify state/idle-code logic against `user-state/src/helper.ts`. | +| `task` (`@webex/cc-task`, `packages/contact-center/task`) | DRAFT | Tier-1 bundle of sub-widgets CallControl, CallControlCAD, IncomingTask, OutdialCall, TaskList. Verify per-widget behavior against `task/src/{Widget}/index.tsx` and `task/src/helper.ts`. | +| `ui-logging` (`@webex/cc-ui-logging`, `packages/contact-center/ui-logging`) | DRAFT | Tier-2 telemetry. `withMetrics` HOC + `metricsLogger` in `ui-logging/src/`. | +| `test-fixtures` (`@webex/test-fixtures`, `packages/contact-center/test-fixtures`) | DRAFT | Tier-2 shared mocks/helpers in `test-fixtures/src/`. | +| `meetings-widgets` (`@webex/widgets`, `packages/@webex/widgets`) | DRAFT | Tier-2 legacy meetings family, separate from the CC widget family. Out of scope for CC rules below unless explicitly named. | ## Autonomy & Ask-First + - **May proceed:** read-only research; bug fixes and feature work scoped to a single CC widget package that follow the established Widget → Hook → Component → Store flow; adding unit/E2E tests; doc edits under `ai-docs/`; copy/string and styling tweaks. - **Ask first / plan + confirm:** changes to the dependency direction or to `cc-widgets/src/wc.ts` custom-element names/attributes (a public contract); changes to the store's observable shape or SDK access surface in `store/src/`; new third-party dependencies; touching the `@webex/widgets` legacy meetings family. - **Never without explicit human approval:** `git push`, opening/merging PRs, or any deploy. PRs target the `next` branch and are draft by default. ## Naming + Grounded in `patterns/typescript-patterns.md` and the real code: + - Interfaces are prefixed with `I` and PascalCase: `IUserState`, `IStationLoginProps`, `IContactCenter` (`store/src/store.types.ts`, `task/src/task.types.ts`). Never an un-prefixed `UserState` interface. - Components are PascalCase in `.tsx` files: `UserState.tsx`, `CallControl/index.tsx`. Hooks are camelCase with a `use` prefix in `.ts` files: `useUserState`, `useCallControl` (`*/src/helper.ts`). - Types are co-located in `{name}.types.ts` (e.g. `user-state/src/user-state.types.ts`); derive subsets with `Pick`/`Partial` rather than re-declaring (e.g. `IUserStateProps = Pick`). - Event/state names are enums in SCREAMING_SNAKE_CASE values, e.g. `CC_EVENTS.AGENT_STATE_CHANGED`, `TASK_EVENTS.TASK_INCOMING`. Constants are SCREAMING_SNAKE_CASE. ## Logging + - Use the `ui-logging` helpers — `withMetrics` HOC and `metricsLogger` (`ui-logging/src/index.ts`, `ui-logging/src/metricsLogger.ts`) — and the store `logger` passed into hooks. Calls carry a structured context object `{module, method}` (see `task/src/helper.ts` `loadBuddyAgents`: `logger.info('Loaded N buddy agents', {module: 'helper.ts', method: 'loadBuddyAgents'})`). - **NEVER log PII or credentials** (agent identifiers in sensitive contexts, dial numbers, tokens, session secrets). Severity: high. Verification: review + grep for new `console.*`/`logger.*` calls in a diff. See Security below. - Prefer the injected `logger` over bare `console.*`; `console.error` is tolerated only inside hook catch blocks where no logger is available. ## Error Handling + - Every widget MUST be wrapped in an `ErrorBoundary` (`react-error-boundary`) at its exported boundary, with `onError` routing to `store.onErrorCallback?.('WidgetName', error)` and a non-throwing fallback (`patterns/react-patterns.md`; pattern realized across `*/src/{Widget}/index.tsx`). Severity: high. Verification: review only. - SDK/async calls in hooks (`helper.ts`) MUST be wrapped in `try/catch`; on failure, log via the injected `logger` and invoke the optional `onError`-style callback rather than letting the rejection escape. Never swallow an error silently. - Surface user-friendly errors in the presentational component (loading/error/empty states); never leak raw SDK errors or stack traces to the UI. +## Styling + +- In `*.style.scss` / `*.scss` files, use `rem` (not `px`) for spacing, sizing, border-radius, border-width, + and box-shadow offsets, and Momentum theme tokens (`var(--mds-color-theme-*)`) for color instead of hex + codes; don't hardcode `font-size`/`font-weight` when the Momentum `Text` component's `type` prop already + controls it. Exceptions: media-query breakpoints and pixel-perfect image/SVG asset dimensions may stay + `px`. See [`rules/css-rem-units.md`](rules/css-rem-units.md). Severity: low. Verification: review only + (no Stylelint config in this repo yet). + ## Imports / Dependencies + **Dependency flow is one direction only** (`.sdd/manifest.json`; enforced by review, source: legacy task-router rule "Circular Dependency Prevention"): + ``` cc-widgets → widget packages (station-login, user-state, task) → cc-components → store → @webex/contact-center SDK ``` + - A widget package MUST NOT import from `@webex/cc-widgets`. `cc-components` MUST NOT import from any widget package. No package imports upstream. Severity: high — if a circular import is detected, STOP and refactor. - Access the SDK ONLY through the store: `store.cc.methodName()` / `store.getBuddyAgents()` (`task/src/helper.ts:519`). NEVER `import ... from '@webex/contact-center'` in widget or component code. Severity: high. - Import the store as the singleton default export: `import store from '@webex/cc-store'`. Never `new Store()`; the instance comes from `Store.getInstance()` (`store/src/store.ts:64`). - New third-party dependencies require maintainer approval (ask-first). ## Testing + Grounded in `patterns/testing-patterns.md`: + - Unit/component tests use Jest + React Testing Library and live in each package's `tests/` folder; E2E tests use Playwright under `playwright/` at repo root. - Each behavior gets both a positive test and the relevant negative/guard test (e.g. error path fires `onError`, callback NOT called on failure). Test behavior via `data-testid`, not CSS selectors or implementation details. - Mock the store with `@webex/test-fixtures` (`test-fixtures/src/`); never hit the real SDK. @@ -65,12 +84,14 @@ Grounded in `patterns/testing-patterns.md`: - Run tests with `yarn workspace @webex/{pkg} test:unit` (single package) or `yarn test:cc-widgets` (all CC); styles via `yarn test:styles`; E2E via `yarn test:e2e`. **Never** `npx jest` directly. ## Security + - No PII or credentials in logs (see Logging) — agent dial numbers, tokens, session/auth material must never reach `logger`/`console` or telemetry payloads. - Reach the SDK only through the store; never import the SDK directly (prevents bypassing the store's auth/state boundary). - No hardcoded secrets/tokens/keys anywhere (see Secrets Policy). Sanitize user-provided input rendered in the UI. - This repo owns NO persistent datastore — all domain data comes from the SDK at runtime, so there is no at-rest data-handling surface here (N/A by construction). A dedicated `SECURITY.md` is not yet present; these rules are the current security posture. ## Spec-Currency & Drift Thresholds + - Update the spec/docs in the SAME change as the code (spec-currency: `.sdd/coverage-policy.defaults.yaml` `specCurrency.sameChangeRequired: true`). - Drift thresholds mirror `.sdd/coverage-policy.defaults.yaml` (drift = share of spec claims no longer matching code): - AUTHORITATIVE ≤ 5% @@ -79,15 +100,19 @@ Grounded in `patterns/testing-patterns.md`: - NONE — no spec to drift from ## Secrets Policy + - No hardcoded secrets/tokens/keys/connection strings — ever. The widgets receive auth context from the host application via the SDK/store at runtime; nothing is sourced from a committed file. Never log secrets, never commit them. ## Concurrency & Async + The repo is reactive (MobX) and event-driven (SDK events), so these apply: + - All store mutations MUST go through `runInAction(() => { ... })` — never mutate observable state directly (`patterns/mobx-patterns.md`; realized in `store/src/storeEventsWrapper.ts`). Severity: high. Verification: review only. - Widgets that read store state MUST be wrapped in `observer()` from `mobx-react-lite` so re-renders track observable reads. Severity: high. - SDK event subscriptions registered in `useEffect` MUST be torn down in the cleanup return (`cc.on(...)` paired with `cc.off(...)`) to avoid duplicate handlers/leaks (`patterns/react-patterns.md`). - `cc` is held as `observable.ref` (`store/src/store.ts`) — replace the reference, don't deep-mutate the SDK instance. ## Maintenance + - Add a rule when a review correction recurs; remove it when a lint rule starts enforcing it. - Cross-reference: patterns → `patterns/` (`typescript-patterns.md`, `react-patterns.md`, `mobx-patterns.md`, `testing-patterns.md`); module specs → `SPEC_INDEX.md`. diff --git a/ai-docs/rules/css-rem-units.md b/ai-docs/rules/css-rem-units.md new file mode 100644 index 000000000..bc48448cb --- /dev/null +++ b/ai-docs/rules/css-rem-units.md @@ -0,0 +1,78 @@ + + +# Rule: Use rem units in component styles, not px + +> Start here → repo root [`AGENTS.md`](../../AGENTS.md) (agent entry, carries the critical rules) · router [`SPEC_INDEX.md`](../SPEC_INDEX.md). This is an `ai-docs/rules/` fill-in; the folder README explains generic-vs-per-language routing; the repo-wide rules digest is `../RULES.md`. +> Context-efficiency: link to canonical docs — don't duplicate them; one rule per file; defer to the linter where it enforces. + +## Rule + +In `*.style.scss` / `*.scss` files under `packages/contact-center/*/src/`, express spacing, sizing, +border-radius, border-width, and box-shadow offsets in `rem`, not hardcoded `px`. Use Momentum design +tokens (`var(--mds-color-theme-*)`) for color instead of hex codes, and avoid hardcoding `font-size` / +`font-weight` when the Momentum `Text` component's `type` prop already controls that typography. + +## Why + +This repo's component styles are built on the Momentum design system, which defines its own type scale +and spacing scale in `rem` (root-relative units respect the user/host app's base font size; `px` does +not). Hardcoded `px` values silently drift from the design tokens and break if a host app changes its +root font size for accessibility. This was flagged in review on PR #719 (`e911-modal.style.scss` used +`px` spacing and hex colors instead of the established convention). + +## How to follow + +- Convert `px` to `rem` at a 16px base (e.g. `8px` → `0.5rem`, `4px` → `0.25rem`, `1px` → `0.0625rem`). + See the inline `// Npx to rem` comments in + `packages/contact-center/cc-components/src/components/task/CallControl/call-control.styles.scss`. +- Reference: + ```scss + // from packages/contact-center/cc-components/src/components/task/CampaignErrorDialog/campaign-error-dialog.style.scss + .campaign-error-dialog { + width: 25rem; + border-radius: 0.5rem; + padding: 1rem; + box-shadow: + 0rem 0.25rem 0.5rem 0rem rgba(0, 0, 0, 0.16), + 0rem 0rem 0.0625rem 0rem rgba(0, 0, 0, 0.16); + } + ``` + Incorrect (pre-fix `e911-modal.style.scss`): + ```scss + .e911-modal { + border-radius: 8px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2); + } + .e911-warning-box { + background-color: #fff3cd; // hardcoded hex instead of a Momentum token + border: 1px solid #ffc107; + } + .e911-modal-title { + font-size: 18px; // redundant: Text type="body-large-bold" already sets this + font-weight: 600; + } + ``` +- Use Momentum theme tokens for color, not hex: `var(--mds-color-theme-background-alert-warning-normal)`, + `var(--mds-color-theme-outline-warning-normal)`, `var(--mds-color-theme-text-warning-normal)`, + `var(--mds-color-theme-text-secondary-normal)`, `var(--mds-color-theme-text-accent-normal)`, + `var(--mds-color-theme-outline-primary-normal)` (see `call-control.styles.scss`, `user-state.scss`). +- Don't set `font-size`/`font-weight` in SCSS for text rendered through the Momentum `Text` component — + its `type` prop (e.g. `type="body-large-bold"`) already governs typography. +- **Exceptions:** media-query breakpoints (`@media (max-width: 600px)`) and exact pixel-perfect image/SVG + asset dimensions (e.g. a background-image sized to a specific icon export) may stay in `px` — see the + same `call-control.styles.scss` file for both cases. Don't over-apply the rule to these. + +## Enforced by + +Review only. `yarn test:styles` runs ESLint (`"test:styles": "eslint"` in each package's +`package.json`), not a CSS/SCSS linter — there is no Stylelint config in this repo, so nothing currently +bans `px` or hex colors in `.scss` files automatically. Consider adding Stylelint with +`declaration-property-unit-disallowed-list` (scoped to spacing/sizing properties) and `color-no-hex` to +make this automatic. diff --git a/packages/contact-center/cc-components/src/components/StationLogin/E911Modal/e911-modal.constants.ts b/packages/contact-center/cc-components/src/components/StationLogin/E911Modal/e911-modal.constants.ts index 1064a566b..1e2afcd7a 100644 --- a/packages/contact-center/cc-components/src/components/StationLogin/E911Modal/e911-modal.constants.ts +++ b/packages/contact-center/cc-components/src/components/StationLogin/E911Modal/e911-modal.constants.ts @@ -3,7 +3,6 @@ export const E911ModalLabels = { WARNING_TITLE: 'Important', WARNING_MESSAGE: 'If your address has changed, update it in the Emergency Callback Number (ECBN) section of your profile before making any calls.', - HELP_LINK_TEXT: 'See help section', DIALING_TITLE: 'Dialing Emergency Services', DIALING_MESSAGE: 'When you dial an emergency number (e.g., 911), your call will be routed to the appropriate emergency services based on your registered address. Ensure your address is always up to date to receive timely assistance.', diff --git a/packages/contact-center/cc-components/src/components/StationLogin/E911Modal/e911-modal.style.scss b/packages/contact-center/cc-components/src/components/StationLogin/E911Modal/e911-modal.style.scss index 9eb54e2db..60503d3b3 100644 --- a/packages/contact-center/cc-components/src/components/StationLogin/E911Modal/e911-modal.style.scss +++ b/packages/contact-center/cc-components/src/components/StationLogin/E911Modal/e911-modal.style.scss @@ -1,9 +1,9 @@ .e911-modal { padding: 0; border: none; - border-radius: 8px; - box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2); - max-width: 480px; + border-radius: 0.5rem; + box-shadow: 0rem 0.25rem 1rem 0rem rgba(0, 0, 0, 0.2); + max-width: 30rem; width: 90%; &::backdrop { @@ -11,84 +11,66 @@ } .e911-modal-content { - padding: 24px; + padding: 1.5rem; } .e911-modal-header { display: flex; justify-content: space-between; align-items: center; - margin-bottom: 16px; + margin-bottom: 1rem; .e911-modal-title { margin: 0; - font-size: 18px; - font-weight: 600; } .e911-close-button { background: none; border: none; cursor: pointer; - padding: 4px; + padding: 0.25rem; } } .e911-warning-box { - background-color: #fff3cd; - border: 1px solid #ffc107; - border-radius: 4px; - padding: 12px; - margin-bottom: 16px; + background-color: var(--mds-color-theme-background-alert-warning-normal); + border: 0.0625rem solid var(--mds-color-theme-outline-warning-normal); + border-radius: 0.25rem; + padding: 0.75rem; + margin-bottom: 1rem; .e911-warning-title { display: flex; align-items: center; - gap: 8px; - margin-bottom: 8px; - font-weight: 600; - color: #856404; + gap: 0.5rem; + margin-bottom: 0.5rem; + color: var(--mds-color-theme-text-warning-normal); } .e911-warning-message { - color: #856404; - font-size: 14px; - line-height: 1.5; - } - - .e911-help-link { - color: #0056b3; - text-decoration: underline; - cursor: pointer; - font-size: 14px; - margin-top: 8px; - display: inline-block; + color: var(--mds-color-theme-text-warning-normal); } } .e911-dialing-section { - margin-bottom: 20px; + margin-bottom: 1.25rem; .e911-dialing-title { - font-weight: 600; - margin-bottom: 8px; + margin-bottom: 0.5rem; } .e911-dialing-message { - font-size: 14px; - line-height: 1.5; - color: #666; + color: var(--mds-color-theme-text-secondary-normal); } } .e911-checkbox-container { display: flex; align-items: center; - gap: 8px; - margin-bottom: 20px; + gap: 0.5rem; + margin-bottom: 1.25rem; .e911-checkbox-label { - font-size: 14px; cursor: pointer; } } @@ -96,8 +78,8 @@ .e911-modal-footer { display: flex; justify-content: flex-end; - gap: 12px; - padding-top: 16px; - border-top: 1px solid #e0e0e0; + gap: 0.75rem; + padding-top: 1rem; + border-top: 0.0625rem solid var(--mds-color-theme-outline-primary-normal); } } diff --git a/packages/contact-center/cc-components/src/components/StationLogin/E911Modal/e911-modal.tsx b/packages/contact-center/cc-components/src/components/StationLogin/E911Modal/e911-modal.tsx index 0ad5b4062..c66ceb211 100644 --- a/packages/contact-center/cc-components/src/components/StationLogin/E911Modal/e911-modal.tsx +++ b/packages/contact-center/cc-components/src/components/StationLogin/E911Modal/e911-modal.tsx @@ -81,9 +81,6 @@ const E911Modal: React.FC = ({isOpen, onSaveAndContinue, onCance {E911ModalLabels.WARNING_MESSAGE} - - {E911ModalLabels.HELP_LINK_TEXT} -
diff --git a/packages/contact-center/cc-components/tests/components/StationLogin/E911Modal/e911-modal.test.tsx b/packages/contact-center/cc-components/tests/components/StationLogin/E911Modal/e911-modal.test.tsx index 3d5e743f6..79d20deab 100644 --- a/packages/contact-center/cc-components/tests/components/StationLogin/E911Modal/e911-modal.test.tsx +++ b/packages/contact-center/cc-components/tests/components/StationLogin/E911Modal/e911-modal.test.tsx @@ -121,12 +121,6 @@ describe('E911Modal', () => { expect(screen.getByTestId('e911-modal')).toBeInTheDocument(); }); - it('should display help link', () => { - render(); - expect(screen.getByTestId('e911-help-link')).toBeInTheDocument(); - expect(screen.getByText(E911ModalLabels.HELP_LINK_TEXT)).toBeInTheDocument(); - }); - it('should call onCancel when the native dialog is dismissed via the cancel event (e.g. Escape key)', () => { render(); const dialog = screen.getByTestId('e911-modal'); diff --git a/packages/contact-center/station-login/src/helper.ts b/packages/contact-center/station-login/src/helper.ts index bf114337e..fe4455db6 100644 --- a/packages/contact-center/station-login/src/helper.ts +++ b/packages/contact-center/station-login/src/helper.ts @@ -1,7 +1,7 @@ import {useEffect, useState} from 'react'; import {LogoutSuccess, AgentProfileUpdate, LoginOption, StationLoginSuccessResponse} from '@webex/contact-center'; import {UseStationLoginProps} from './station-login/station-login.types'; -import store, {CC_EVENTS} from '@webex/cc-store'; // we need to import as we are losing the context of this in store +import store, {CC_EVENTS, DEVICE_TYPE_BROWSER} from '@webex/cc-store'; // we need to import as we are losing the context of this in store import {LoginOptionsState} from '@webex/cc-components'; export const useStationLogin = (props: UseStationLoginProps) => { @@ -80,7 +80,7 @@ export const useStationLogin = (props: UseStationLoginProps) => { // Compare logic for Save button const isLoginOptionsChanged = originalLoginOptions.deviceType !== currentLoginOptions.deviceType || - (currentLoginOptions.deviceType !== 'BROWSER' && + (currentLoginOptions.deviceType !== DEVICE_TYPE_BROWSER && originalLoginOptions.dialNumber !== currentLoginOptions.dialNumber) || originalLoginOptions.teamId !== currentLoginOptions.teamId; @@ -104,7 +104,7 @@ export const useStationLogin = (props: UseStationLoginProps) => { loginOption: currentLoginOptions.deviceType as LoginOption, teamId: currentLoginOptions.teamId || undefined, }; - if (currentLoginOptions.deviceType !== 'BROWSER') { + if (currentLoginOptions.deviceType !== DEVICE_TYPE_BROWSER) { payload.dialNumber = currentLoginOptions.dialNumber; } @@ -239,7 +239,7 @@ export const useStationLogin = (props: UseStationLoginProps) => { const checkE911ModalDisplay = async () => { try { - if (deviceType !== 'BROWSER') { + if (deviceType !== DEVICE_TYPE_BROWSER) { return; } diff --git a/packages/contact-center/station-login/tests/helper.ts b/packages/contact-center/station-login/tests/helper.ts index d33d10769..5320e5822 100644 --- a/packages/contact-center/station-login/tests/helper.ts +++ b/packages/contact-center/station-login/tests/helper.ts @@ -26,9 +26,13 @@ jest.mock('@webex/cc-store', () => { setLogoutCallback: jest.fn(), setCCCallback: jest.fn(), removeCCCallback: jest.fn(), + fetchUserPreferences: jest.fn(), + setShowE911Modal: jest.fn(), + isEmergencyModalAlreadyDisplayed: false, CC_EVENTS: { AGENT_STATION_LOGIN_SUCCESS: 'AgentStationLoginSuccess', }, + DEVICE_TYPE_BROWSER: 'BROWSER', }; }); @@ -1024,4 +1028,104 @@ describe('useStationLogin Hook', () => { }); }); }); + + describe('checkE911ModalDisplay (via login)', () => { + afterEach(() => { + (store.fetchUserPreferences as jest.Mock).mockReset(); + (store.setShowE911Modal as jest.Mock).mockReset(); + (store as unknown as {isEmergencyModalAlreadyDisplayed: boolean}).isEmergencyModalAlreadyDisplayed = false; + }); + + it('should show the E911 modal on successful BROWSER login when not already acknowledged', async () => { + ccMock.stationLogin.mockResolvedValue(successResponse); + (store.fetchUserPreferences as jest.Mock).mockResolvedValue(undefined); + (store as unknown as {isEmergencyModalAlreadyDisplayed: boolean}).isEmergencyModalAlreadyDisplayed = false; + + const {result} = renderHook(() => + useStationLogin({ + ...baseStationLoginProps, + deviceType: 'BROWSER', + }) + ); + + await act(async () => { + await result.current.login(); + }); + + await waitFor(() => { + expect(store.fetchUserPreferences).toHaveBeenCalled(); + expect(store.setShowE911Modal).toHaveBeenCalledWith(true); + }); + }); + + it('should skip the E911 modal for non-BROWSER device types', async () => { + ccMock.stationLogin.mockResolvedValue(successResponse); + + const {result} = renderHook(() => + useStationLogin({ + ...baseStationLoginProps, + deviceType: 'EXTENSION', + }) + ); + + await act(async () => { + await result.current.login(); + }); + + await waitFor(() => { + expect(store.fetchUserPreferences).not.toHaveBeenCalled(); + expect(store.setShowE911Modal).not.toHaveBeenCalled(); + }); + }); + + it('should skip the E911 modal when the preference is already acknowledged', async () => { + ccMock.stationLogin.mockResolvedValue(successResponse); + (store.fetchUserPreferences as jest.Mock).mockResolvedValue(undefined); + (store as unknown as {isEmergencyModalAlreadyDisplayed: boolean}).isEmergencyModalAlreadyDisplayed = true; + + const {result} = renderHook(() => + useStationLogin({ + ...baseStationLoginProps, + deviceType: 'BROWSER', + }) + ); + + await act(async () => { + await result.current.login(); + }); + + await waitFor(() => { + expect(store.fetchUserPreferences).toHaveBeenCalled(); + expect(store.setShowE911Modal).not.toHaveBeenCalled(); + }); + }); + + it('should log an error and not show the modal when fetchUserPreferences fails', async () => { + ccMock.stationLogin.mockResolvedValue(successResponse); + const fetchError = new Error('userPreference service not available'); + (store.fetchUserPreferences as jest.Mock).mockRejectedValue(fetchError); + + const {result} = renderHook(() => + useStationLogin({ + ...baseStationLoginProps, + deviceType: 'BROWSER', + }) + ); + + await act(async () => { + await result.current.login(); + }); + + await waitFor(() => { + expect(store.setShowE911Modal).not.toHaveBeenCalled(); + expect(logger.error).toHaveBeenCalledWith( + expect.stringContaining('CC-Widgets: Error checking E911 modal display'), + expect.objectContaining({ + module: 'widget-station-login#helper.ts', + method: 'checkE911ModalDisplay', + }) + ); + }); + }); + }); }); diff --git a/packages/contact-center/store/src/storeEventsWrapper.ts b/packages/contact-center/store/src/storeEventsWrapper.ts index f5a9a00b6..a42bbe853 100644 --- a/packages/contact-center/store/src/storeEventsWrapper.ts +++ b/packages/contact-center/store/src/storeEventsWrapper.ts @@ -655,7 +655,27 @@ class StoreWrapper implements IStoreWrapper { throw new Error('userPreference service not available'); } + const response = await this.store.cc.userPreference.getUserPreference(); + const existingDesktopPrefString = response?.desktopPreference; + + let existingDesktopPref = {}; + if (existingDesktopPrefString) { + try { + existingDesktopPref = JSON.parse(existingDesktopPrefString); + } catch (parseError) { + this.store.logger.error( + 'CC-Widgets: updateEmergencyModalAcknowledgment(): failed to parse existing desktopPreference', + { + module: 'storeEventsWrapper.ts', + method: 'updateEmergencyModalAcknowledgment', + error: parseError, + } + ); + } + } + const desktopPreference = JSON.stringify({ + ...existingDesktopPref, isEmergencyModalAlreadyDisplayed: true, }); diff --git a/packages/contact-center/store/tests/storeEventsWrapper.ts b/packages/contact-center/store/tests/storeEventsWrapper.ts index 8abe3f8e5..ef4f13998 100644 --- a/packages/contact-center/store/tests/storeEventsWrapper.ts +++ b/packages/contact-center/store/tests/storeEventsWrapper.ts @@ -2689,6 +2689,44 @@ describe('storeEventsWrapper', () => { expect(storeWrapper['store'].showE911Modal).toBe(false); }); + it('should merge the E911 flag into existing desktopPreference instead of overwriting it', async () => { + const mockUserPreference = { + getUserPreference: jest.fn().mockResolvedValue({ + desktopPreference: JSON.stringify({someOtherSetting: 'value'}), + }), + updateUserPreference: jest.fn().mockResolvedValue({}), + }; + storeWrapper['store'].cc.userPreference = mockUserPreference; + storeWrapper['store'].agentId = 'test-agent-id'; + + await storeWrapper.updateEmergencyModalAcknowledgment(); + + expect(mockUserPreference.updateUserPreference).toHaveBeenCalledWith('test-agent-id', { + desktopPreference: JSON.stringify({someOtherSetting: 'value', isEmergencyModalAlreadyDisplayed: true}), + }); + }); + + it('should handle unparsable existing desktopPreference gracefully when merging', async () => { + const mockUserPreference = { + getUserPreference: jest.fn().mockResolvedValue({ + desktopPreference: 'invalid-json', + }), + updateUserPreference: jest.fn().mockResolvedValue({}), + }; + storeWrapper['store'].cc.userPreference = mockUserPreference; + storeWrapper['store'].agentId = 'test-agent-id'; + + await storeWrapper.updateEmergencyModalAcknowledgment(); + + expect(storeWrapper['store'].logger.error).toHaveBeenCalledWith( + 'CC-Widgets: updateEmergencyModalAcknowledgment(): failed to parse existing desktopPreference', + expect.any(Object) + ); + expect(mockUserPreference.updateUserPreference).toHaveBeenCalledWith('test-agent-id', { + desktopPreference: JSON.stringify({isEmergencyModalAlreadyDisplayed: true}), + }); + }); + it('should throw error on API failure', async () => { const mockError = new Error('Update Error'); const mockUserPreference = { From 419bdb6813e9fd05a5d6e8f919c4081e85c87774 Mon Sep 17 00:00:00 2001 From: Matthew Olker Date: Mon, 20 Jul 2026 12:13:02 -0400 Subject: [PATCH 06/14] feat(e911-modal): finalize modal integration, dismissal, and triggers - Replace native with Momentum Dialog component; disable the built-in close button and Escape-key dismissal so Cancel is the only way to dismiss the modal - Add missing triggers for checkE911ModalDisplay(): on mount when already logged in with BROWSER, on AGENT_STATION_LOGIN_SUCCESS/relogin events, on profile save switching to BROWSER, and on multi-login takeover (handleContinue) - Fix updateEmergencyModalAcknowledgment() to persist the acknowledgment under the preference service's own userId (from getUserPreference()) instead of the CC agentId, since the two identifiers can differ and using the wrong one could cause the modal to reappear erroneously - Merge existing desktopPreference fields when updating the acknowledgment flag instead of overwriting sibling preferences - Add a manual debug button in the samples app to force-open the E911 modal for testing - Add/update unit tests covering the new triggers, dismissal behavior, and the userId-vs-agentId fix --- .../E911Modal/e911-modal.style.scss | 42 +------ .../StationLogin/E911Modal/e911-modal.tsx | 83 ++++--------- .../E911Modal/e911-modal.test.tsx | 29 ++--- .../station-login/src/helper.ts | 19 ++- .../station-login/tests/helper.ts | 113 ++++++++++++++++++ .../store/src/storeEventsWrapper.ts | 2 +- .../store/tests/storeEventsWrapper.ts | 27 ++++- .../cc/samples-cc-react-app/src/App.tsx | 19 +-- 8 files changed, 200 insertions(+), 134 deletions(-) diff --git a/packages/contact-center/cc-components/src/components/StationLogin/E911Modal/e911-modal.style.scss b/packages/contact-center/cc-components/src/components/StationLogin/E911Modal/e911-modal.style.scss index 60503d3b3..059f9181b 100644 --- a/packages/contact-center/cc-components/src/components/StationLogin/E911Modal/e911-modal.style.scss +++ b/packages/contact-center/cc-components/src/components/StationLogin/E911Modal/e911-modal.style.scss @@ -1,35 +1,7 @@ .e911-modal { - padding: 0; - border: none; - border-radius: 0.5rem; - box-shadow: 0rem 0.25rem 1rem 0rem rgba(0, 0, 0, 0.2); - max-width: 30rem; - width: 90%; - - &::backdrop { - background-color: rgba(0, 0, 0, 0.5); - } - - .e911-modal-content { - padding: 1.5rem; - } - - .e911-modal-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 1rem; - - .e911-modal-title { - margin: 0; - } - - .e911-close-button { - background: none; - border: none; - cursor: pointer; - padding: 0.25rem; - } + // Cancel is the only way to dismiss this modal - hide the Dialog's built-in close ("X") button. + &::part(dialog-close-btn) { + display: none; } .e911-warning-box { @@ -74,12 +46,4 @@ cursor: pointer; } } - - .e911-modal-footer { - display: flex; - justify-content: flex-end; - gap: 0.75rem; - padding-top: 1rem; - border-top: 0.0625rem solid var(--mds-color-theme-outline-primary-normal); - } } diff --git a/packages/contact-center/cc-components/src/components/StationLogin/E911Modal/e911-modal.tsx b/packages/contact-center/cc-components/src/components/StationLogin/E911Modal/e911-modal.tsx index c66ceb211..998a91e80 100644 --- a/packages/contact-center/cc-components/src/components/StationLogin/E911Modal/e911-modal.tsx +++ b/packages/contact-center/cc-components/src/components/StationLogin/E911Modal/e911-modal.tsx @@ -1,13 +1,18 @@ -import React, {useRef, useEffect, useState} from 'react'; -import {Button, Text, Icon, Checkbox} from '@momentum-design/components/dist/react'; +import React, {useEffect, useState} from 'react'; +import {Button, Text, Icon, Checkbox, Dialog} from '@momentum-design/components/dist/react'; import {E911ModalProps} from './e911-modal.types'; import {E911ModalLabels} from './e911-modal.constants'; import './e911-modal.style.scss'; const E911Modal: React.FC = ({isOpen, onSaveAndContinue, onCancel}) => { - const dialogRef = useRef(null); const [isChecked, setIsChecked] = useState(false); + useEffect(() => { + if (!isOpen) { + setIsChecked(false); + } + }, [isOpen]); + const handleCheckboxChange = () => { setIsChecked(!isChecked); }; @@ -17,33 +22,6 @@ const E911Modal: React.FC = ({isOpen, onSaveAndContinue, onCance onCancel(); }; - useEffect(() => { - const dialog = dialogRef.current; - if (!dialog) return; - - if (isOpen) { - if (!dialog.open) { - dialog.showModal(); - } - } else { - if (dialog.open) { - dialog.close(); - } - setIsChecked(false); - } - - const handleNativeCancel = (event: Event) => { - event.preventDefault(); - handleCancel(); - }; - - dialog.addEventListener('cancel', handleNativeCancel); - - return () => { - dialog.removeEventListener('cancel', handleNativeCancel); - }; - }, [isOpen, onCancel]); - const handleSaveAndContinue = () => { if (isChecked) { onSaveAndContinue(); @@ -51,26 +29,8 @@ const E911Modal: React.FC = ({isOpen, onSaveAndContinue, onCance }; return ( - -
-
- - {E911ModalLabels.TITLE} - -
- + +
@@ -101,17 +61,20 @@ const E911Modal: React.FC = ({isOpen, onSaveAndContinue, onCance label={E911ModalLabels.CHECKBOX_LABEL} />
- -
- - -
-
+ + + +
); }; diff --git a/packages/contact-center/cc-components/tests/components/StationLogin/E911Modal/e911-modal.test.tsx b/packages/contact-center/cc-components/tests/components/StationLogin/E911Modal/e911-modal.test.tsx index 79d20deab..0f7ceb4ad 100644 --- a/packages/contact-center/cc-components/tests/components/StationLogin/E911Modal/e911-modal.test.tsx +++ b/packages/contact-center/cc-components/tests/components/StationLogin/E911Modal/e911-modal.test.tsx @@ -17,8 +17,6 @@ describe('E911Modal', () => { beforeEach(() => { jest.clearAllMocks(); - HTMLDialogElement.prototype.showModal = jest.fn(); - HTMLDialogElement.prototype.close = jest.fn(); }); it('should render the modal when isOpen is true', () => { @@ -28,7 +26,10 @@ describe('E911Modal', () => { it('should display the modal title', () => { render(); - expect(screen.getByText(E911ModalLabels.TITLE)).toBeInTheDocument(); + // The Dialog custom element renders headerText in its shadow DOM, so assert via the + // headerText property @lit/react sets on the element rather than a light-DOM text query. + const dialog = screen.getByTestId('e911-modal') as HTMLElement & {headerText?: string}; + expect(dialog.headerText).toBe(E911ModalLabels.TITLE); }); it('should display the warning message', () => { @@ -78,15 +79,6 @@ describe('E911Modal', () => { expect(defaultProps.onCancel).toHaveBeenCalledTimes(1); }); - it('should call onCancel when close button is clicked', () => { - render(); - const closeButton = screen.getByTestId('e911-close-button'); - - fireEvent.click(closeButton); - - expect(defaultProps.onCancel).toHaveBeenCalledTimes(1); - }); - it('should call onSaveAndContinue when Save & Continue is clicked with checkbox checked', async () => { render(); const checkbox = screen.getByTestId('e911-checkbox'); @@ -104,12 +96,13 @@ describe('E911Modal', () => { expect(defaultProps.onSaveAndContinue).toHaveBeenCalledTimes(1); }); - it('should call showModal when isOpen changes to true', () => { + it('should set visible on the Dialog when isOpen changes to true', () => { const {rerender} = render(); rerender(); - expect(HTMLDialogElement.prototype.showModal).toHaveBeenCalled(); + const dialog = screen.getByTestId('e911-modal') as HTMLElement & {visible?: boolean}; + expect(dialog.visible).toBe(true); }); it('should reset checkbox state when modal closes', () => { @@ -121,14 +114,12 @@ describe('E911Modal', () => { expect(screen.getByTestId('e911-modal')).toBeInTheDocument(); }); - it('should call onCancel when the native dialog is dismissed via the cancel event (e.g. Escape key)', () => { + it('should not call onCancel when the Dialog fires close (built-in close button or Escape key) - Cancel is the only way to dismiss', () => { render(); const dialog = screen.getByTestId('e911-modal'); - const cancelEvent = new Event('cancel', {cancelable: true}); - dialog.dispatchEvent(cancelEvent); + dialog.dispatchEvent(new CustomEvent('close')); - expect(cancelEvent.defaultPrevented).toBe(true); - expect(defaultProps.onCancel).toHaveBeenCalledTimes(1); + expect(defaultProps.onCancel).not.toHaveBeenCalled(); }); }); diff --git a/packages/contact-center/station-login/src/helper.ts b/packages/contact-center/station-login/src/helper.ts index fe4455db6..fc60d056d 100644 --- a/packages/contact-center/station-login/src/helper.ts +++ b/packages/contact-center/station-login/src/helper.ts @@ -121,6 +121,8 @@ export const useStationLogin = (props: UseStationLoginProps) => { module: 'widget-station-login#helper.ts', method: 'saveLoginOptions', }); + // Covers switching to BROWSER via profile save while already logged in. + checkE911ModalDisplay(currentLoginOptions.deviceType); if (props.onSaveEnd) props.onSaveEnd(true); }) .catch((error: Error) => { @@ -146,6 +148,11 @@ export const useStationLogin = (props: UseStationLoginProps) => { if (loginCb && store.isAgentLoggedIn) { loginCb(); } + if (store.isAgentLoggedIn) { + // Covers page refresh while already logged in with BROWSER: login() never runs, so this is + // the only place that can trigger the E911 modal for an already-hydrated BROWSER session. + checkE911ModalDisplay(store.deviceType); + } } catch (error) { logger.error(`CC-Widgets: Error in useEffect (loginCb) - ${error.message}`, { module: 'widget-station-login#helper.ts', @@ -172,6 +179,9 @@ export const useStationLogin = (props: UseStationLoginProps) => { if (loginCb) { loginCb(); } + // Covers AGENT_STATION_LOGIN_SUCCESS / relogin events that aren't driven by this widget's + // own login() call (e.g. another tab/session logging in as BROWSER). + checkE911ModalDisplay(store.deviceType); } catch (error) { logger.error(`CC-Widgets: Error in handleLogin - ${error.message}`, { module: 'widget-station-login#helper.ts', @@ -223,6 +233,9 @@ export const useStationLogin = (props: UseStationLoginProps) => { module: 'widget-station-login#helper.ts', method: 'handleContinue', }); + // Covers the multi-login takeover flow: registerCC() completes the relogin but never + // runs the E911 preference check on its own. + checkE911ModalDisplay(store.deviceType); } else { logger.error(`Agent Relogin Failed`, { module: 'widget-station-login#helper.ts', @@ -237,9 +250,9 @@ export const useStationLogin = (props: UseStationLoginProps) => { } }; - const checkE911ModalDisplay = async () => { + const checkE911ModalDisplay = async (deviceTypeToCheck: string) => { try { - if (deviceType !== DEVICE_TYPE_BROWSER) { + if (deviceTypeToCheck !== DEVICE_TYPE_BROWSER) { return; } @@ -271,7 +284,7 @@ export const useStationLogin = (props: UseStationLoginProps) => { setLoginSuccess(res); setLoginFailure(undefined); - checkE911ModalDisplay(); + checkE911ModalDisplay(deviceType); }) .catch((error: Error) => { logger.error(`Error logging in: ${error}`, { diff --git a/packages/contact-center/station-login/tests/helper.ts b/packages/contact-center/station-login/tests/helper.ts index 5320e5822..85c79606f 100644 --- a/packages/contact-center/station-login/tests/helper.ts +++ b/packages/contact-center/station-login/tests/helper.ts @@ -17,6 +17,7 @@ jest.mock('@webex/cc-store', () => { isAgentLoggedIn = value; // Update internal variable }), registerCC: jest.fn(), + deviceType: 'EXTENSION', setDeviceType: jest.fn(), setDialNumber: jest.fn(), setCurrentState: jest.fn(), @@ -1034,6 +1035,8 @@ describe('useStationLogin Hook', () => { (store.fetchUserPreferences as jest.Mock).mockReset(); (store.setShowE911Modal as jest.Mock).mockReset(); (store as unknown as {isEmergencyModalAlreadyDisplayed: boolean}).isEmergencyModalAlreadyDisplayed = false; + (store as unknown as {deviceType: string}).deviceType = 'EXTENSION'; + store.setIsAgentLoggedIn(false); }); it('should show the E911 modal on successful BROWSER login when not already acknowledged', async () => { @@ -1127,5 +1130,115 @@ describe('useStationLogin Hook', () => { ); }); }); + + it('should show the E911 modal on mount when already logged in with BROWSER (e.g. page refresh)', async () => { + (store.fetchUserPreferences as jest.Mock).mockResolvedValue(undefined); + store.setIsAgentLoggedIn(true); + (store as unknown as {deviceType: string}).deviceType = 'BROWSER'; + + renderHook(() => useStationLogin(baseStationLoginProps)); + + await waitFor(() => { + expect(store.fetchUserPreferences).toHaveBeenCalled(); + expect(store.setShowE911Modal).toHaveBeenCalledWith(true); + }); + }); + + it('should not show the E911 modal on mount when already logged in with a non-BROWSER device', async () => { + store.setIsAgentLoggedIn(true); + (store as unknown as {deviceType: string}).deviceType = 'EXTENSION'; + + renderHook(() => useStationLogin(baseStationLoginProps)); + + await waitFor(() => { + expect(store.fetchUserPreferences).not.toHaveBeenCalled(); + expect(store.setShowE911Modal).not.toHaveBeenCalled(); + }); + }); + + it('should show the E911 modal on AGENT_STATION_LOGIN_SUCCESS / relogin when store.deviceType is BROWSER', async () => { + (store.fetchUserPreferences as jest.Mock).mockResolvedValue(undefined); + (store as unknown as {deviceType: string}).deviceType = 'BROWSER'; + jest.spyOn(store, 'setCCCallback').mockImplementation((event, cb) => { + ccMock.on(event, cb); + }); + const onSpy = jest.spyOn(ccMock, 'on'); + + renderHook(() => useStationLogin(baseStationLoginProps)); + + expect(ccMock.on).toHaveBeenCalledWith(CC_EVENTS.AGENT_STATION_LOGIN_SUCCESS, expect.any(Function)); + + act(() => { + (onSpy.mock.calls[0][1] as (payload: unknown) => void)({deviceType: 'BROWSER', auxCodeId: '0'}); + }); + + await waitFor(() => { + expect(store.fetchUserPreferences).toHaveBeenCalled(); + expect(store.setShowE911Modal).toHaveBeenCalledWith(true); + }); + }); + + it('should show the E911 modal after a successful multi-login takeover (handleContinue) with BROWSER', async () => { + (store.fetchUserPreferences as jest.Mock).mockResolvedValue(undefined); + store.setIsAgentLoggedIn(true); + (store as unknown as {deviceType: string}).deviceType = 'BROWSER'; + + const {result} = renderHook(() => useStationLogin(baseStationLoginProps)); + + await act(async () => { + await result.current.handleContinue(); + }); + + await waitFor(() => { + expect(store.fetchUserPreferences).toHaveBeenCalled(); + expect(store.setShowE911Modal).toHaveBeenCalledWith(true); + }); + }); + + it('should not show the E911 modal after handleContinue when store.deviceType is not BROWSER', async () => { + store.setIsAgentLoggedIn(true); + (store as unknown as {deviceType: string}).deviceType = 'EXTENSION'; + + const {result} = renderHook(() => useStationLogin(baseStationLoginProps)); + + await act(async () => { + await result.current.handleContinue(); + }); + + await waitFor(() => { + expect(store.fetchUserPreferences).not.toHaveBeenCalled(); + expect(store.setShowE911Modal).not.toHaveBeenCalled(); + }); + }); + + it('should show the E911 modal after saving login options with BROWSER as the new device type', async () => { + (store.fetchUserPreferences as jest.Mock).mockResolvedValue(undefined); + const cc = {...ccMock, updateAgentProfile: jest.fn().mockResolvedValue({})}; + + const {result} = renderHook(() => + useStationLogin({ + ...baseStationLoginProps, + cc, + deviceType: 'EXTENSION', + }) + ); + + act(() => { + result.current.setCurrentLoginOptions({ + deviceType: 'BROWSER', + dialNumber: '', + teamId: 'team123', + }); + }); + + await act(async () => { + await result.current.saveLoginOptions(); + }); + + await waitFor(() => { + expect(store.fetchUserPreferences).toHaveBeenCalled(); + expect(store.setShowE911Modal).toHaveBeenCalledWith(true); + }); + }); }); }); diff --git a/packages/contact-center/store/src/storeEventsWrapper.ts b/packages/contact-center/store/src/storeEventsWrapper.ts index a42bbe853..544eaaabb 100644 --- a/packages/contact-center/store/src/storeEventsWrapper.ts +++ b/packages/contact-center/store/src/storeEventsWrapper.ts @@ -679,7 +679,7 @@ class StoreWrapper implements IStoreWrapper { isEmergencyModalAlreadyDisplayed: true, }); - await this.store.cc.userPreference.updateUserPreference(this.store.agentId, { + await this.store.cc.userPreference.updateUserPreference(response?.userId, { desktopPreference, }); diff --git a/packages/contact-center/store/tests/storeEventsWrapper.ts b/packages/contact-center/store/tests/storeEventsWrapper.ts index ef4f13998..ba77f926c 100644 --- a/packages/contact-center/store/tests/storeEventsWrapper.ts +++ b/packages/contact-center/store/tests/storeEventsWrapper.ts @@ -2674,15 +2674,16 @@ describe('storeEventsWrapper', () => { it('should update user preferences and store state successfully', async () => { const mockUserPreference = { - getUserPreference: jest.fn(), + getUserPreference: jest.fn().mockResolvedValue({userId: 'test-preference-user-id'}), updateUserPreference: jest.fn().mockResolvedValue({}), }; storeWrapper['store'].cc.userPreference = mockUserPreference; + // agentId (CC identifier) intentionally differs from the preference service's userId storeWrapper['store'].agentId = 'test-agent-id'; await storeWrapper.updateEmergencyModalAcknowledgment(); - expect(mockUserPreference.updateUserPreference).toHaveBeenCalledWith('test-agent-id', { + expect(mockUserPreference.updateUserPreference).toHaveBeenCalledWith('test-preference-user-id', { desktopPreference: JSON.stringify({isEmergencyModalAlreadyDisplayed: true}), }); expect(storeWrapper['store'].isEmergencyModalAlreadyDisplayed).toBe(true); @@ -2692,6 +2693,7 @@ describe('storeEventsWrapper', () => { it('should merge the E911 flag into existing desktopPreference instead of overwriting it', async () => { const mockUserPreference = { getUserPreference: jest.fn().mockResolvedValue({ + userId: 'test-preference-user-id', desktopPreference: JSON.stringify({someOtherSetting: 'value'}), }), updateUserPreference: jest.fn().mockResolvedValue({}), @@ -2701,7 +2703,7 @@ describe('storeEventsWrapper', () => { await storeWrapper.updateEmergencyModalAcknowledgment(); - expect(mockUserPreference.updateUserPreference).toHaveBeenCalledWith('test-agent-id', { + expect(mockUserPreference.updateUserPreference).toHaveBeenCalledWith('test-preference-user-id', { desktopPreference: JSON.stringify({someOtherSetting: 'value', isEmergencyModalAlreadyDisplayed: true}), }); }); @@ -2709,6 +2711,7 @@ describe('storeEventsWrapper', () => { it('should handle unparsable existing desktopPreference gracefully when merging', async () => { const mockUserPreference = { getUserPreference: jest.fn().mockResolvedValue({ + userId: 'test-preference-user-id', desktopPreference: 'invalid-json', }), updateUserPreference: jest.fn().mockResolvedValue({}), @@ -2722,7 +2725,7 @@ describe('storeEventsWrapper', () => { 'CC-Widgets: updateEmergencyModalAcknowledgment(): failed to parse existing desktopPreference', expect.any(Object) ); - expect(mockUserPreference.updateUserPreference).toHaveBeenCalledWith('test-agent-id', { + expect(mockUserPreference.updateUserPreference).toHaveBeenCalledWith('test-preference-user-id', { desktopPreference: JSON.stringify({isEmergencyModalAlreadyDisplayed: true}), }); }); @@ -2730,13 +2733,27 @@ describe('storeEventsWrapper', () => { it('should throw error on API failure', async () => { const mockError = new Error('Update Error'); const mockUserPreference = { - getUserPreference: jest.fn(), + getUserPreference: jest.fn().mockResolvedValue({userId: 'test-preference-user-id'}), updateUserPreference: jest.fn().mockRejectedValue(mockError), }; storeWrapper['store'].cc.userPreference = mockUserPreference; await expect(storeWrapper.updateEmergencyModalAcknowledgment()).rejects.toThrow('Update Error'); }); + + it('should use the preference service userId, not the CC agentId, when updating', async () => { + const mockUserPreference = { + getUserPreference: jest.fn().mockResolvedValue({userId: 'preference-user-id'}), + updateUserPreference: jest.fn().mockResolvedValue({}), + }; + storeWrapper['store'].cc.userPreference = mockUserPreference; + storeWrapper['store'].agentId = 'cc-agent-id'; + + await storeWrapper.updateEmergencyModalAcknowledgment(); + + expect(mockUserPreference.updateUserPreference).toHaveBeenCalledWith('preference-user-id', expect.any(Object)); + expect(mockUserPreference.updateUserPreference).not.toHaveBeenCalledWith('cc-agent-id', expect.any(Object)); + }); }); }); }); diff --git a/widgets-samples/cc/samples-cc-react-app/src/App.tsx b/widgets-samples/cc/samples-cc-react-app/src/App.tsx index 7013b4f7c..97b8565eb 100644 --- a/widgets-samples/cc/samples-cc-react-app/src/App.tsx +++ b/widgets-samples/cc/samples-cc-react-app/src/App.tsx @@ -204,9 +204,7 @@ function App() { const mediaType = task?.data?.interaction?.mediaType; const isSocial = mediaType === 'social'; const title = isSocial ? callAssociatedDetails?.customerName : callAssociatedDetails?.ani; - console.log( - `onTaskSelected invoked for task with title : ${title}, and mediaType : ${mediaType}` - ); + console.log(`onTaskSelected invoked for task with title : ${title}, and mediaType : ${mediaType}`); }; const onHoldResume = ({isHeld, task}) => { @@ -742,10 +740,9 @@ function App() { }} > Note: Disabling WebRTC registration prevents browser-based calling. When - enabled, the Incoming Task, Task List, Call Control, and Call Control with CAD - widgets will be unchecked and disabled because they depend on call handling. Set this - option before clicking the Init Widgets button - changes after SDK initialization will - not take effect. + enabled, the Incoming Task, Task List, Call Control, and Call Control with CAD widgets will + be unchecked and disabled because they depend on call handling. Set this option before + clicking the Init Widgets button - changes after SDK initialization will not take effect.
@@ -906,6 +903,14 @@ function App() { allowInternationalDn={allowInternationalDn} /> +
+ +
From 94c8372b75db79a0d426e62604b5425dcbc44d22 Mon Sep 17 00:00:00 2001 From: Matthew Olker Date: Mon, 20 Jul 2026 12:42:40 -0400 Subject: [PATCH 07/14] docs(station-login-spec): add E911 modal requirements (spec-currency) --- .../ai-docs/station-login-spec.md | 180 +++++++++++------- 1 file changed, 106 insertions(+), 74 deletions(-) diff --git a/packages/contact-center/station-login/ai-docs/station-login-spec.md b/packages/contact-center/station-login/ai-docs/station-login-spec.md index 68ed0d8a9..5fc3c3ce3 100644 --- a/packages/contact-center/station-login/ai-docs/station-login-spec.md +++ b/packages/contact-center/station-login/ai-docs/station-login-spec.md @@ -4,17 +4,19 @@ > Context-efficiency: link to canonical docs — don't duplicate them. Load specs on demand per `SPEC_INDEX.md`. ## Metadata -| Field | Value | -|---|---| -| Module id | `station-login` | -| Source path(s) | `packages/contact-center/station-login/src/` | -| Doc kind | Module spec | -| Coverage score | Pending coverage assessment | -| Generated from | `module-spec` @ SDLC template library `0.1.0-draft` | -| generated_by / approved_by / updated_at | migration agent / pending / 2026-06-29 | -| Validation status | not-run | + +| Field | Value | +| --------------------------------------- | --------------------------------------------------- | +| Module id | `station-login` | +| Source path(s) | `packages/contact-center/station-login/src/` | +| Doc kind | Module spec | +| Coverage score | Pending coverage assessment | +| Generated from | `module-spec` @ SDLC template library `0.1.0-draft` | +| generated_by / approved_by / updated_at | migration agent / pending / 2026-06-29 | +| Validation status | not-run | ## Evidence Rules + Every generated requirement below must cite concrete source evidence using `file path`. Separate source evidence, test evidence, examples, assumptions, and gaps so validators and future agents can distinguish truth from context. Test evidence is preferred for WHY. Commit evidence is allowed only when the @@ -23,12 +25,14 @@ conflicting, ask a focused discovery question before finalizing the requirement; as approved unknowns only when the human explicitly defers or does not know. ## Source Material Register -| Source doc | Scope | Decision | Detail location or disposition | -|---|---|---|---| -| `ai-docs/_archive/pre-sdlc-migration/packages/contact-center/station-login/ai-docs/AGENTS.md` | overview / API | migrated | Orientation → Overview/Purpose; props → Public Surface; usage examples → Use Cases; error callback → Error Handling | -| `ai-docs/_archive/pre-sdlc-migration/packages/contact-center/station-login/ai-docs/ARCHITECTURE.md` | architecture | reconciled | Layer table → Class/Component Relationships; data flow + sequences → Data Flow / Sequence Diagram(s); troubleshooting → Pitfalls. Old "renders blank screen" / silent-fail notes mapped to Error Handling; over-generalized `store.login()` arrow in the archived diagram corrected — the hook calls `cc.stationLogin()` directly, not `store.login()` | + +| Source doc | Scope | Decision | Detail location or disposition | +| --------------------------------------------------------------------------------------------------- | -------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `ai-docs/_archive/pre-sdlc-migration/packages/contact-center/station-login/ai-docs/AGENTS.md` | overview / API | migrated | Orientation → Overview/Purpose; props → Public Surface; usage examples → Use Cases; error callback → Error Handling | +| `ai-docs/_archive/pre-sdlc-migration/packages/contact-center/station-login/ai-docs/ARCHITECTURE.md` | architecture | reconciled | Layer table → Class/Component Relationships; data flow + sequences → Data Flow / Sequence Diagram(s); troubleshooting → Pitfalls. Old "renders blank screen" / silent-fail notes mapped to Error Handling; over-generalized `store.login()` arrow in the archived diagram corrected — the hook calls `cc.stationLogin()` directly, not `store.login()` | ## Overview + `station-login` is the agent station-login widget for Webex Contact Center. It lets an agent pick a team and a device/login type (Desktop/`BROWSER` WebRTC, `EXTENSION`, or `AGENT_DN` dial number), log in to and out of their station, sign out of Contact Center, and — in profile mode — update those login options @@ -49,6 +53,7 @@ read `src/helper.ts` (all business logic and SDK integration). Prop and state sh `@webex/cc-components`. ## Purpose / Responsibility + Owns the agent station-login UI flow: team + device-type selection, login, logout, CC sign-out, profile (login-option) updates, and the multiple-login alert/Continue flow. Does NOT own the SDK instance, the observable agent state (`teams`, `deviceType`, `dialNumber`, `teamId`, `isAgentLoggedIn`, @@ -56,12 +61,14 @@ observable agent state (`teams`, `deviceType`, `dialNumber`, `teamId`, `isAgentL `@webex/cc-components` respectively. ## Stack + TypeScript 5.6.3, React `>=18.3.1` (functional component + hooks), MobX via `mobx-react-lite` `^4.1.0` (`observer`), `react-error-boundary` `^6.0.0`. Tests: Jest 29 + React Testing Library 16 (jsdom). Build: `tsc` (types) and Webpack 5 (`build:src`). Published as ESM/CJS package `@webex/cc-station-login` (`main: dist/index.js`, `types: dist/types/index.d.ts`). No datastore or messaging of its own. ## Folder / Package Structure + ``` station-login/ ├── src/ @@ -77,38 +84,42 @@ station-login/ ``` ## Key Files (source of truth) -| File | Holds | -|---|---| -| `src/index.ts` | Package export barrel; the public surface is whatever this re-exports (`StationLogin`) | -| `src/station-login/index.tsx` | Public widget, prop-to-hook wiring, store reads, ErrorBoundary → `store.onErrorCallback('StationLogin', error)` | -| `src/station-login/station-login.types.ts` | Authoritative public prop type `StationLoginProps` and hook input `UseStationLoginProps` (both `Pick` from `IStationLoginProps`) | -| `src/helper.ts` | `useStationLogin` — login/logout/saveLoginOptions/handleContinue/handleCCSignOut logic, `isLoginOptionsChanged` comparison, SDK event subscriptions | -| `@webex/cc-components` `components/StationLogin/station-login.types.ts` | Canonical `IStationLoginProps` / `LoginOptionsState`; do not redefine prop shapes here | + +| File | Holds | +| ----------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | +| `src/index.ts` | Package export barrel; the public surface is whatever this re-exports (`StationLogin`) | +| `src/station-login/index.tsx` | Public widget, prop-to-hook wiring, store reads, ErrorBoundary → `store.onErrorCallback('StationLogin', error)` | +| `src/station-login/station-login.types.ts` | Authoritative public prop type `StationLoginProps` and hook input `UseStationLoginProps` (both `Pick` from `IStationLoginProps`) | +| `src/helper.ts` | `useStationLogin` — login/logout/saveLoginOptions/handleContinue/handleCCSignOut logic, `isLoginOptionsChanged` comparison, SDK event subscriptions | +| `@webex/cc-components` `components/StationLogin/station-login.types.ts` | Canonical `IStationLoginProps` / `LoginOptionsState`; do not redefine prop shapes here | ## Public Surface -| Contract ID | Type | Surface | Purpose | Compatibility / deprecation | Schema / detail link | Root index | -|---|---|---|---|---|---|---| + +| Contract ID | Type | Surface | Purpose | Compatibility / deprecation | Schema / detail link | Root index | +| ------------------------- | ------------------- | ---------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | ---------------------------------- | | `cc-widgets.StationLogin` | SDK / Web Component | React component `StationLogin`; mounted in `@webex/cc-widgets` as custom element `widget-cc-station-login` | Agent station login/logout, CC sign-out, profile-option update, multi-login alert | Stable semver; the custom-element tag name and the `profileMode`/`onLogin`/`onLogout`/`onCCSignOut` prop surface are breaking changes | `src/station-login/station-login.types.ts` (`StationLoginProps`), `IStationLoginProps` in `@webex/cc-components` | `../../../../ai-docs/CONTRACTS.md` | Public props (`StationLoginProps`, from `src/station-login/station-login.types.ts`): -| Prop | Type | Required | Notes | -|---|---|---|---| -| `profileMode` | `boolean` | Yes | `true` = profile/save mode; `false` = login/logout mode | -| `onLogin` | `() => void` | No | Invoked on login success (and on mount if already logged in — `helper.ts`) | -| `onLogout` | `() => void` | No | Invoked on `AGENT_LOGOUT_SUCCESS` | -| `onCCSignOut` | `() => void` | No | Invoked after CC sign-out; presence enables the sign-out handler | -| `onSaveStart` | `() => void` | No | Invoked when a profile save begins | -| `onSaveEnd` | `(isComplete: boolean) => void` | No | Invoked when a profile save resolves (`true`) or fails / no-change (`false`) | -| `teamId` | `string` | No | Default/seed team id | -| `doStationLogout` | `boolean` | No | Defaults to `true` when omitted/null; if `false`, CC sign-out skips station logout | -| `hideDesktopLogin` | `boolean` | No | Hides the Desktop (`BROWSER`) option in dropdown | -| `allowInternationalDn` | `boolean` | No | Use international dial-number regex instead of agentConfig/US fallback | +| Prop | Type | Required | Notes | +| ---------------------- | ------------------------------- | -------- | ---------------------------------------------------------------------------------- | +| `profileMode` | `boolean` | Yes | `true` = profile/save mode; `false` = login/logout mode | +| `onLogin` | `() => void` | No | Invoked on login success (and on mount if already logged in — `helper.ts`) | +| `onLogout` | `() => void` | No | Invoked on `AGENT_LOGOUT_SUCCESS` | +| `onCCSignOut` | `() => void` | No | Invoked after CC sign-out; presence enables the sign-out handler | +| `onSaveStart` | `() => void` | No | Invoked when a profile save begins | +| `onSaveEnd` | `(isComplete: boolean) => void` | No | Invoked when a profile save resolves (`true`) or fails / no-change (`false`) | +| `teamId` | `string` | No | Default/seed team id | +| `doStationLogout` | `boolean` | No | Defaults to `true` when omitted/null; if `false`, CC sign-out skips station logout | +| `hideDesktopLogin` | `boolean` | No | Hides the Desktop (`BROWSER`) option in dropdown | +| `allowInternationalDn` | `boolean` | No | Use international dial-number regex instead of agentConfig/US fallback | Compatibility notes: + - Adding a new optional prop is a minor change; removing a prop or changing the custom-element tag name is a major (breaking) change. ## Requires (dependencies) + - `@webex/cc-store` (`workspace:*`) — MobX singleton; provides `cc` (SDK), `teams`, `loginOptions`, `deviceType`, `dialNumber`, `teamId`, `isAgentLoggedIn`, `showMultipleLoginAlert`, `logger`, `CC_EVENTS`, `setCCCallback`/`removeCCCallback`, `setShowMultipleLoginAlert`, `registerCC`, `onErrorCallback`. - `@webex/cc-components` (`workspace:*`) — `StationLoginComponent` (presentational), `IStationLoginProps`/`StationLoginComponentProps`/`LoginOptionsState` types. - `@webex/contact-center` (the SDK, via the store's `cc`) — `stationLogin()`, `stationLogout()`, `updateAgentProfile()`, `deregister()`; types `StationLoginSuccessResponse`, `LogoutSuccess`, `AgentProfileUpdate`, `LoginOption`. @@ -116,20 +127,25 @@ Compatibility notes: - Peer: `react`/`react-dom` `>=18.3.1`, `@momentum-ui/react-collaboration` `>=26.201.9`. ## Requirements -| ID | WHAT | WHY | Source Evidence | Test / Example Evidence | Assumptions / Gaps | Confidence | -|---|---|---|---|---|---|---| -| `STATION-LOGIN-R-001` | `login()` calls `cc.stationLogin({teamId, loginOption, dialNumber})`; on success sets `loginSuccess` and clears `loginFailure`, on failure sets `loginFailure` and clears `loginSuccess` | Login result must drive UI success/error display | `src/helper.ts` (`login`) | `tests/helper.ts` "should set loginSuccess on successful login and set loginFailure to undefined", "should set loginFailure on failed login" | none | PRESENT | -| `STATION-LOGIN-R-002` | `logout()` calls `cc.stationLogout({logoutReason})` and sets `logoutSuccess` on success; on failure it logs and does not throw | Logout must update state and never crash the widget | `src/helper.ts` (`logout`) | `tests/helper.ts` "should set logoutSuccess on successful logout", "should log error on logout failure" | none | PRESENT | -| `STATION-LOGIN-R-003` | The `onLogin` callback fires when the agent becomes logged in (on `AGENT_STATION_LOGIN_SUCCESS` and on mount when already logged in); `onLogout` fires on `AGENT_LOGOUT_SUCCESS`. Both are guarded so absent callbacks are a no-op | Host app needs login/logout lifecycle hooks without crashing when omitted | `src/helper.ts` (`handleLogin`/`handleLogout`, `setCCCallback` effect, mount effect) | `tests/helper.ts` "should set loginSuccess on successful login without onLogin callback", "should not call logout callback if not present" | none | PRESENT | -| `STATION-LOGIN-R-004` | `saveLoginOptions()` short-circuits when `isLoginOptionsChanged` is false: sets `saveError` to "No changes detected…" and calls `onSaveEnd(false)` without calling the SDK | Avoids no-op profile writes and gives the host a deterministic failure signal | `src/helper.ts` (`saveLoginOptions`, `isLoginOptionsChanged`) | `tests/helper.ts` "should not save if isLoginOptionsChanged is false" | none | PRESENT | -| `STATION-LOGIN-R-005` | On a real change, `saveLoginOptions()` calls `onSaveStart()`, calls `cc.updateAgentProfile()` with `{loginOption, teamId}` (plus `dialNumber` only when deviceType ≠ `BROWSER`), and on success copies `currentLoginOptions`→`originalLoginOptions` and calls `onSaveEnd(true)` | Profile update must persist only changed options and resync the baseline so the Save button disables | `src/helper.ts` (`saveLoginOptions`) | `tests/helper.ts` "should call updateAgentProfile and update originalLoginOptions on save when changed", "should call updateAgentProfile with no dialNumber when deviceType is BROWSER" | none | PRESENT | -| `STATION-LOGIN-R-006` | When `cc.updateAgentProfile()` rejects, `saveError` is set to the error message and `onSaveEnd(false)` is called | Caller must be able to surface profile-save failures | `src/helper.ts` (`saveLoginOptions` `.catch`) | `tests/helper.ts` "should handle updateAgentProfile errors", "should handle errors in saveLoginOptions main logic" | none | PRESENT | -| `STATION-LOGIN-R-007` | `handleContinue()` clears the multiple-login alert (`store.setShowMultipleLoginAlert(false)`) then calls `store.registerCC()` to force re-registration | Agents already logged in elsewhere must be able to continue/take over the session | `src/helper.ts` (`handleContinue`) | `tests/helper.ts` "should call handleContinue and set device type", "should call handleContinue with agent not logged in", "should call handleContinue and handle error" | none | PRESENT | -| `STATION-LOGIN-R-008` | `handleCCSignOut()` calls `cc.stationLogout()` then `cc.deregister()` only when `doStationLogout` AND `isAgentLoggedIn`; otherwise it skips straight to invoking `onCCSignOut()`. `doStationLogout` defaults to `true` when omitted | Lets profile-mode hosts sign out without dropping the station, while default behavior fully logs out | `src/helper.ts` (`handleCCSignOut`, `doStationLogout` default) | `tests/helper.ts` "should call stationLogout when doStationLogout is not passed", "should not call stationLogout if doStationLogout is false", "should handle error if stationLogout fails in onCCSignOut", "should handle error if deregister fails in onCCSignOut" | none | PRESENT | -| `STATION-LOGIN-R-009` | The widget is wrapped in an `ErrorBoundary` that renders an empty fragment and routes the error to `store.onErrorCallback('StationLogin', error)` | A render/hook error must not blank-crash the host and must be reported with the component name | `src/station-login/index.tsx` (`ErrorBoundary`) | `tests/station-login/index.tsx` "should render empty fragment when ErrorBoundary catches an error" | none | PRESENT | -| `STATION-LOGIN-R-010` | `StationLoginInternal` is an `observer()` that reads store state and forwards it plus hook results into `StationLoginComponent` (including `dialNumberRegex = cc?.agentConfig?.regexUS`, `hideDesktopLogin`, `allowInternationalDn`) | Re-render must be driven by observable store changes and props must reach the presentational component intact | `src/station-login/index.tsx` (`StationLoginInternal`) | `tests/station-login/index.tsx` "renders StationLoginPresentational with correct props" | DN-regex selection (`allowInternationalDn`) is enforced inside `@webex/cc-components`, not this package | PRESENT | + +| ID | WHAT | WHY | Source Evidence | Test / Example Evidence | Assumptions / Gaps | Confidence | +| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------- | +| `STATION-LOGIN-R-001` | `login()` calls `cc.stationLogin({teamId, loginOption, dialNumber})`; on success sets `loginSuccess` and clears `loginFailure`, on failure sets `loginFailure` and clears `loginSuccess` | Login result must drive UI success/error display | `src/helper.ts` (`login`) | `tests/helper.ts` "should set loginSuccess on successful login and set loginFailure to undefined", "should set loginFailure on failed login" | none | PRESENT | +| `STATION-LOGIN-R-002` | `logout()` calls `cc.stationLogout({logoutReason})` and sets `logoutSuccess` on success; on failure it logs and does not throw | Logout must update state and never crash the widget | `src/helper.ts` (`logout`) | `tests/helper.ts` "should set logoutSuccess on successful logout", "should log error on logout failure" | none | PRESENT | +| `STATION-LOGIN-R-003` | The `onLogin` callback fires when the agent becomes logged in (on `AGENT_STATION_LOGIN_SUCCESS` and on mount when already logged in); `onLogout` fires on `AGENT_LOGOUT_SUCCESS`. Both are guarded so absent callbacks are a no-op | Host app needs login/logout lifecycle hooks without crashing when omitted | `src/helper.ts` (`handleLogin`/`handleLogout`, `setCCCallback` effect, mount effect) | `tests/helper.ts` "should set loginSuccess on successful login without onLogin callback", "should not call logout callback if not present" | none | PRESENT | +| `STATION-LOGIN-R-004` | `saveLoginOptions()` short-circuits when `isLoginOptionsChanged` is false: sets `saveError` to "No changes detected…" and calls `onSaveEnd(false)` without calling the SDK | Avoids no-op profile writes and gives the host a deterministic failure signal | `src/helper.ts` (`saveLoginOptions`, `isLoginOptionsChanged`) | `tests/helper.ts` "should not save if isLoginOptionsChanged is false" | none | PRESENT | +| `STATION-LOGIN-R-005` | On a real change, `saveLoginOptions()` calls `onSaveStart()`, calls `cc.updateAgentProfile()` with `{loginOption, teamId}` (plus `dialNumber` only when deviceType ≠ `BROWSER`), and on success copies `currentLoginOptions`→`originalLoginOptions` and calls `onSaveEnd(true)` | Profile update must persist only changed options and resync the baseline so the Save button disables | `src/helper.ts` (`saveLoginOptions`) | `tests/helper.ts` "should call updateAgentProfile and update originalLoginOptions on save when changed", "should call updateAgentProfile with no dialNumber when deviceType is BROWSER" | none | PRESENT | +| `STATION-LOGIN-R-006` | When `cc.updateAgentProfile()` rejects, `saveError` is set to the error message and `onSaveEnd(false)` is called | Caller must be able to surface profile-save failures | `src/helper.ts` (`saveLoginOptions` `.catch`) | `tests/helper.ts` "should handle updateAgentProfile errors", "should handle errors in saveLoginOptions main logic" | none | PRESENT | +| `STATION-LOGIN-R-007` | `handleContinue()` clears the multiple-login alert (`store.setShowMultipleLoginAlert(false)`) then calls `store.registerCC()` to force re-registration | Agents already logged in elsewhere must be able to continue/take over the session | `src/helper.ts` (`handleContinue`) | `tests/helper.ts` "should call handleContinue and set device type", "should call handleContinue with agent not logged in", "should call handleContinue and handle error" | none | PRESENT | +| `STATION-LOGIN-R-008` | `handleCCSignOut()` calls `cc.stationLogout()` then `cc.deregister()` only when `doStationLogout` AND `isAgentLoggedIn`; otherwise it skips straight to invoking `onCCSignOut()`. `doStationLogout` defaults to `true` when omitted | Lets profile-mode hosts sign out without dropping the station, while default behavior fully logs out | `src/helper.ts` (`handleCCSignOut`, `doStationLogout` default) | `tests/helper.ts` "should call stationLogout when doStationLogout is not passed", "should not call stationLogout if doStationLogout is false", "should handle error if stationLogout fails in onCCSignOut", "should handle error if deregister fails in onCCSignOut" | none | PRESENT | +| `STATION-LOGIN-R-009` | The widget is wrapped in an `ErrorBoundary` that renders an empty fragment and routes the error to `store.onErrorCallback('StationLogin', error)` | A render/hook error must not blank-crash the host and must be reported with the component name | `src/station-login/index.tsx` (`ErrorBoundary`) | `tests/station-login/index.tsx` "should render empty fragment when ErrorBoundary catches an error" | none | PRESENT | +| `STATION-LOGIN-R-010` | `StationLoginInternal` is an `observer()` that reads store state and forwards it plus hook results into `StationLoginComponent` (including `dialNumberRegex = cc?.agentConfig?.regexUS`, `hideDesktopLogin`, `allowInternationalDn`) | Re-render must be driven by observable store changes and props must reach the presentational component intact | `src/station-login/index.tsx` (`StationLoginInternal`) | `tests/station-login/index.tsx` "renders StationLoginPresentational with correct props" | DN-regex selection (`allowInternationalDn`) is enforced inside `@webex/cc-components`, not this package | PRESENT | +| `STATION-LOGIN-R-011` | `checkE911ModalDisplay(deviceType)` is invoked as a gate (no-op unless `deviceType === DEVICE_TYPE_BROWSER`) from every point where the agent can end up on a `BROWSER` station: (1) after a successful `login()` using the just-submitted device type, (2) on mount when `store.isAgentLoggedIn` is already `true` with `store.deviceType === BROWSER` (covers page refresh), (3) inside `handleLogin` — the `AGENT_STATION_LOGIN_SUCCESS`/relogin callback — using `store.deviceType`, (4) after a successful multi-login takeover in `handleContinue()`, and (5) after a successful `saveLoginOptions()` profile save using the newly-saved `currentLoginOptions.deviceType` | The E911 emergency-dialing reminder must reach every agent who ends up on a `BROWSER`/WebRTC station regardless of how they got there (first login, refresh, relogin/takeover, or a profile-save device-type switch), not only the initial-login path | `src/helper.ts` (`checkE911ModalDisplay`, `login`, mount `useEffect`, `handleLogin`, `handleContinue`, `saveLoginOptions`) | `tests/helper.ts` `describe('checkE911ModalDisplay (via login)')`: "should show the E911 modal on successful BROWSER login when not already acknowledged", "should show the E911 modal on mount when already logged in with BROWSER (e.g. page refresh)", "should not show the E911 modal on mount when already logged in with a non-BROWSER device", "should show the E911 modal on AGENT_STATION_LOGIN_SUCCESS / relogin when store.deviceType is BROWSER", "should show the E911 modal after a successful multi-login takeover (handleContinue) with BROWSER", "should not show the E911 modal after handleContinue when store.deviceType is not BROWSER", "should show the E911 modal after saving login options with BROWSER as the new device type" | none | PRESENT | +| `STATION-LOGIN-R-012` | `checkE911ModalDisplay()` returns early when `deviceTypeToCheck !== DEVICE_TYPE_BROWSER`; otherwise it `await`s `store.fetchUserPreferences()` and calls `store.setShowE911Modal(true)` only when `store.isEmergencyModalAlreadyDisplayed` is falsy. The whole method is wrapped in try/catch that logs and swallows errors, so a preference-service failure never breaks login/relogin/profile-save | The modal must not reappear for agents who already acknowledged the reminder, and reading the acknowledgment state from the preference service must never crash the login/profile-save flow it's attached to | `src/helper.ts` (`checkE911ModalDisplay`) | `tests/helper.ts` "should skip the E911 modal for non-BROWSER device types", "should skip the E911 modal when the preference is already acknowledged", error-handling case asserting `logger.error` with `method: 'checkE911ModalDisplay'` | The `desktopPreference` JSON parse/merge and the persisted acknowledgment write — including which identifier (`userId` vs. CC `agentId`) is used — are owned by `@webex/cc-store` (`store/src/storeEventsWrapper.ts` `fetchUserPreferences`/`updateEmergencyModalAcknowledgment`); see that package's spec for the persistence contract, not duplicated here | PRESENT | +| `STATION-LOGIN-R-013` | This package neither renders nor imports `E911Modal` — it only requests display via `store.setShowE911Modal(true)` (R-012). The modal's Cancel-only dismissal (built-in close ("X") button hidden via `::part(dialog-close-btn)`, and the Dialog's native `close` event — fired by the close button or Escape — intentionally left unhandled so it cannot invoke `onCancel`) is implemented entirely inside the `E911Modal` component in `@webex/cc-components` | Documents the module boundary so dismiss-behavior changes are made in `@webex/cc-components`, not searched for in this package | `../../cc-components/src/components/StationLogin/E911Modal/e911-modal.tsx`, `../../cc-components/src/components/StationLogin/E911Modal/e911-modal.style.scss` | `../../cc-components/tests/components/StationLogin/E911Modal/e911-modal.test.tsx` "should call onCancel when Cancel button is clicked", "should not call onCancel when the Dialog fires close (built-in close button or Escape key) - Cancel is the only way to dismiss" | Full requirement/test mapping for the modal's own behavior belongs in `cc-components-spec.md`, not this file | PRESENT | ## Design Overview + The widget is intentionally thin. `StationLogin` exists only to provide the `ErrorBoundary`; the real work is in `StationLoginInternal`, an `observer()` that pulls observable state off the store singleton and constructs the `StationLoginComponentProps` object. All mutating behavior is delegated to `useStationLogin`, @@ -150,6 +166,7 @@ and `AGENT_LOGOUT_SUCCESS`. The cleanup/`removeCCCallback` is intentionally comm avoid tearing down the shared store-level event wrapper. ## Data Flow + In-process React/MobX data flow (no network/queue transport owned by this module — the SDK owns the wire). Inputs are host props and observable store state; outputs are SDK calls and host callbacks. @@ -168,15 +185,16 @@ graph LR ``` ## Sequence Diagram(s) + Sequence coverage: -| Operation group | Diagram | Failure / recovery coverage | -|---|---|---| -| Station login | "Login flow" | `alt` success vs. failure branch (`loginFailure` set) | -| Station logout | folded into login diagram's `AGENT_LOGOUT_SUCCESS` path | logout `.catch` logs and no-ops | -| Profile option save | "Profile save flow" | no-change short-circuit + `updateAgentProfile` reject branch | -| CC sign-out | "CC sign-out flow" | conditional station logout/deregister + their failure handling | -| Multiple-login Continue | "Multiple-login Continue flow" | re-register failure logged | +| Operation group | Diagram | Failure / recovery coverage | +| ----------------------- | ------------------------------------------------------- | -------------------------------------------------------------- | +| Station login | "Login flow" | `alt` success vs. failure branch (`loginFailure` set) | +| Station logout | folded into login diagram's `AGENT_LOGOUT_SUCCESS` path | logout `.catch` logs and no-ops | +| Profile option save | "Profile save flow" | no-change short-circuit + `updateAgentProfile` reject branch | +| CC sign-out | "CC sign-out flow" | conditional station logout/deregister + their failure handling | +| Multiple-login Continue | "Multiple-login Continue flow" | re-register failure logged | ```mermaid sequenceDiagram @@ -267,6 +285,7 @@ sequenceDiagram ``` ## Class / Component Relationships + ```mermaid graph TD StationLogin -->|wraps| StationLoginInternal @@ -279,6 +298,7 @@ graph TD UseStationLoginProps -.Pick.-> IStationLoginProps StationLoginComponentProps -.Pick.-> IStationLoginProps ``` + `StationLogin` (exported) is a plain FC that mounts an `ErrorBoundary` around `StationLoginInternal`, an `observer()` FC. `StationLoginInternal` composes the `useStationLogin` hook's return with store-derived props into `StationLoginComponentProps` and renders the presentational `StationLoginComponent` from @@ -287,6 +307,7 @@ and the component prop type are all `Pick`s of the single canonical `IStationLog cc-components, so prop shapes never diverge. ## Use Cases + - **UC-1 Agent login:** Agent selects team + device type (+ dial number for `EXTENSION`/`AGENT_DN`), clicks Login → `login()` → `cc.stationLogin()` → success sets `loginSuccess`, `AGENT_STATION_LOGIN_SUCCESS` fires `onLogin`, widget re-renders logged-in. Evidence: `src/helper.ts`, `tests/helper.ts`. UI flow: login form → loading → logged-in view or inline error. - **UC-2 Agent logout:** Agent clicks Logout → `logout()` → `cc.stationLogout()` → `logoutSuccess` set, `onLogout` fires. Evidence: `src/helper.ts`, `tests/helper.ts`. UI flow: logged-in view → login form. - **UC-3 Update profile options:** In `profileMode`, agent edits device type/team/dial number → `isLoginOptionsChanged` enables Save → `saveLoginOptions()` → `cc.updateAgentProfile()` → `onSaveEnd(true)` and Save disables. Evidence: `src/helper.ts`, `tests/helper.ts`. UI flow: profile form → Save (disabled until changed) → success/error message. @@ -294,6 +315,7 @@ cc-components, so prop shapes never diverge. - **UC-5 Multiple-login Continue:** Agent already logged in elsewhere → store sets `showMultipleLoginAlert` → alert shown → agent clicks Continue → `handleContinue()` clears alert + `registerCC()` takes over the session. Evidence: `src/helper.ts`, `tests/helper.ts`. UI flow: alert dialog → Continue → logged-in view. ## UI Flow + - **Login screen (logged out):** Team dropdown, login-option/device-type selector (`EXTENSION`, `AGENT_DN`, Desktop/`BROWSER`), dial-number field (shown for non-`BROWSER` types), Login button. Desktop option hidden when `hideDesktopLogin` is set. - **Logged-in view:** Logout and/or Sign Out actions. - **Profile mode (`profileMode=true`):** Same selectors plus a Save button that is disabled until `isLoginOptionsChanged` is true. @@ -301,17 +323,19 @@ cc-components, so prop shapes never diverge. - **Validation:** Dial-number validation uses `dialNumberRegex` (`cc.agentConfig.regexUS`) or international regex when `allowInternationalDn` is set; the regex selection/validation itself is enforced in `@webex/cc-components`. ## Error Handling & Failure Modes -| Condition | Signal (error/code/result) | Caller recovery | -|---|---|---| -| `cc.stationLogin()` rejects | `loginFailure` set (Error), `loginSuccess` cleared; logged | Surface `loginFailure` in UI; agent retries | -| `cc.stationLogout()` rejects | Logged via `logger.error`; no state change, no throw | None required; agent may retry logout | -| `cc.updateAgentProfile()` rejects | `saveError` = error message; `onSaveEnd(false)` | Host shows error; agent edits and re-saves | -| No changed login options | `saveError` = "No changes detected…"; `onSaveEnd(false)` | None; expected no-op | -| `stationLogout`/`deregister` fail during CC sign-out | Logged; `onCCSignOut()` still invoked | Host proceeds with app sign-out | -| `registerCC()` fails on Continue | Logged ("Agent Relogin Failed" / caught error) | Agent retries Continue | -| Render/hook throws | ErrorBoundary → empty fragment + `store.onErrorCallback('StationLogin', error)` | Host's error callback surfaces a notification | + +| Condition | Signal (error/code/result) | Caller recovery | +| ---------------------------------------------------- | ------------------------------------------------------------------------------- | --------------------------------------------- | +| `cc.stationLogin()` rejects | `loginFailure` set (Error), `loginSuccess` cleared; logged | Surface `loginFailure` in UI; agent retries | +| `cc.stationLogout()` rejects | Logged via `logger.error`; no state change, no throw | None required; agent may retry logout | +| `cc.updateAgentProfile()` rejects | `saveError` = error message; `onSaveEnd(false)` | Host shows error; agent edits and re-saves | +| No changed login options | `saveError` = "No changes detected…"; `onSaveEnd(false)` | None; expected no-op | +| `stationLogout`/`deregister` fail during CC sign-out | Logged; `onCCSignOut()` still invoked | Host proceeds with app sign-out | +| `registerCC()` fails on Continue | Logged ("Agent Relogin Failed" / caught error) | Agent retries Continue | +| Render/hook throws | ErrorBoundary → empty fragment + `store.onErrorCallback('StationLogin', error)` | Host's error callback surfaces a notification | ## Pitfalls + - **CC callback cleanup is intentionally disabled.** The `useEffect` that registers `setCCCallback` does NOT call `removeCCCallback` on unmount (commented out in `helper.ts`) because doing so tore down the shared store-level event wrapper for all consumers. Re-adding naive cleanup will break login/logout events repo-wide. - **`login()` uses hook-prop values, not the local form state.** `cc.stationLogin` is called with `{teamId: team, loginOption: deviceType, dialNumber}` where `deviceType`/`dialNumber` come from store-derived props and `team` from local `setTeam` state — not from `selectedDeviceType`/`dialNumberValue`. When changing the login payload, trace which value actually feeds `cc.stationLogin`. - **`isLoginOptionsChanged` ignores `dialNumber` for `BROWSER`.** Desktop/WebRTC has no dial number, so the comparison skips it; a stale `dialNumber` will not (and must not) enable Save in `BROWSER` mode. @@ -319,23 +343,27 @@ cc-components, so prop shapes never diverge. - **Archived-doc drift.** The pre-migration ARCHITECTURE diagram showed a `store.login()` call; the real code calls `cc.stationLogin()` directly from the hook. Trust `src/helper.ts`. ## Module Do's / Don'ts + - DO: route all SDK access through `props.cc` (from `store.cc`) inside the hook; never import the SDK directly. - DO: wrap every hook method body in try/catch and log via the SDK `logger` with `{module, method}` metadata. - DON'T: reintroduce `removeCCCallback` cleanup without verifying the shared store event wrapper survives. - DON'T: redefine prop shapes locally — `Pick` from `IStationLoginProps` in `@webex/cc-components`. ## Export Stability + `src/index.ts` re-exports only `StationLogin`. Adding an optional prop to `StationLoginProps` is a minor change; removing/renaming a prop, changing a callback signature, or renaming the `widget-cc-station-login` custom element is a major (breaking) change. Type surface ships from `dist/types/index.d.ts`. ## Host Integration & Theming + Consumed via `@webex/cc-widgets`, which wraps `StationLogin` as the custom element `widget-cc-station-login` (r2wc). The store must be initialized (`store.init(...)`) before the widget renders. Hosts typically wrap it in Momentum `ThemeProvider`/`IconProvider`; peer deps require `react`/`react-dom` `>=18.3.1` and `@momentum-ui/react-collaboration` `>=26.201.9`. Error reporting is wired through `store.onErrorCallback`. ## Test-Case Strategy (module) + Hook tests (`tests/helper.ts`) exercise login success/failure, logout success/failure, callback-present and callback-absent paths, profile save (no-change short-circuit, changed save, `BROWSER` no-dial-number, update-profile error), `handleContinue` (logged-in, not-logged-in, error), and CC sign-out @@ -344,20 +372,24 @@ asserting that try/catch swallows errors in each method. Widget tests (`tests/st assert prop wiring into `useStationLogin` and the ErrorBoundary empty-fragment behavior. Each major method has both a positive and a negative case. -| Behavior / Requirement | Existing test evidence | Gap | -|---|---|---| -| `STATION-LOGIN-R-001` | `tests/helper.ts` login success/failure cases | none | -| `STATION-LOGIN-R-002` | `tests/helper.ts` logout success + failure | none | -| `STATION-LOGIN-R-003` | `tests/helper.ts` callback-present / callback-absent | mount-already-logged-in `onLogin` path is implicit, not a dedicated assertion | -| `STATION-LOGIN-R-004` | `tests/helper.ts` "should not save if isLoginOptionsChanged is false" | none | -| `STATION-LOGIN-R-005` | `tests/helper.ts` save-when-changed + `BROWSER` no-dial-number | none | -| `STATION-LOGIN-R-006` | `tests/helper.ts` updateAgentProfile error cases | none | -| `STATION-LOGIN-R-007` | `tests/helper.ts` handleContinue (3 cases) | none | -| `STATION-LOGIN-R-008` | `tests/helper.ts` `#onCCSignOut` block (4 cases) | none | -| `STATION-LOGIN-R-009` | `tests/station-login/index.tsx` ErrorBoundary test | none | -| `STATION-LOGIN-R-010` | `tests/station-login/index.tsx` "renders … with correct props" | `hideDesktopLogin`/`allowInternationalDn` forwarding not asserted directly | +| Behavior / Requirement | Existing test evidence | Gap | +| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | +| `STATION-LOGIN-R-001` | `tests/helper.ts` login success/failure cases | none | +| `STATION-LOGIN-R-002` | `tests/helper.ts` logout success + failure | none | +| `STATION-LOGIN-R-003` | `tests/helper.ts` callback-present / callback-absent | mount-already-logged-in `onLogin` path is implicit, not a dedicated assertion | +| `STATION-LOGIN-R-004` | `tests/helper.ts` "should not save if isLoginOptionsChanged is false" | none | +| `STATION-LOGIN-R-005` | `tests/helper.ts` save-when-changed + `BROWSER` no-dial-number | none | +| `STATION-LOGIN-R-006` | `tests/helper.ts` updateAgentProfile error cases | none | +| `STATION-LOGIN-R-007` | `tests/helper.ts` handleContinue (3 cases) | none | +| `STATION-LOGIN-R-008` | `tests/helper.ts` `#onCCSignOut` block (4 cases) | none | +| `STATION-LOGIN-R-009` | `tests/station-login/index.tsx` ErrorBoundary test | none | +| `STATION-LOGIN-R-010` | `tests/station-login/index.tsx` "renders … with correct props" | `hideDesktopLogin`/`allowInternationalDn` forwarding not asserted directly | +| `STATION-LOGIN-R-011` | `tests/helper.ts` `checkE911ModalDisplay (via login)` — 5 trigger-point cases (login, mount/refresh, relogin, multi-login takeover, profile save) | none | +| `STATION-LOGIN-R-012` | `tests/helper.ts` non-`BROWSER` skip, already-acknowledged skip, error-swallowing cases | Persistence correctness (merge, userId) is validated in `store/tests/storeEventsWrapper.ts`, not here | +| `STATION-LOGIN-R-013` | `cc-components/tests/.../e911-modal.test.tsx` Cancel-button and native-`close`-event cases | Boundary-only entry; no test exists (or is expected) in this package | ## Traceability + - Repo architecture: `../../../../ai-docs/ARCHITECTURE.md` · Registry: `../../../../ai-docs/SPEC_INDEX.md` - Contracts: `../../../../ai-docs/CONTRACTS.md` (`cc-widgets.StationLogin`) - Coverage state & contracts baseline: `.sdd/manifest.json` From 348060aaf3efc2a8d74b0749e7505738efe540f3 Mon Sep 17 00:00:00 2001 From: Matthew Olker Date: Mon, 20 Jul 2026 13:01:14 -0400 Subject: [PATCH 08/14] fix(store): reset E911 modal state on logout cleanUpStore() reset store.isAgentLoggedIn/deviceType/etc on logout but never cleared showE911Modal/isEmergencyModalAlreadyDisplayed. If the SDK logged the agent out while the E911 dialog was open (e.g. an external agent:logoutSuccess), the modal/acknowledgment state leaked into the logged-out view or the next session. Reset both fields alongside the rest of the logout state. --- .../store/src/storeEventsWrapper.ts | 2 ++ .../store/tests/storeEventsWrapper.ts | 26 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/packages/contact-center/store/src/storeEventsWrapper.ts b/packages/contact-center/store/src/storeEventsWrapper.ts index 544eaaabb..88757dc51 100644 --- a/packages/contact-center/store/src/storeEventsWrapper.ts +++ b/packages/contact-center/store/src/storeEventsWrapper.ts @@ -1239,6 +1239,8 @@ class StoreWrapper implements IStoreWrapper { this.store.acceptedCampaignIds = new Set(); this.realtimeTranscriptionListeners = {}; this.setLastConsultDestination(null); + this.setShowE911Modal(false); + this.setIsEmergencyModalAlreadyDisplayed(false); }); }; diff --git a/packages/contact-center/store/tests/storeEventsWrapper.ts b/packages/contact-center/store/tests/storeEventsWrapper.ts index ba77f926c..89a7e7478 100644 --- a/packages/contact-center/store/tests/storeEventsWrapper.ts +++ b/packages/contact-center/store/tests/storeEventsWrapper.ts @@ -1733,6 +1733,32 @@ describe('storeEventsWrapper', () => { expect(storeWrapper['store'].agentProfile).toEqual({}); }); + it('should reset showE911Modal and isEmergencyModalAlreadyDisplayed on logout', async () => { + const cc = storeWrapper['store'].cc; + const onSpy = jest.spyOn(cc, 'on'); + storeWrapper['store'].init = jest.fn().mockImplementation((_options, setupIncomingTaskHandler) => { + setupIncomingTaskHandler(cc); + return Promise.resolve(); + }); + + await storeWrapper.init(options); + + storeWrapper['store'].showE911Modal = true; + storeWrapper['store'].isEmergencyModalAlreadyDisplayed = true; + + act(() => { + onSpy.mock.calls[1][1]({}); + }); + + act(() => { + const logOutCb = onSpy.mock.calls.find((call) => call[0] === CC_EVENTS.AGENT_LOGOUT_SUCCESS)[1]; + logOutCb({}); + }); + + expect(storeWrapper['store'].showE911Modal).toBe(false); + expect(storeWrapper['store'].isEmergencyModalAlreadyDisplayed).toBe(false); + }); + it('should handle task rejection event and call onTaskRejected with the provided reason', () => { const rejectTask = makeMockTask({ data: {interactionId: 'rejectTest', interaction: {state: 'connected'}}, From 1a5c5d49595634b5ddad579acb487efd05893cf7 Mon Sep 17 00:00:00 2001 From: Matthew Olker Date: Mon, 20 Jul 2026 13:24:39 -0400 Subject: [PATCH 09/14] fix(station-login): make checkE911ModalDisplay single-flight A single BROWSER login can trigger checkE911ModalDisplay() from two paths at once - login()'s own success handler and the AGENT_STATION_LOGIN_SUCCESS callback (handleLogin) - each starting an independent fetchUserPreferences() call. If one of those reads resolved after the user already saved/canceled the modal, it could overwrite isEmergencyModalAlreadyDisplayed back to false and reopen the modal. Guard checkE911ModalDisplay with an in-flight promise ref so concurrent callers join the same fetchUserPreferences() call instead of racing. --- .../station-login/src/helper.ts | 52 +++++++++++++------ .../station-login/tests/helper.ts | 48 +++++++++++++++++ 2 files changed, 84 insertions(+), 16 deletions(-) diff --git a/packages/contact-center/station-login/src/helper.ts b/packages/contact-center/station-login/src/helper.ts index fc60d056d..55ecacd37 100644 --- a/packages/contact-center/station-login/src/helper.ts +++ b/packages/contact-center/station-login/src/helper.ts @@ -1,4 +1,4 @@ -import {useEffect, useState} from 'react'; +import {useEffect, useRef, useState} from 'react'; import {LogoutSuccess, AgentProfileUpdate, LoginOption, StationLoginSuccessResponse} from '@webex/contact-center'; import {UseStationLoginProps} from './station-login/station-login.types'; import store, {CC_EVENTS, DEVICE_TYPE_BROWSER} from '@webex/cc-store'; // we need to import as we are losing the context of this in store @@ -54,6 +54,9 @@ export const useStationLogin = (props: UseStationLoginProps) => { // Error for Save button const [saveError, setSaveError] = useState(''); + // Single-flight guard for checkE911ModalDisplay - see its usage below + const e911CheckInFlightRef = useRef | null>(null); + // Set original login options after successful login useEffect(() => { try { @@ -250,27 +253,44 @@ export const useStationLogin = (props: UseStationLoginProps) => { } }; - const checkE911ModalDisplay = async (deviceTypeToCheck: string) => { - try { - if (deviceTypeToCheck !== DEVICE_TYPE_BROWSER) { - return; - } + const checkE911ModalDisplay = (deviceTypeToCheck: string): Promise => { + if (deviceTypeToCheck !== DEVICE_TYPE_BROWSER) { + return Promise.resolve(); + } - await store.fetchUserPreferences(); + // A single login can trigger this from more than one path at once (e.g. login()'s own + // success handler racing the AGENT_STATION_LOGIN_SUCCESS callback). Make the check + // single-flight so concurrent callers join the same fetchUserPreferences() call instead of + // each starting their own - two independent reads could otherwise race with the user's + // Save/Cancel action and reopen the modal with a stale "not yet acknowledged" result. + if (e911CheckInFlightRef.current) { + return e911CheckInFlightRef.current; + } + + const check = (async () => { + try { + await store.fetchUserPreferences(); - if (!store.isEmergencyModalAlreadyDisplayed) { - store.setShowE911Modal(true); - logger.log('CC-Widgets: E911 modal displayed for BROWSER login', { + if (!store.isEmergencyModalAlreadyDisplayed) { + store.setShowE911Modal(true); + logger.log('CC-Widgets: E911 modal displayed for BROWSER login', { + module: 'widget-station-login#helper.ts', + method: 'checkE911ModalDisplay', + }); + } + } catch (error) { + logger.error(`CC-Widgets: Error checking E911 modal display - ${error.message}`, { module: 'widget-station-login#helper.ts', method: 'checkE911ModalDisplay', }); + } finally { + e911CheckInFlightRef.current = null; } - } catch (error) { - logger.error(`CC-Widgets: Error checking E911 modal display - ${error.message}`, { - module: 'widget-station-login#helper.ts', - method: 'checkE911ModalDisplay', - }); - } + })(); + + e911CheckInFlightRef.current = check; + + return check; }; const login = () => { diff --git a/packages/contact-center/station-login/tests/helper.ts b/packages/contact-center/station-login/tests/helper.ts index 85c79606f..6ad0548dc 100644 --- a/packages/contact-center/station-login/tests/helper.ts +++ b/packages/contact-center/station-login/tests/helper.ts @@ -1178,6 +1178,54 @@ describe('useStationLogin Hook', () => { }); }); + it('should dedupe concurrent checkE911ModalDisplay calls (login() racing AGENT_STATION_LOGIN_SUCCESS) into a single fetchUserPreferences() call', async () => { + ccMock.stationLogin.mockResolvedValue(successResponse); + (store as unknown as {deviceType: string}).deviceType = 'BROWSER'; + + let resolveFetch: () => void = () => {}; + (store.fetchUserPreferences as jest.Mock).mockImplementation( + () => + new Promise((resolve) => { + resolveFetch = resolve; + }) + ); + + jest.spyOn(store, 'setCCCallback').mockImplementation((event, cb) => { + ccMock.on(event, cb); + }); + const onSpy = jest.spyOn(ccMock, 'on'); + + const {result} = renderHook(() => + useStationLogin({ + ...baseStationLoginProps, + deviceType: 'BROWSER', + }) + ); + + const loginSuccessCb = onSpy.mock.calls.find((call) => call[0] === CC_EVENTS.AGENT_STATION_LOGIN_SUCCESS)[1] as ( + payload: unknown + ) => void; + + act(() => { + // Simulate the real race: login()'s own success handler and the SDK's + // AGENT_STATION_LOGIN_SUCCESS callback both firing for the same login before + // fetchUserPreferences() resolves. + result.current.login(); + loginSuccessCb({deviceType: 'BROWSER', auxCodeId: '0'}); + }); + + await act(async () => { + resolveFetch(); + // Flush pending microtasks so both callers' continuations run. + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + }); + + expect(store.fetchUserPreferences).toHaveBeenCalledTimes(1); + expect(store.setShowE911Modal).toHaveBeenCalledWith(true); + }); + it('should show the E911 modal after a successful multi-login takeover (handleContinue) with BROWSER', async () => { (store.fetchUserPreferences as jest.Mock).mockResolvedValue(undefined); store.setIsAgentLoggedIn(true); From d221975490f2dbb15437d10caff29c776233385d Mon Sep 17 00:00:00 2001 From: Matthew Olker Date: Mon, 20 Jul 2026 13:44:35 -0400 Subject: [PATCH 10/14] fix(store): handle missing user-preference record for first-time E911 check getUserPreference() 404s for a first-time user who has no preference row yet. fetchUserPreferences() was rethrowing that error, which checkE911ModalDisplay() then swallowed - so first-time BROWSER agents never saw the E911 modal. updateEmergencyModalAcknowledgment() then called updateUserPreference() (update-only) with no userId, which would also fail for that same user on Save. - fetchUserPreferences(): treat a 404 from getUserPreference() as isEmergencyModalAlreadyDisplayed=false instead of rethrowing. - updateEmergencyModalAcknowledgment(): on a 404, call createUserPreference({userId: agentId, desktopPreference}) instead of updateUserPreference(), since there is no existing record (and no preference-service userId) to update. - Add createUserPreference to the local IUserPreferenceService shim type (store.types.ts) pending the real SDK types (CAI-7906). --- .../contact-center/store/src/store.types.ts | 10 +++ .../store/src/storeEventsWrapper.ts | 50 +++++++++++++-- .../store/tests/storeEventsWrapper.ts | 63 +++++++++++++++++++ 3 files changed, 118 insertions(+), 5 deletions(-) diff --git a/packages/contact-center/store/src/store.types.ts b/packages/contact-center/store/src/store.types.ts index cee641c6c..5dc2b3ceb 100644 --- a/packages/contact-center/store/src/store.types.ts +++ b/packages/contact-center/store/src/store.types.ts @@ -390,6 +390,15 @@ export type UserPreferenceResponse = { lastUpdatedTime?: number; }; +/** + * Request payload for creating user preferences. + * @public + */ +export type CreateUserPreferenceRequest = { + userId: string; + desktopPreference: string; +}; + /** * Request payload for updating user preferences. * @public @@ -415,6 +424,7 @@ export type GetUserPreferenceParams = { */ export interface IUserPreferenceService { getUserPreference(params?: GetUserPreferenceParams): Promise; + createUserPreference(data: CreateUserPreferenceRequest): Promise; updateUserPreference(userId: string, data: UpdateUserPreferenceRequest): Promise; } diff --git a/packages/contact-center/store/src/storeEventsWrapper.ts b/packages/contact-center/store/src/storeEventsWrapper.ts index 88757dc51..70ed18548 100644 --- a/packages/contact-center/store/src/storeEventsWrapper.ts +++ b/packages/contact-center/store/src/storeEventsWrapper.ts @@ -611,7 +611,27 @@ class StoreWrapper implements IStoreWrapper { throw new Error('userPreference service not available'); } - const response = await this.store.cc.userPreference.getUserPreference(); + let response; + try { + response = await this.store.cc.userPreference.getUserPreference(); + } catch (getError) { + if ((getError as {statusCode?: number})?.statusCode === 404) { + // First-time user: no preference record has been created yet. This is not a failure - + // treat it the same as "not yet acknowledged" so the E911 modal can still be shown. + this.store.logger.info('CC-Widgets: fetchUserPreferences(): no user preference record exists yet', { + module: 'storeEventsWrapper.ts', + method: 'fetchUserPreferences', + }); + runInAction(() => { + this.store.isEmergencyModalAlreadyDisplayed = false; + }); + + return; + } + + throw getError; + } + const desktopPrefString = response?.desktopPreference; let isEmergencyModalAlreadyDisplayed = false; @@ -655,7 +675,20 @@ class StoreWrapper implements IStoreWrapper { throw new Error('userPreference service not available'); } - const response = await this.store.cc.userPreference.getUserPreference(); + let response; + let hasExistingRecord = true; + try { + response = await this.store.cc.userPreference.getUserPreference(); + } catch (getError) { + if ((getError as {statusCode?: number})?.statusCode === 404) { + // First-time user: no preference record exists yet, so there's nothing to merge and + // no userId to reuse. Fall through and create the record below. + hasExistingRecord = false; + } else { + throw getError; + } + } + const existingDesktopPrefString = response?.desktopPreference; let existingDesktopPref = {}; @@ -679,9 +712,16 @@ class StoreWrapper implements IStoreWrapper { isEmergencyModalAlreadyDisplayed: true, }); - await this.store.cc.userPreference.updateUserPreference(response?.userId, { - desktopPreference, - }); + if (hasExistingRecord) { + await this.store.cc.userPreference.updateUserPreference(response?.userId, { + desktopPreference, + }); + } else { + await this.store.cc.userPreference.createUserPreference({ + userId: this.store.agentId, + desktopPreference, + }); + } runInAction(() => { this.store.isEmergencyModalAlreadyDisplayed = true; diff --git a/packages/contact-center/store/tests/storeEventsWrapper.ts b/packages/contact-center/store/tests/storeEventsWrapper.ts index 89a7e7478..a41c986b1 100644 --- a/packages/contact-center/store/tests/storeEventsWrapper.ts +++ b/packages/contact-center/store/tests/storeEventsWrapper.ts @@ -2614,6 +2614,7 @@ describe('storeEventsWrapper', () => { getUserPreference: jest.fn().mockResolvedValue({ desktopPreference: JSON.stringify({isEmergencyModalAlreadyDisplayed: true}), }), + createUserPreference: jest.fn(), updateUserPreference: jest.fn(), }; storeWrapper['store'].cc.userPreference = mockUserPreference; @@ -2629,6 +2630,7 @@ describe('storeEventsWrapper', () => { getUserPreference: jest.fn().mockResolvedValue({ desktopPreference: null, }), + createUserPreference: jest.fn(), updateUserPreference: jest.fn(), }; storeWrapper['store'].cc.userPreference = mockUserPreference; @@ -2645,6 +2647,7 @@ describe('storeEventsWrapper', () => { getUserPreference: jest.fn().mockResolvedValue({ desktopPreference: null, }), + createUserPreference: jest.fn(), updateUserPreference: jest.fn(), }; storeWrapper['store'].cc.userPreference = mockUserPreference; @@ -2659,6 +2662,7 @@ describe('storeEventsWrapper', () => { getUserPreference: jest.fn().mockResolvedValue({ desktopPreference: 'invalid-json', }), + createUserPreference: jest.fn(), updateUserPreference: jest.fn(), }; storeWrapper['store'].cc.userPreference = mockUserPreference; @@ -2676,12 +2680,30 @@ describe('storeEventsWrapper', () => { const mockError = new Error('API Error'); const mockUserPreference = { getUserPreference: jest.fn().mockRejectedValue(mockError), + createUserPreference: jest.fn(), updateUserPreference: jest.fn(), }; storeWrapper['store'].cc.userPreference = mockUserPreference; await expect(storeWrapper.fetchUserPreferences()).rejects.toThrow('API Error'); }); + + it('should treat a missing (404) preference record as not-yet-acknowledged instead of throwing', async () => { + const notFoundError = Object.assign(new Error('Not Found'), {statusCode: 404}); + const mockUserPreference = { + getUserPreference: jest.fn().mockRejectedValue(notFoundError), + createUserPreference: jest.fn(), + updateUserPreference: jest.fn(), + }; + storeWrapper['store'].cc.userPreference = mockUserPreference; + storeWrapper['store'].isEmergencyModalAlreadyDisplayed = true; + const errorCallCountBefore = (storeWrapper['store'].logger.error as jest.Mock).mock.calls.length; + + await expect(storeWrapper.fetchUserPreferences()).resolves.toBeUndefined(); + + expect(storeWrapper['store'].isEmergencyModalAlreadyDisplayed).toBe(false); + expect((storeWrapper['store'].logger.error as jest.Mock).mock.calls.length).toBe(errorCallCountBefore); + }); }); describe('updateEmergencyModalAcknowledgment', () => { @@ -2701,6 +2723,7 @@ describe('storeEventsWrapper', () => { it('should update user preferences and store state successfully', async () => { const mockUserPreference = { getUserPreference: jest.fn().mockResolvedValue({userId: 'test-preference-user-id'}), + createUserPreference: jest.fn(), updateUserPreference: jest.fn().mockResolvedValue({}), }; storeWrapper['store'].cc.userPreference = mockUserPreference; @@ -2722,6 +2745,7 @@ describe('storeEventsWrapper', () => { userId: 'test-preference-user-id', desktopPreference: JSON.stringify({someOtherSetting: 'value'}), }), + createUserPreference: jest.fn(), updateUserPreference: jest.fn().mockResolvedValue({}), }; storeWrapper['store'].cc.userPreference = mockUserPreference; @@ -2740,6 +2764,7 @@ describe('storeEventsWrapper', () => { userId: 'test-preference-user-id', desktopPreference: 'invalid-json', }), + createUserPreference: jest.fn(), updateUserPreference: jest.fn().mockResolvedValue({}), }; storeWrapper['store'].cc.userPreference = mockUserPreference; @@ -2760,6 +2785,7 @@ describe('storeEventsWrapper', () => { const mockError = new Error('Update Error'); const mockUserPreference = { getUserPreference: jest.fn().mockResolvedValue({userId: 'test-preference-user-id'}), + createUserPreference: jest.fn(), updateUserPreference: jest.fn().mockRejectedValue(mockError), }; storeWrapper['store'].cc.userPreference = mockUserPreference; @@ -2770,6 +2796,7 @@ describe('storeEventsWrapper', () => { it('should use the preference service userId, not the CC agentId, when updating', async () => { const mockUserPreference = { getUserPreference: jest.fn().mockResolvedValue({userId: 'preference-user-id'}), + createUserPreference: jest.fn(), updateUserPreference: jest.fn().mockResolvedValue({}), }; storeWrapper['store'].cc.userPreference = mockUserPreference; @@ -2780,6 +2807,42 @@ describe('storeEventsWrapper', () => { expect(mockUserPreference.updateUserPreference).toHaveBeenCalledWith('preference-user-id', expect.any(Object)); expect(mockUserPreference.updateUserPreference).not.toHaveBeenCalledWith('cc-agent-id', expect.any(Object)); }); + + it('should create a new preference record (using agentId) when the user has none yet (404 on getUserPreference)', async () => { + const notFoundError = Object.assign(new Error('Not Found'), {statusCode: 404}); + const mockUserPreference = { + getUserPreference: jest.fn().mockRejectedValue(notFoundError), + createUserPreference: jest.fn().mockResolvedValue({}), + updateUserPreference: jest.fn().mockResolvedValue({}), + }; + storeWrapper['store'].cc.userPreference = mockUserPreference; + storeWrapper['store'].agentId = 'first-time-agent-id'; + + await storeWrapper.updateEmergencyModalAcknowledgment(); + + expect(mockUserPreference.createUserPreference).toHaveBeenCalledWith({ + userId: 'first-time-agent-id', + desktopPreference: JSON.stringify({isEmergencyModalAlreadyDisplayed: true}), + }); + expect(mockUserPreference.updateUserPreference).not.toHaveBeenCalled(); + expect(storeWrapper['store'].isEmergencyModalAlreadyDisplayed).toBe(true); + expect(storeWrapper['store'].showE911Modal).toBe(false); + }); + + it('should rethrow non-404 errors from getUserPreference without creating a record', async () => { + const mockError = new Error('Server Error'); + const mockUserPreference = { + getUserPreference: jest.fn().mockRejectedValue(mockError), + createUserPreference: jest.fn(), + updateUserPreference: jest.fn(), + }; + storeWrapper['store'].cc.userPreference = mockUserPreference; + + await expect(storeWrapper.updateEmergencyModalAcknowledgment()).rejects.toThrow('Server Error'); + + expect(mockUserPreference.createUserPreference).not.toHaveBeenCalled(); + expect(mockUserPreference.updateUserPreference).not.toHaveBeenCalled(); + }); }); }); }); From 0508bbc4d0040e8e779e08490f1baf68c853d17d Mon Sep 17 00:00:00 2001 From: Matthew Olker Date: Mon, 20 Jul 2026 14:12:03 -0400 Subject: [PATCH 11/14] fix(store): read desktopPreference from the correct SDK response shape getUserPreference() returns the persisted desktopPreference JSON string nested under response.preferences.desktopPreference, not as a top-level response.desktopPreference field. fetchUserPreferences() and updateEmergencyModalAcknowledgment() were reading the nonexistent top-level field, so desktopPrefString was always undefined for an existing record - resetting isEmergencyModalAlreadyDisplayed to false and re-showing the E911 modal on every BROWSER login even after the agent had already acknowledged it. Read from response.preferences.desktopPreference in both methods, and correct the local UserPreferenceResponse shim type (pending the real SDK types - CAI-7906) to match. --- packages/contact-center/store/src/store.types.ts | 6 +++++- .../contact-center/store/src/storeEventsWrapper.ts | 8 ++++++-- .../contact-center/store/tests/storeEventsWrapper.ts | 12 ++++++------ 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/packages/contact-center/store/src/store.types.ts b/packages/contact-center/store/src/store.types.ts index 5dc2b3ceb..b52d436b2 100644 --- a/packages/contact-center/store/src/store.types.ts +++ b/packages/contact-center/store/src/store.types.ts @@ -385,7 +385,11 @@ export type UserPreferenceResponse = { id: string; organizationId: string; userId: string; - desktopPreference?: string; + // The SDK returns the persisted desktopPreference JSON string nested under `preferences`, + // not as a top-level field - see CAI-7906. + preferences?: { + desktopPreference?: string; + }; createdTime?: number; lastUpdatedTime?: number; }; diff --git a/packages/contact-center/store/src/storeEventsWrapper.ts b/packages/contact-center/store/src/storeEventsWrapper.ts index 70ed18548..152e9032c 100644 --- a/packages/contact-center/store/src/storeEventsWrapper.ts +++ b/packages/contact-center/store/src/storeEventsWrapper.ts @@ -632,7 +632,9 @@ class StoreWrapper implements IStoreWrapper { throw getError; } - const desktopPrefString = response?.desktopPreference; + // The SDK returns the persisted desktopPreference JSON string nested under `preferences`, + // not as a top-level field - see CAI-7906. + const desktopPrefString = response?.preferences?.desktopPreference; let isEmergencyModalAlreadyDisplayed = false; @@ -689,7 +691,9 @@ class StoreWrapper implements IStoreWrapper { } } - const existingDesktopPrefString = response?.desktopPreference; + // The SDK returns the persisted desktopPreference JSON string nested under `preferences`, + // not as a top-level field - see CAI-7906. + const existingDesktopPrefString = response?.preferences?.desktopPreference; let existingDesktopPref = {}; if (existingDesktopPrefString) { diff --git a/packages/contact-center/store/tests/storeEventsWrapper.ts b/packages/contact-center/store/tests/storeEventsWrapper.ts index a41c986b1..8f8a2c4a1 100644 --- a/packages/contact-center/store/tests/storeEventsWrapper.ts +++ b/packages/contact-center/store/tests/storeEventsWrapper.ts @@ -2612,7 +2612,7 @@ describe('storeEventsWrapper', () => { it('should fetch and parse user preferences successfully', async () => { const mockUserPreference = { getUserPreference: jest.fn().mockResolvedValue({ - desktopPreference: JSON.stringify({isEmergencyModalAlreadyDisplayed: true}), + preferences: {desktopPreference: JSON.stringify({isEmergencyModalAlreadyDisplayed: true})}, }), createUserPreference: jest.fn(), updateUserPreference: jest.fn(), @@ -2628,7 +2628,7 @@ describe('storeEventsWrapper', () => { it('should handle empty desktopPreference', async () => { const mockUserPreference = { getUserPreference: jest.fn().mockResolvedValue({ - desktopPreference: null, + preferences: {desktopPreference: null}, }), createUserPreference: jest.fn(), updateUserPreference: jest.fn(), @@ -2645,7 +2645,7 @@ describe('storeEventsWrapper', () => { storeWrapper['store'].isEmergencyModalAlreadyDisplayed = true; const mockUserPreference = { getUserPreference: jest.fn().mockResolvedValue({ - desktopPreference: null, + preferences: {desktopPreference: null}, }), createUserPreference: jest.fn(), updateUserPreference: jest.fn(), @@ -2660,7 +2660,7 @@ describe('storeEventsWrapper', () => { it('should handle parse error gracefully', async () => { const mockUserPreference = { getUserPreference: jest.fn().mockResolvedValue({ - desktopPreference: 'invalid-json', + preferences: {desktopPreference: 'invalid-json'}, }), createUserPreference: jest.fn(), updateUserPreference: jest.fn(), @@ -2743,7 +2743,7 @@ describe('storeEventsWrapper', () => { const mockUserPreference = { getUserPreference: jest.fn().mockResolvedValue({ userId: 'test-preference-user-id', - desktopPreference: JSON.stringify({someOtherSetting: 'value'}), + preferences: {desktopPreference: JSON.stringify({someOtherSetting: 'value'})}, }), createUserPreference: jest.fn(), updateUserPreference: jest.fn().mockResolvedValue({}), @@ -2762,7 +2762,7 @@ describe('storeEventsWrapper', () => { const mockUserPreference = { getUserPreference: jest.fn().mockResolvedValue({ userId: 'test-preference-user-id', - desktopPreference: 'invalid-json', + preferences: {desktopPreference: 'invalid-json'}, }), createUserPreference: jest.fn(), updateUserPreference: jest.fn().mockResolvedValue({}), From 6d2ef4c8da5422c0c0da6525a13b7c886a411fd4 Mon Sep 17 00:00:00 2001 From: Matthew Olker Date: Mon, 20 Jul 2026 14:26:42 -0400 Subject: [PATCH 12/14] fix(store): use CI user id, not CC agentId, when creating a new user-preference record createUserPreference()'s userId must be the CI user id - the same identity the SDK's own update path already keys off of via response.userId (see the earlier 'preference service userId, not the CC agentId' test). The first-time-user create path introduced for the 404 case was passing store.agentId (the CC agent identifier) instead, which can differ from the CI id and would create the record under the wrong user or fail outright, leaving Save & Continue unable to persist the E911 acknowledgment for those agents. Source the CI user id from the underlying webex SDK (webex.internal.device.userId) instead of guessing with store.agentId. --- .../store/src/storeEventsWrapper.ts | 9 ++++++++- .../store/tests/storeEventsWrapper.ts | 16 ++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/packages/contact-center/store/src/storeEventsWrapper.ts b/packages/contact-center/store/src/storeEventsWrapper.ts index 152e9032c..f2808a631 100644 --- a/packages/contact-center/store/src/storeEventsWrapper.ts +++ b/packages/contact-center/store/src/storeEventsWrapper.ts @@ -721,8 +721,15 @@ class StoreWrapper implements IStoreWrapper { desktopPreference, }); } else { + // createUserPreference's userId must be the CI user id, not the CC agentId - they can + // differ (see the existing-record update path above, which uses response.userId for the + // same reason). There's no existing preference record to read the CI id from here, so + // pull it from the underlying webex SDK instead of guessing with store.agentId. + // @ts-expect-error - webex internal device API not typed + const ciUserId: string = this.store.cc.webex.internal.device.userId; + await this.store.cc.userPreference.createUserPreference({ - userId: this.store.agentId, + userId: ciUserId, desktopPreference, }); } diff --git a/packages/contact-center/store/tests/storeEventsWrapper.ts b/packages/contact-center/store/tests/storeEventsWrapper.ts index 8f8a2c4a1..9daf8d296 100644 --- a/packages/contact-center/store/tests/storeEventsWrapper.ts +++ b/packages/contact-center/store/tests/storeEventsWrapper.ts @@ -53,6 +53,11 @@ jest.mock('../src/store', () => ({ credentials: { getUserToken: jest.fn(), }, + internal: { + device: { + userId: 'mock-ci-user-id', + }, + }, }, }, logger: { @@ -2808,7 +2813,7 @@ describe('storeEventsWrapper', () => { expect(mockUserPreference.updateUserPreference).not.toHaveBeenCalledWith('cc-agent-id', expect.any(Object)); }); - it('should create a new preference record (using agentId) when the user has none yet (404 on getUserPreference)', async () => { + it('should create a new preference record using the CI user id (not the CC agentId) when the user has none yet (404 on getUserPreference)', async () => { const notFoundError = Object.assign(new Error('Not Found'), {statusCode: 404}); const mockUserPreference = { getUserPreference: jest.fn().mockRejectedValue(notFoundError), @@ -2816,14 +2821,21 @@ describe('storeEventsWrapper', () => { updateUserPreference: jest.fn().mockResolvedValue({}), }; storeWrapper['store'].cc.userPreference = mockUserPreference; + // agentId (CC identifier) intentionally differs from the CI user id used by the + // preference service - createUserPreference must use the latter. storeWrapper['store'].agentId = 'first-time-agent-id'; + // @ts-expect-error - webex internal device API not typed on IContactCenter + storeWrapper['store'].cc.webex = {internal: {device: {userId: 'first-time-ci-user-id'}}}; await storeWrapper.updateEmergencyModalAcknowledgment(); expect(mockUserPreference.createUserPreference).toHaveBeenCalledWith({ - userId: 'first-time-agent-id', + userId: 'first-time-ci-user-id', desktopPreference: JSON.stringify({isEmergencyModalAlreadyDisplayed: true}), }); + expect(mockUserPreference.createUserPreference).not.toHaveBeenCalledWith( + expect.objectContaining({userId: 'first-time-agent-id'}) + ); expect(mockUserPreference.updateUserPreference).not.toHaveBeenCalled(); expect(storeWrapper['store'].isEmergencyModalAlreadyDisplayed).toBe(true); expect(storeWrapper['store'].showE911Modal).toBe(false); From c9c37aaee160572caf02b9f523ceb030c2dbbd8e Mon Sep 17 00:00:00 2001 From: Matthew Olker Date: Tue, 21 Jul 2026 09:37:21 -0400 Subject: [PATCH 13/14] fix(station-login): reclaim E911Modal ownership when the owning instance unmounts The single-owner guard for E911Modal previously only attempted to claim ownership once, on mount. If the owning StationLogin instance unmounted while a sibling instance stayed mounted, the sibling never re-checked and the E911 modal would never render again for the rest of the session. Fix: re-run the ownership-claim effect whenever store.showE911Modal changes, not just on mount, so a surviving instance can take over after the previous owner releases ownership. Also included: - E911Modal disables Save & Continue/Cancel while onSaveAndContinue is in flight and shows a user-facing error on failure, keeping the modal open for retry. - station-login rethrows save errors from handleE911SaveAndContinue so the modal can surface them. - storeEventsWrapper: type JSON.parse results for desktopPreference as DesktopPreference instead of implicit any. - fix(store): rename stale isEndCallEnabled to isEndTaskEnabled in getFeatureFlags()'s key list and the shared mockProfile test fixture - the SDK's Profile field was renamed during the task-refactor migration but these were never updated, causing a tsc failure and a silently-dropped feature flag. Pre-existing, unrelated to E911 work; fixed because it was blocking the pre-commit hook's test:unit run. - docs: update cc-components-spec.md and station-login-spec.md for spec-currency (E911Modal public surface/requirements, single-owner guard, and correcting a stale requirement claiming station-login never renders E911Modal). --- .../ai-docs/cc-components-spec.md | 180 ++++++++++-------- .../E911Modal/e911-modal.constants.ts | 1 + .../E911Modal/e911-modal.style.scss | 4 + .../StationLogin/E911Modal/e911-modal.tsx | 36 +++- .../E911Modal/e911-modal.types.ts | 2 +- .../E911Modal/e911-modal.test.tsx | 80 +++++++- .../ai-docs/station-login-spec.md | 62 +++--- .../station-login/src/station-login/index.tsx | 39 +++- .../tests/station-login/index.tsx | 36 +++- .../store/src/storeEventsWrapper.ts | 7 +- packages/contact-center/store/src/util.ts | 2 +- packages/contact-center/store/tests/util.ts | 1 + .../test-fixtures/src/fixtures.ts | 2 +- 13 files changed, 332 insertions(+), 120 deletions(-) diff --git a/packages/contact-center/cc-components/ai-docs/cc-components-spec.md b/packages/contact-center/cc-components/ai-docs/cc-components-spec.md index 1439d9e0a..7f8cc7078 100644 --- a/packages/contact-center/cc-components/ai-docs/cc-components-spec.md +++ b/packages/contact-center/cc-components/ai-docs/cc-components-spec.md @@ -4,40 +4,47 @@ > Context-efficiency: link to canonical docs — don't duplicate them. Load specs on demand per `SPEC_INDEX.md`. ## Metadata -| Field | Value | -|---|---| -| Module id | `cc-components` | -| Source path(s) | `packages/contact-center/cc-components/src/` | -| Doc kind | Module spec | -| Coverage score | Pending coverage assessment | -| Generated from | `module-spec` @ SDLC template library `0.1.0-draft` | + +| Field | Value | +| --------------------------------------- | -------------------------------------------------------------------------------- | +| Module id | `cc-components` | +| Source path(s) | `packages/contact-center/cc-components/src/` | +| Doc kind | Module spec | +| Coverage score | Pending coverage assessment | +| Generated from | `module-spec` @ SDLC template library `0.1.0-draft` | | generated_by / approved_by / updated_at | generated_by `migration agent` / approved_by `pending` / updated_at `2026-06-29` | -| Validation status | not-run | +| Validation status | not-run | ## Evidence Rules + Every generated requirement below cites concrete source evidence using `file path`. Source evidence, test evidence, examples, assumptions, and gaps are kept separate so validators and future agents can distinguish truth from context. Test evidence is preferred for WHY. This repository's tests are the authoritative behavior record; commit history is not cited here. Where evidence is missing it is recorded as a gap rather than asserted. ## Source Material Register -| Source doc | Scope | Decision | Detail location or disposition | -|---|---|---|---| -| `ai-docs/_archive/pre-sdlc-migration/packages/contact-center/cc-components/ai-docs/AGENTS.md` | overview / API / examples | migrated / reconciled | Orientation → Overview/Purpose/Stack; component table → Public Surface; examples → Use Cases. Reconciled: archived table listed 7 components; code exports 11 (added Campaign* and RealTimeTranscript). | -| `ai-docs/_archive/pre-sdlc-migration/packages/contact-center/cc-components/ai-docs/ARCHITECTURE.md` | architecture / component table | migrated / reconciled | Component table, file structure, patterns, diagrams → Design Overview, Data Flow, Class/Component Relationships, Folder structure, Pitfalls. Conflict: archived doc imports `@momentum-design/components` and `@momentum-ui/react-collaboration` interchangeably; code uses both (see Stack). | -| `packages/contact-center/cc-components/src/` and `tests/` | source / tests | reference-only (ground truth) | All requirements, props, and component inventory grounded against live source and `tests/components/`. | + +| Source doc | Scope | Decision | Detail location or disposition | +| --------------------------------------------------------------------------------------------------- | ------------------------------ | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `ai-docs/_archive/pre-sdlc-migration/packages/contact-center/cc-components/ai-docs/AGENTS.md` | overview / API / examples | migrated / reconciled | Orientation → Overview/Purpose/Stack; component table → Public Surface; examples → Use Cases. Reconciled: archived table listed 7 components; code exports 11 (added Campaign\* and RealTimeTranscript). | +| `ai-docs/_archive/pre-sdlc-migration/packages/contact-center/cc-components/ai-docs/ARCHITECTURE.md` | architecture / component table | migrated / reconciled | Component table, file structure, patterns, diagrams → Design Overview, Data Flow, Class/Component Relationships, Folder structure, Pitfalls. Conflict: archived doc imports `@momentum-design/components` and `@momentum-ui/react-collaboration` interchangeably; code uses both (see Stack). | +| `packages/contact-center/cc-components/src/` and `tests/` | source / tests | reference-only (ground truth) | All requirements, props, and component inventory grounded against live source and `tests/components/`. | ## Overview + `cc-components` is the presentation layer for Webex Contact Center widgets. It is a library of pure, presentational React function components — each receives all data and callbacks via props and renders Momentum UI primitives. Components do not import or read the MobX store, do not call the SDK, and hold only transient local UI state (open/closed menus, selected dropdown values, input text). Business logic and store/SDK access live one layer up, in the widget packages (`station-login`, `user-state`, `task`) that compose these components. The package exports two surfaces from `src/index.ts` (React components + their prop types) and `src/wc.ts` (the same components wrapped as custom elements via `@r2wc/react-to-web-component`). A maintainer should start at `src/index.ts` to see the public component set, then open the component directory under `src/components/` (each has `*.tsx`, a `*.types.ts` or shared `task.types.ts`, a `*.utils.ts(x)` for extracted logic, and a `*.scss`). Shared, cross-component logic lives in `src/utils/index.ts` (`formatTime`, `getMediaTypeInfo`) and `src/hooks/` (`useIntersectionObserver`). -The component set spans the contact-center agent surface: station login, agent state, the task lifecycle (incoming task, call control with consult/transfer, CAD-enabled call control, task list, outdial), live transcript, and campaign-preview dialing (countdown, error dialog, campaign task card/popover/list-item). +The component set spans the contact-center agent surface: station login (including the E911 emergency-service acknowledgment modal), agent state, the task lifecycle (incoming task, call control with consult/transfer, CAD-enabled call control, task list, outdial), live transcript, and campaign-preview dialing (countdown, error dialog, campaign task card/popover/list-item). ## Purpose / Responsibility + Owns the presentational React UI primitives for contact-center widgets: render agent/task UI from props and emit user intent back through callback props. Does NOT own state management, SDK access, business logic, or web-component registration into the host (the actual custom-element registration into the host app is `cc-widgets`' responsibility; `wc.ts` here only defines `component-cc-*` elements for the library build). ## Stack + TypeScript 5.6, React 18 (peer `react`/`react-dom` `>=18.3.1`), function components with hooks. UI primitives from `@momentum-ui/react-collaboration` (peer `>=26.197.0`) and `@momentum-design/components/dist/react`. Web-component wrapping via `@r2wc/react-to-web-component` `2.0.3`. Metrics via `@webex/cc-ui-logging` (`withMetrics` HOC). Types consumed from `@webex/cc-store`. Test stack: Jest 29 + React Testing Library 16 + `@testing-library/jest-dom`, jsdom environment, snapshot tests alongside behavioral tests. Build: `tsc` for the type build, Webpack 5 for the bundle. Test command: `yarn workspace @webex/cc-components test:unit`. ## Folder / Package Structure + ``` packages/contact-center/cc-components/src/ ├── index.ts # React component + type barrel (public surface) @@ -74,41 +81,46 @@ tests/hooks/ # Hook tests ``` ## Key Files (source of truth) -| File | Holds | -|---|---| -| `src/index.ts` | The public React component set and re-exported type barrels — authoritative export list. | -| `src/wc.ts` | Custom-element tag names (`component-cc-*`) and the r2wc prop type maps per component. | -| `src/components/task/task.types.ts` | Shared task prop interfaces and the `Pick`-derived component prop types (`CallControlComponentProps`, `IncomingTaskComponentProps`, `TaskListComponentProps`, `OutdialCallComponentProps`, `RealTimeTranscriptComponentProps`), plus `MEDIA_CHANNEL`, `TaskState`, `ControlVisibility`, campaign types. | -| `src/components/StationLogin/station-login.types.ts` | `IStationLoginProps` and the `StationLoginComponentProps` Pick. | -| `src/components/UserState/user-state.types.ts` | `IUserState`, `UserStateComponentsProps` Pick, `AgentUserState` enum. | -| `src/components/StationLogin/constants.ts` | Login labels/error strings — re-exported from the barrel; never hardcode these elsewhere. | -| `src/components/task/constants.ts` | Task UI label/string constants (e.g. `CAMPAIGN_CALL`, `WRAP_UP`). | -| `src/utils/index.ts` | `formatTime` (timer formatting) and `getMediaTypeInfo` (media icon/label mapping). | + +| File | Holds | +| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `src/index.ts` | The public React component set and re-exported type barrels — authoritative export list. | +| `src/wc.ts` | Custom-element tag names (`component-cc-*`) and the r2wc prop type maps per component. | +| `src/components/task/task.types.ts` | Shared task prop interfaces and the `Pick`-derived component prop types (`CallControlComponentProps`, `IncomingTaskComponentProps`, `TaskListComponentProps`, `OutdialCallComponentProps`, `RealTimeTranscriptComponentProps`), plus `MEDIA_CHANNEL`, `TaskState`, `ControlVisibility`, campaign types. | +| `src/components/StationLogin/station-login.types.ts` | `IStationLoginProps` and the `StationLoginComponentProps` Pick. | +| `src/components/UserState/user-state.types.ts` | `IUserState`, `UserStateComponentsProps` Pick, `AgentUserState` enum. | +| `src/components/StationLogin/constants.ts` | Login labels/error strings — re-exported from the barrel; never hardcode these elsewhere. | +| `src/components/task/constants.ts` | Task UI label/string constants (e.g. `CAMPAIGN_CALL`, `WRAP_UP`). | +| `src/utils/index.ts` | `formatTime` (timer formatting) and `getMediaTypeInfo` (media icon/label mapping). | ## Public Surface + Consumed as an imported SDK/code API. The React barrel (`src/index.ts`) is the primary surface; `src/wc.ts` exposes the same components as custom elements for the library build. Exact prop schemas live in the `*.types.ts` files (linked below); the root contract index (`CONTRACTS.md`) documents how the consuming `cc-widgets`/widget layer re-exposes these as custom elements. -| Contract ID | Type | Surface | Purpose | Compatibility / deprecation | Schema / detail link | Root index | -|---|---|---|---|---|---|---| -| `cc-components.StationLoginComponent` | SDK | `StationLoginComponent` (`StationLoginComponentProps`) | Agent login: device/team selection, login/logout, multiple-login alert, profile mode | semver; props are `Pick`ed — adding optional props = minor, removing/renaming a picked prop = major | `src/components/StationLogin/station-login.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | -| `cc-components.UserStateComponent` | SDK | `UserStateComponent` (`UserStateComponentsProps`) | Agent state dropdown + idle codes + state timer | semver as above | `src/components/UserState/user-state.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | -| `cc-components.CallControlComponent` | SDK | `CallControlComponent` (`CallControlComponentProps`) | Call control buttons: hold/resume, mute, record, end, wrapup, consult/transfer/conference | semver as above | `src/components/task/task.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | -| `cc-components.CallControlCADComponent` | SDK | `CallControlCADComponent` (`CallControlComponentProps`) | Call control with customer/queue header and agent-viewable CAD global variables | semver as above | `src/components/task/task.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | -| `cc-components.IncomingTaskComponent` | SDK | `IncomingTaskComponent` (`IncomingTaskComponentProps`) | Incoming task notification with Answer/Decline | semver as above | `src/components/task/task.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | -| `cc-components.TaskListComponent` | SDK | `TaskListComponent` (`TaskListComponentProps`) | Active + incoming task list; renders campaign preview when enabled | semver as above | `src/components/task/task.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | -| `cc-components.OutdialCallComponent` | SDK | `OutdialCallComponent` (`OutdialCallComponentProps`) | Outbound dialpad, ANI selection, address-book search | semver as above | `src/components/task/task.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | -| `cc-components.RealTimeTranscriptComponent` | SDK | `RealTimeTranscriptComponent` (`RealTimeTranscriptComponentProps`) | Renders sorted live transcript entries; empty state when none | semver as above | `src/components/task/task.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | -| `cc-components.CampaignCountdownComponent` | SDK | `CampaignCountdownComponent` (`CampaignCountdownProps`) | Campaign preview offer countdown; fires `onTimeout` at zero | semver as above | `src/components/task/CampaignCountdown/campaign-countdown.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | -| `cc-components.CampaignErrorDialogComponent` | SDK | `CampaignErrorDialogComponent` (`CampaignErrorDialogProps`) | Modal shown when a campaign action (accept/skip/remove/cancel) fails | semver as above | `src/components/task/CampaignErrorDialog/campaign-error-dialog.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | -| `cc-components.CampaignTaskComponent` | SDK | `CampaignTaskComponent` (`CampaignTaskProps`) | Campaign preview card: accept/skip/remove, countdown, error dialog | semver as above | `src/components/task/task.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | -| `cc-components.wc` | SDK | `@webex/cc-components/wc` → custom elements `component-cc-user-state`, `component-cc-station-login`, `component-cc-call-control`, `component-cc-call-control-cad`, `component-cc-incoming-task`, `component-cc-task-list`, `component-cc-out-dial-call`, `component-cc-realtime-transcript` | Custom-element build of the components | tag names are breaking surface; r2wc prop type map is part of the contract | `src/wc.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| Contract ID | Type | Surface | Purpose | Compatibility / deprecation | Schema / detail link | Root index | +| -------------------------------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | -------------------------------------------------- | +| `cc-components.StationLoginComponent` | SDK | `StationLoginComponent` (`StationLoginComponentProps`) | Agent login: device/team selection, login/logout, multiple-login alert, profile mode | semver; props are `Pick`ed — adding optional props = minor, removing/renaming a picked prop = major | `src/components/StationLogin/station-login.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `cc-components.UserStateComponent` | SDK | `UserStateComponent` (`UserStateComponentsProps`) | Agent state dropdown + idle codes + state timer | semver as above | `src/components/UserState/user-state.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `cc-components.CallControlComponent` | SDK | `CallControlComponent` (`CallControlComponentProps`) | Call control buttons: hold/resume, mute, record, end, wrapup, consult/transfer/conference | semver as above | `src/components/task/task.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `cc-components.CallControlCADComponent` | SDK | `CallControlCADComponent` (`CallControlComponentProps`) | Call control with customer/queue header and agent-viewable CAD global variables | semver as above | `src/components/task/task.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `cc-components.IncomingTaskComponent` | SDK | `IncomingTaskComponent` (`IncomingTaskComponentProps`) | Incoming task notification with Answer/Decline | semver as above | `src/components/task/task.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `cc-components.TaskListComponent` | SDK | `TaskListComponent` (`TaskListComponentProps`) | Active + incoming task list; renders campaign preview when enabled | semver as above | `src/components/task/task.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `cc-components.OutdialCallComponent` | SDK | `OutdialCallComponent` (`OutdialCallComponentProps`) | Outbound dialpad, ANI selection, address-book search | semver as above | `src/components/task/task.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `cc-components.RealTimeTranscriptComponent` | SDK | `RealTimeTranscriptComponent` (`RealTimeTranscriptComponentProps`) | Renders sorted live transcript entries; empty state when none | semver as above | `src/components/task/task.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `cc-components.CampaignCountdownComponent` | SDK | `CampaignCountdownComponent` (`CampaignCountdownProps`) | Campaign preview offer countdown; fires `onTimeout` at zero | semver as above | `src/components/task/CampaignCountdown/campaign-countdown.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `cc-components.CampaignErrorDialogComponent` | SDK | `CampaignErrorDialogComponent` (`CampaignErrorDialogProps`) | Modal shown when a campaign action (accept/skip/remove/cancel) fails | semver as above | `src/components/task/CampaignErrorDialog/campaign-error-dialog.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `cc-components.CampaignTaskComponent` | SDK | `CampaignTaskComponent` (`CampaignTaskProps`) | Campaign preview card: accept/skip/remove, countdown, error dialog | semver as above | `src/components/task/task.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `cc-components.E911Modal` | SDK | `E911Modal` (`E911ModalProps`) | Emergency-service (E911) acknowledgment modal shown on BROWSER station login; Cancel is the only dismissal path, Save & Continue is gated on the checkbox and disabled while the save is in flight | semver as above; not yet in `wc.ts` (React-only, no custom-element wrapper) | `src/components/StationLogin/E911Modal/e911-modal.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `cc-components.wc` | SDK | `@webex/cc-components/wc` → custom elements `component-cc-user-state`, `component-cc-station-login`, `component-cc-call-control`, `component-cc-call-control-cad`, `component-cc-incoming-task`, `component-cc-task-list`, `component-cc-out-dial-call`, `component-cc-realtime-transcript` | Custom-element build of the components | tag names are breaking surface; r2wc prop type map is part of the contract | `src/wc.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | Compatibility notes: + - Component props are derived with `Pick<...>` over a larger interface (e.g. `IStationLoginProps`, `ControlProps`); only the picked keys are public. Adding an optional picked prop is additive (minor); removing/renaming a picked prop, or narrowing a prop's type, is breaking (major). - Custom-element tag names in `wc.ts` (`component-cc-*`) and their r2wc prop type maps are a breaking surface — renaming a tag or changing a prop's r2wc type (`json`/`string`/`function`/...) breaks host consumers. - `wc.ts` aliases `CallControlCADComponent` to `../CallControl/call-control` (imports `CallControlComponent` under the `CallControlCADComponent` name); the distinct CAD component is `src/components/task/CallControlCAD/call-control-cad.tsx`. See Pitfalls. ## Requires (dependencies) + - `@webex/cc-store` (workspace:\*) — type-only import surface here (`ITask`, `ILogger`, `IContactCenter`, `IdleCode`, `IWrapupCode`, `BuddyDetails`, etc.) and constants such as `ERROR_TRIGGERING_IDLE_CODES`, `LoginOptions`, `DESKTOP`. Components consume types/constants, not the store singleton. - `@webex/cc-ui-logging` (workspace:\*) — `withMetrics` HOC wrapping each top-level component for mount/metrics tracking. - `@momentum-ui/react-collaboration` (peer `>=26.197.0`) and `@momentum-design/components/dist/react` — UI primitives. @@ -118,25 +130,28 @@ Compatibility notes: - `@webex/test-fixtures` (workspace:\*, dev) — shared test mocks. ## Requirements -| ID | WHAT | WHY | Source Evidence | Test / Example Evidence | Assumptions / Gaps | Confidence | -|---|---|---|---|---|---|---| -| `CC-COMPONENTS-R-001` | Components are pure presentational: they receive data + callbacks via props and never read the MobX store or call the SDK directly. | Keeps presentation decoupled from state/SDK so components are testable in isolation and reusable across widgets. | `src/components/StationLogin/station-login.tsx`, `src/components/UserState/user-state.tsx`, `src/components/task/CallControl/call-control.tsx` (props-only; no `@webex/cc-store` singleton import) | `tests/components/StationLogin/station-login.tsx` (renders from props, mocks callbacks) | None | PRESENT | -| `CC-COMPONENTS-R-002` | The public React surface is exactly the 11 components exported from `index.ts` plus the re-exported type barrels. | Defines the supported import surface; consumers must not import internal subcomponents. | `src/index.ts` | `tests/components/StationLogin/station-login.tsx`, `tests/components/task/CampaignTask/campaign-task.test.tsx`, `tests/components/task/RealtimeTranscript/realtime-transcript.tsx` (import the exported components) | None | PRESENT | -| `CC-COMPONENTS-R-003` | `StationLoginComponent` invokes the supplied callbacks (`login`, `setDeviceType`, `setDialNumber`, `handleContinue`, `saveLoginOptions`) on the matching user action and surfaces `loginFailure`/`saveError` as error UI. | Login intent and errors must propagate to the widget layer without the component owning login logic. | `src/components/StationLogin/station-login.tsx`, `src/components/StationLogin/station-login.utils.tsx` | `tests/components/StationLogin/station-login.tsx` (`calls login function...`, `renders login failure when passed`, `renders save error when passed`) | None | PRESENT | -| `CC-COMPONENTS-R-004` | `StationLoginComponent` hides the Desktop login option when `hideDesktopLogin` is true (in both login and profile mode) and shows it when false/undefined. | Deployments can disable Desktop login; must be honored consistently across modes. | `src/components/StationLogin/station-login.tsx`, `src/components/StationLogin/station-login.utils.tsx` | `tests/components/StationLogin/station-login.tsx` (`hides Desktop login option when hideDesktopLogin is true`, `... when false`, `... when undefined`, `... in profile mode`) | None | PRESENT | -| `CC-COMPONENTS-R-005` | `UserStateComponent` renders idle codes sorted/built into the dropdown, reflects `currentState`/`elapsedTime`, and calls `setAgentStatus(auxCodeId)` on selection; error-triggering idle codes are styled distinctly. | Agent must change state and see correct current state + timing; error idle codes need visual emphasis. | `src/components/UserState/user-state.tsx`, `src/components/UserState/user-state.utils.ts` (`buildDropdownItems`, `sortDropdownItems`, `handleSelectionChange`, `getDropdownClass`) | `tests/components/UserState/user-state.tsx`, `tests/components/UserState/user-state.utils.tsx` | None | PRESENT | -| `CC-COMPONENTS-R-006` | `CallControlComponent` builds its button set from `controlVisibility` and current task, and routes button presses to the matching callback (`toggleHold`, `toggleMute`, `toggleRecording`, `endCall`, `wrapupCall`, consult/transfer/conference handlers); wrapup requires selecting a reason. | Call control must reflect the allowed actions for the current interaction state and emit the right intent. | `src/components/task/CallControl/call-control.tsx`, `src/components/task/CallControl/call-control.utils.ts` (`buildCallControlButtons`, `filterButtonsForConsultation`, `handleWrapupCall`) | `tests/components/task/CallControl/call-control.tsx`, `tests/components/task/CallControl/call-control.utils.tsx` | None | PRESENT | -| `CC-COMPONENTS-R-007` | `CallControlCADComponent` renders the customer/queue/caller header and an agent-viewable CAD global variables panel, and renders the campaign call icon + "Campaign call" label when `isCampaignCall` is true. | CAD/header info and campaign branding must be visible to the agent during a call. | `src/components/task/CallControlCAD/call-control-cad.tsx`, `src/components/task/Task/task.utils.ts` (`getAgentViewableGlobalVariables`) | `tests/components/task/CallControlCAD/call-control-cad.tsx` | None | PRESENT | -| `CC-COMPONENTS-R-008` | `IncomingTaskComponent` renders the standard `Task` with Answer/Decline when an `incomingTask` is present and renders nothing (hidden) when it is absent; Accept/Decline invoke `accept(task)`/`reject(task)`. | Avoids a stray empty notification when no task; routes accept/decline intent up. | `src/components/task/IncomingTask/incoming-task.tsx`, `src/components/task/IncomingTask/incoming-task.utils.tsx` (`extractIncomingTaskData`) | `tests/components/task/IncomingTask/incoming-task.tsx`, `tests/components/task/IncomingTask/incoming-task.utils.tsx` | None | PRESENT | -| `CC-COMPONENTS-R-009` | `TaskListComponent` renders nothing when the task list is empty, otherwise renders one row per task; campaign preview tasks render `CampaignTask` (instead of `Task`) only when `hasCampaignPreviewEnabled` (default true) and the task is a campaign preview. | List must collapse when empty and switch row UI for campaign previews per the feature flag. | `src/components/task/TaskList/task-list.tsx`, `src/components/task/TaskList/task-list.utils.ts` (`isTaskListEmpty`, `getTasksArray`, `isCampaignPreviewTask`, `getActiveCampaignPreviewId`) | `tests/components/task/TaskList/task-list.tsx`, `tests/components/task/TaskList/task-list.utils.tsx` | None | PRESENT | -| `CC-COMPONENTS-R-010` | `OutdialCallComponent` validates the entered destination, supports dialpad / ANI / address-book tabs, disables the outdial action while a telephony task is active, and calls `startOutdial(destination, origin?)`. | Outbound dialing must validate input and not start a second call over an active one. | `src/components/task/OutdialCall/outdial-call.tsx`, `src/components/task/OutdialCall/constants.ts` | `tests/components/task/OutdialCall/out-dial-call.tsx` | None | PRESENT | -| `CC-COMPONENTS-R-011` | `RealTimeTranscriptComponent` sorts `liveTranscriptEntries` by ascending `timestamp` before rendering and shows an empty-state message when there are no entries. | Transcript must read chronologically and degrade gracefully when empty. | `src/components/task/RealTimeTranscript/real-time-transcript.tsx` | `tests/components/task/RealtimeTranscript/realtime-transcript.tsx` | None | PRESENT | -| `CC-COMPONENTS-R-012` | Campaign preview UI: `CampaignTaskComponent` renders accept/skip/remove + countdown and triggers the configured auto-action on timeout; failed actions open `CampaignErrorDialogComponent` with the mapped `CampaignErrorType`; `CampaignCountdownComponent` fires `onTimeout` at zero. | Campaign preview offers are time-boxed; failures and timeouts must be surfaced and auto-handled. | `src/components/task/CampaignTask/campaign-task.tsx`, `src/components/task/CampaignErrorDialog/campaign-error-dialog.tsx` (+ `.types.ts` `CAMPAIGN_ACTION_ERROR_MAP`, `ERROR_TITLES`), `src/components/task/CampaignCountdown/campaign-countdown.tsx` | `tests/components/task/CampaignTask/campaign-task.test.tsx`, `tests/components/task/CampaignErrorDialog/campaign-error-dialog.tsx`, `tests/components/task/CampaignCountdown/campaign-countdown.tsx` | None | PRESENT | -| `CC-COMPONENTS-R-013` | `formatTime` renders `HH:MM:SS` for durations ≥ 1 hour and `MM:SS` otherwise, with zero-padding; `getMediaTypeInfo` maps media type/channel to icon/label/className/brand-visual, falling back to telephony/chat defaults. | Timers and media badges must format consistently across all task components. | `src/utils/index.ts` | `tests/components/task/CallControl/call-control.utils.tsx`, snapshot tests under `tests/components/task/**/__snapshots__/` exercise formatted output | No dedicated `tests/utils/` file found for `formatTime`/`getMediaTypeInfo` (exercised indirectly via component/utils tests) | WEAK | -| `CC-COMPONENTS-R-014` | `useIntersectionObserver` reports element visibility for infinite-scroll/lazy paths (e.g. outdial address-book paging). | Paged lists must load more on scroll without per-component observer wiring. | `src/hooks/useIntersectionObserver.ts` | `tests/hooks/useIntersectionObserver.test.ts` | None | PRESENT | -| `CC-COMPONENTS-R-015` | Each top-level exported component is wrapped with the `withMetrics` HOC so mount/usage metrics are tracked uniformly. | Consistent telemetry across all widgets without per-component instrumentation. | `withMetrics` import + wrap in `src/components/StationLogin/station-login.tsx`, `src/components/UserState/user-state.tsx`, `src/components/task/CallControl/call-control.tsx`, `src/components/task/RealTimeTranscript/real-time-transcript.tsx` | Covered indirectly by each component's render test | No test asserts the HOC wrapping itself | WEAK | + +| ID | WHAT | WHY | Source Evidence | Test / Example Evidence | Assumptions / Gaps | Confidence | +| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | ---------- | +| `CC-COMPONENTS-R-001` | Components are pure presentational: they receive data + callbacks via props and never read the MobX store or call the SDK directly. | Keeps presentation decoupled from state/SDK so components are testable in isolation and reusable across widgets. | `src/components/StationLogin/station-login.tsx`, `src/components/UserState/user-state.tsx`, `src/components/task/CallControl/call-control.tsx` (props-only; no `@webex/cc-store` singleton import) | `tests/components/StationLogin/station-login.tsx` (renders from props, mocks callbacks) | None | PRESENT | +| `CC-COMPONENTS-R-002` | The public React surface is exactly the 12 components exported from `index.ts` (11 base components plus `E911Modal`) plus the re-exported type barrels. | Defines the supported import surface; consumers must not import internal subcomponents. | `src/index.ts` | `tests/components/StationLogin/station-login.tsx`, `tests/components/task/CampaignTask/campaign-task.test.tsx`, `tests/components/task/RealtimeTranscript/realtime-transcript.tsx`, `tests/components/StationLogin/E911Modal/e911-modal.test.tsx` (import the exported components) | `E911Modal` is not yet wrapped as a custom element in `wc.ts` - React-only for now | PRESENT | +| `CC-COMPONENTS-R-003` | `StationLoginComponent` invokes the supplied callbacks (`login`, `setDeviceType`, `setDialNumber`, `handleContinue`, `saveLoginOptions`) on the matching user action and surfaces `loginFailure`/`saveError` as error UI. | Login intent and errors must propagate to the widget layer without the component owning login logic. | `src/components/StationLogin/station-login.tsx`, `src/components/StationLogin/station-login.utils.tsx` | `tests/components/StationLogin/station-login.tsx` (`calls login function...`, `renders login failure when passed`, `renders save error when passed`) | None | PRESENT | +| `CC-COMPONENTS-R-004` | `StationLoginComponent` hides the Desktop login option when `hideDesktopLogin` is true (in both login and profile mode) and shows it when false/undefined. | Deployments can disable Desktop login; must be honored consistently across modes. | `src/components/StationLogin/station-login.tsx`, `src/components/StationLogin/station-login.utils.tsx` | `tests/components/StationLogin/station-login.tsx` (`hides Desktop login option when hideDesktopLogin is true`, `... when false`, `... when undefined`, `... in profile mode`) | None | PRESENT | +| `CC-COMPONENTS-R-005` | `UserStateComponent` renders idle codes sorted/built into the dropdown, reflects `currentState`/`elapsedTime`, and calls `setAgentStatus(auxCodeId)` on selection; error-triggering idle codes are styled distinctly. | Agent must change state and see correct current state + timing; error idle codes need visual emphasis. | `src/components/UserState/user-state.tsx`, `src/components/UserState/user-state.utils.ts` (`buildDropdownItems`, `sortDropdownItems`, `handleSelectionChange`, `getDropdownClass`) | `tests/components/UserState/user-state.tsx`, `tests/components/UserState/user-state.utils.tsx` | None | PRESENT | +| `CC-COMPONENTS-R-006` | `CallControlComponent` builds its button set from `controlVisibility` and current task, and routes button presses to the matching callback (`toggleHold`, `toggleMute`, `toggleRecording`, `endCall`, `wrapupCall`, consult/transfer/conference handlers); wrapup requires selecting a reason. | Call control must reflect the allowed actions for the current interaction state and emit the right intent. | `src/components/task/CallControl/call-control.tsx`, `src/components/task/CallControl/call-control.utils.ts` (`buildCallControlButtons`, `filterButtonsForConsultation`, `handleWrapupCall`) | `tests/components/task/CallControl/call-control.tsx`, `tests/components/task/CallControl/call-control.utils.tsx` | None | PRESENT | +| `CC-COMPONENTS-R-007` | `CallControlCADComponent` renders the customer/queue/caller header and an agent-viewable CAD global variables panel, and renders the campaign call icon + "Campaign call" label when `isCampaignCall` is true. | CAD/header info and campaign branding must be visible to the agent during a call. | `src/components/task/CallControlCAD/call-control-cad.tsx`, `src/components/task/Task/task.utils.ts` (`getAgentViewableGlobalVariables`) | `tests/components/task/CallControlCAD/call-control-cad.tsx` | None | PRESENT | +| `CC-COMPONENTS-R-008` | `IncomingTaskComponent` renders the standard `Task` with Answer/Decline when an `incomingTask` is present and renders nothing (hidden) when it is absent; Accept/Decline invoke `accept(task)`/`reject(task)`. | Avoids a stray empty notification when no task; routes accept/decline intent up. | `src/components/task/IncomingTask/incoming-task.tsx`, `src/components/task/IncomingTask/incoming-task.utils.tsx` (`extractIncomingTaskData`) | `tests/components/task/IncomingTask/incoming-task.tsx`, `tests/components/task/IncomingTask/incoming-task.utils.tsx` | None | PRESENT | +| `CC-COMPONENTS-R-009` | `TaskListComponent` renders nothing when the task list is empty, otherwise renders one row per task; campaign preview tasks render `CampaignTask` (instead of `Task`) only when `hasCampaignPreviewEnabled` (default true) and the task is a campaign preview. | List must collapse when empty and switch row UI for campaign previews per the feature flag. | `src/components/task/TaskList/task-list.tsx`, `src/components/task/TaskList/task-list.utils.ts` (`isTaskListEmpty`, `getTasksArray`, `isCampaignPreviewTask`, `getActiveCampaignPreviewId`) | `tests/components/task/TaskList/task-list.tsx`, `tests/components/task/TaskList/task-list.utils.tsx` | None | PRESENT | +| `CC-COMPONENTS-R-010` | `OutdialCallComponent` validates the entered destination, supports dialpad / ANI / address-book tabs, disables the outdial action while a telephony task is active, and calls `startOutdial(destination, origin?)`. | Outbound dialing must validate input and not start a second call over an active one. | `src/components/task/OutdialCall/outdial-call.tsx`, `src/components/task/OutdialCall/constants.ts` | `tests/components/task/OutdialCall/out-dial-call.tsx` | None | PRESENT | +| `CC-COMPONENTS-R-011` | `RealTimeTranscriptComponent` sorts `liveTranscriptEntries` by ascending `timestamp` before rendering and shows an empty-state message when there are no entries. | Transcript must read chronologically and degrade gracefully when empty. | `src/components/task/RealTimeTranscript/real-time-transcript.tsx` | `tests/components/task/RealtimeTranscript/realtime-transcript.tsx` | None | PRESENT | +| `CC-COMPONENTS-R-012` | Campaign preview UI: `CampaignTaskComponent` renders accept/skip/remove + countdown and triggers the configured auto-action on timeout; failed actions open `CampaignErrorDialogComponent` with the mapped `CampaignErrorType`; `CampaignCountdownComponent` fires `onTimeout` at zero. | Campaign preview offers are time-boxed; failures and timeouts must be surfaced and auto-handled. | `src/components/task/CampaignTask/campaign-task.tsx`, `src/components/task/CampaignErrorDialog/campaign-error-dialog.tsx` (+ `.types.ts` `CAMPAIGN_ACTION_ERROR_MAP`, `ERROR_TITLES`), `src/components/task/CampaignCountdown/campaign-countdown.tsx` | `tests/components/task/CampaignTask/campaign-task.test.tsx`, `tests/components/task/CampaignErrorDialog/campaign-error-dialog.tsx`, `tests/components/task/CampaignCountdown/campaign-countdown.tsx` | None | PRESENT | +| `CC-COMPONENTS-R-013` | `formatTime` renders `HH:MM:SS` for durations ≥ 1 hour and `MM:SS` otherwise, with zero-padding; `getMediaTypeInfo` maps media type/channel to icon/label/className/brand-visual, falling back to telephony/chat defaults. | Timers and media badges must format consistently across all task components. | `src/utils/index.ts` | `tests/components/task/CallControl/call-control.utils.tsx`, snapshot tests under `tests/components/task/**/__snapshots__/` exercise formatted output | No dedicated `tests/utils/` file found for `formatTime`/`getMediaTypeInfo` (exercised indirectly via component/utils tests) | WEAK | +| `CC-COMPONENTS-R-014` | `useIntersectionObserver` reports element visibility for infinite-scroll/lazy paths (e.g. outdial address-book paging). | Paged lists must load more on scroll without per-component observer wiring. | `src/hooks/useIntersectionObserver.ts` | `tests/hooks/useIntersectionObserver.test.ts` | None | PRESENT | +| `CC-COMPONENTS-R-015` | Each top-level exported component is wrapped with the `withMetrics` HOC so mount/usage metrics are tracked uniformly. | Consistent telemetry across all widgets without per-component instrumentation. | `withMetrics` import + wrap in `src/components/StationLogin/station-login.tsx`, `src/components/UserState/user-state.tsx`, `src/components/task/CallControl/call-control.tsx`, `src/components/task/RealTimeTranscript/real-time-transcript.tsx` | Covered indirectly by each component's render test | No test asserts the HOC wrapping itself | WEAK | +| `CC-COMPONENTS-R-016` | `E911Modal` gates `Save & Continue` on the acknowledgment checkbox, disables both `Save & Continue` and `Cancel` while `onSaveAndContinue` is in flight (guarding against a double-click firing concurrent saves), shows a user-facing error and re-enables the buttons if the save rejects, and only `Cancel` (not the Dialog's built-in close button or Escape) dismisses the modal; checkbox/saving/error state resets when the modal closes. | An emergency-notification acknowledgment must not be skippable, must not double-submit against the preference API, and must give the agent visible recourse on failure. | `src/components/StationLogin/E911Modal/e911-modal.tsx` | `tests/components/StationLogin/E911Modal/e911-modal.test.tsx` (checkbox gating, save-in-flight button disabling, save-error display, close-only-via-Cancel) | None | PRESENT | ## Design Overview + Every component follows the same shape: a typed function component destructures props, derives display data through pure helpers in a co-located `*.utils.ts(x)`, renders Momentum primitives, and calls back through callback props on user interaction. Local `useState` holds only transient UI (open menus, selected-but-not-yet-submitted values, input text) — never domain state. Top-level components are wrapped in `withMetrics`. This keeps each component unit-testable with plain props and jest mocks and is the reason the archived "presentational pattern" guidance still holds. Logic that is non-trivial or shared is pulled out of the JSX: per-component utils (`station-login.utils.tsx`, `call-control.utils.ts`, `task-list.utils.ts`, etc.) and library-wide utils (`src/utils/index.ts`: `formatTime`, `getMediaTypeInfo`). `task.types.ts` is the shared type hub for the task family — the larger `ControlProps`/`TaskProps` interfaces describe the full data set, and each component's public prop type is a `Pick` of the keys it actually uses, which is why the public surface is intentionally narrower than the interfaces. @@ -144,6 +159,7 @@ Logic that is non-trivial or shared is pulled out of the JSX: per-component util Composition is deliberate: `IncomingTaskComponent` and `TaskListComponent` both render the generic `Task` row; `CallControlCADComponent` wraps `CallControlComponent` and adds a CAD header + `GlobalVariablesPanel`; `CallControlComponent` embeds the consult/transfer popover (`CallControlCustom/`) and `AutoWrapupTimer`; `CampaignTask` composes `CampaignTaskListItem`, `CampaignTaskPopover`, `CampaignCountdown`, and `CampaignErrorDialog`. The `wc.ts` module is a thin adapter that re-exposes the same components as `component-cc-*` custom elements with explicit r2wc prop type maps; actual registration into a host app happens in `cc-widgets`. ## Data Flow + In-process React props/callbacks only — there is no network, queue, or socket transport in this module. Data flows down as props (sourced by the widget layer from the store/SDK), is transformed by pure utils into render data, rendered via Momentum primitives, and user interaction flows back up by invoking callback props. ```mermaid @@ -164,14 +180,15 @@ flowchart LR ``` ## Sequence Diagram(s) + These components share one interaction pattern (props in → local UI state → callback out); they differ only in which callbacks fire. One representative sequence plus its failure branch covers the module; the campaign timeout/error path is the one non-trivial async branch and is included. Sequence coverage: -| Operation group | Diagram | Failure / recovery coverage | -|---|---|---| +| Operation group | Diagram | Failure / recovery coverage | +| ----------------------------------------------------------------------------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | | User-action components (login, state change, call control, accept/decline, outdial) | "Props-in / callback-out interaction" | Error props (`loginFailure`, `saveError`) rendered as error UI; no internal retry — recovery owned by widget layer | -| Campaign preview offer (countdown + action) | "Campaign preview timeout & error" | Countdown timeout auto-action; failed action opens error dialog (alt branch) | +| Campaign preview offer (countdown + action) | "Campaign preview timeout & error" | Countdown timeout auto-action; failed action opens error dialog (alt branch) | ```mermaid sequenceDiagram @@ -214,6 +231,7 @@ sequenceDiagram ``` ## Class / Component Relationships + ```mermaid graph TD Index["index.ts (public barrel)"] @@ -243,9 +261,11 @@ graph TD CampTask --> CampError CampListItem --> CampCountdown ``` + The exported components are leaves of `index.ts`. Composition is one-directional: `Task` is the shared row reused by `IncomingTask` and `TaskList`; `CallControlCAD` decorates `CallControl` with a CAD header, `GlobalVariablesPanel`, and `TaskTimer`; `CallControl` owns the consult/transfer subtree under `CallControlCustom/` plus `AutoWrapupTimer`; the campaign family composes `CampaignTaskListItem`, `CampaignTaskPopover`, `CampaignCountdown`, and `CampaignErrorDialog`. Prop types are unified in `task.types.ts` via `Pick` over `ControlProps`/`TaskProps`. ## Use Cases + - **UC-1 Agent logs in:** Widget passes `teams`, `loginOptions`, `deviceType`, and handlers → `StationLoginComponent` renders selectors → agent selects device/team, optionally enters DN → clicks Continue/Save → component calls `login`/`saveLoginOptions`; `loginFailure`/`saveError` props render error UI. Evidence: `src/components/StationLogin/station-login.tsx`, `tests/components/StationLogin/station-login.tsx`. - **UC-2 Agent changes state:** `UserStateComponent` shows idle-code dropdown with current state + elapsed time → agent selects a code → `setAgentStatus(auxCodeId)` fires. Evidence: `src/components/UserState/user-state.tsx`, `tests/components/UserState/user-state.tsx`. - **UC-3 Agent controls an active call:** `CallControlComponent` builds buttons from `controlVisibility` → agent presses hold/mute/record/end/wrapup or opens consult/transfer popover → matching callback fires; wrapup requires a selected reason. Evidence: `src/components/task/CallControl/call-control.tsx`, `tests/components/task/CallControl/call-control.tsx`. @@ -257,10 +277,13 @@ The exported components are leaves of `index.ts`. Composition is one-directional UI flow per use case is detailed in the UI Flow section below. ## State Model + These components hold only transient, local UI state via React `useState` (e.g. open consult/transfer menu, selected-but-unsubmitted wrapup reason and id, mute-button disabled flag in `call-control.tsx`; selected tab, destination text, validation flag, selected ANI in `outdial-call.tsx`). They hold no domain/application state and never read or mutate the MobX store — all persistent state lives in `@webex/cc-store`, owned by the widget layer. Transitions are driven directly by user events and reset on prop changes/remount. No store slices, reducers, or actions are defined in this module (evidence: no MobX or store-singleton import in `src/components/**`). ## UI Flow + These are UI components; the non-happy-path states are part of the contract. + - **StationLogin:** login screen vs. profile mode; device-type select drives dial-number input visibility; Desktop option hidden when `hideDesktopLogin`; multiple-login alert when `showMultipleLoginAlert`; error states from `loginFailure`/`saveError`; Save/Continue disabled until valid. (`src/components/StationLogin/station-login.tsx`) - **UserState:** dropdown of idle/custom states with current state highlighted; `isSettingAgentStatus` shows a busy/disabled state; error-triggering idle codes styled distinctly; state timer renders via `formatTime`. (`src/components/UserState/user-state.tsx`) - **CallControl / CallControlCAD:** button visibility/enablement driven by `controlVisibility`; consult/transfer popover with Agents/Queues/Dial Number/Entry Point tabs (empty-state when no results, loading spinner while fetching); auto-wrapup countdown bar; wrapup requires reason selection before submit. (`src/components/task/CallControl/`, `src/components/task/CallControlCAD/`) @@ -270,11 +293,13 @@ These are UI components; the non-happy-path states are part of the contract. - **Campaign preview:** countdown bar (urgent styling near zero); accept "Connecting..." state; disabled accept/skip/remove flags; error dialog modal on failure. (`src/components/task/CampaignTask/`, `src/components/task/CampaignCountdown/`, `src/components/task/CampaignErrorDialog/`) ## Host Integration & Theming + - Components render Momentum UI primitives (`@momentum-ui/react-collaboration`, `@momentum-design/components/dist/react`) and inherit Momentum theming tokens (e.g. CSS custom properties such as `--mds-color-theme-background-glass-normal` referenced in `task.types.ts` defaults). They assume the host provides Momentum theming/CSS; no `ThemeProvider` is mounted inside this package. - React peer requirement: `react`/`react-dom` `>=18.3.1`, provided by the host. - The `wc.ts` build registers custom elements `component-cc-user-state`, `component-cc-station-login`, `component-cc-call-control`, `component-cc-call-control-cad`, `component-cc-incoming-task`, `component-cc-task-list`, `component-cc-out-dial-call`, `component-cc-realtime-transcript`, each guarded by a `customElements.get(...)` check before `define`. The host-facing custom elements (`widget-cc-*`) and their registration are owned by `cc-widgets`; see [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md). ## Pitfalls + - `wc.ts` imports `CallControlCADComponent` from `../CallControl/call-control` (i.e. it wraps the plain `CallControlComponent`, not the CAD component at `CallControlCAD/call-control-cad.tsx`). The `component-cc-call-control-cad` custom element therefore does NOT render the CAD header from `call-control-cad.tsx`. Confirm intended before relying on the custom-element CAD variant. Evidence: `src/wc.ts` import block. - Public prop types are `Pick`s over larger interfaces (`IStationLoginProps`, `ControlProps`, `TaskProps`). A field can exist on the interface yet not be public — only keys in the `Pick` are part of the contract. Don't infer a prop is supported just because it's on the interface. - Components do not own state: passing a new object/array/callback identity each render (instead of memoized) causes avoidable re-renders, especially for list components (`TaskList`, `OutdialCall`). Memoize props in the widget layer. @@ -283,6 +308,7 @@ These are UI components; the non-happy-path states are part of the contract. - Campaign types (`CampaignCallProcessingDetails`) are bridge types for SDK fields not yet in the installed SDK typings; they can drift from the runtime payload until the SDK is updated. Evidence: `src/components/task/task.types.ts`. ## Module Do's / Don'ts + - DO: keep components props-only — pass all data and callbacks in; never import the `@webex/cc-store` singleton or call the SDK here. - DO: derive display data in co-located `*.utils.ts(x)` (and shared logic in `src/utils`) so components stay thin and unit-testable. - DO: add new public components/types through `src/index.ts` and, for the custom-element build, register them in `src/wc.ts` with an explicit r2wc prop type map guarded by `customElements.get`. @@ -291,29 +317,33 @@ These are UI components; the non-happy-path states are part of the contract. - DON'T: widen a component's public surface by exporting internal subcomponents (e.g. `Task`, `CallControlCustom/*`) from the barrel. ## Export Stability + This package is published (`@webex/cc-components`, `main` → `dist/index.js`, `types` → `dist/types/index.d.ts`, and a `./wc` subpath export). The `.d.ts` of the `index.ts` barrel and the `task.types.ts`/`*.types.ts` exports are the type surface. Semver sensitivity: adding an optional prop to a `Pick`ed component type or adding a new exported component is a minor; removing/renaming a picked prop, narrowing a prop type, removing an exported component, or renaming/removing a `component-cc-*` custom element (or changing its r2wc prop type) is a major. Evidence: `package.json` (`exports`, `version`), `src/index.ts`, `src/wc.ts`. ## Test-Case Strategy (module) + Each component is tested in isolation with React Testing Library: render from a minimal props object, assert rendered UI (positive) and assert callbacks fire on interaction / error props render error UI (negative), with snapshot tests guarding stable markup. Utils have dedicated `*.utils.tsx` tests asserting pure transformations (e.g. button building, sorting, data extraction). Edge cases covered include empty task list/incoming task (hidden render), `hideDesktopLogin` across modes, campaign timeout/error, and transcript empty + sort. Gaps: no dedicated test file for `src/utils/index.ts` (`formatTime`/`getMediaTypeInfo`) and no explicit assertion that components are `withMetrics`-wrapped. -| Behavior / Requirement | Existing test evidence | Gap | -|---|---|---| -| `CC-COMPONENTS-R-001` | `tests/components/StationLogin/station-login.tsx`, `tests/components/UserState/user-state.tsx`, `tests/components/task/CallControl/call-control.tsx` | None | -| `CC-COMPONENTS-R-002` | Import sites across `tests/components/**` | No single test asserting the full export list | -| `CC-COMPONENTS-R-003` | `tests/components/StationLogin/station-login.tsx` (actions + failure + save error) | None | -| `CC-COMPONENTS-R-004` | `tests/components/StationLogin/station-login.tsx` (hideDesktopLogin cases) | None | -| `CC-COMPONENTS-R-005` | `tests/components/UserState/user-state.tsx`, `tests/components/UserState/user-state.utils.tsx` | None | -| `CC-COMPONENTS-R-006` | `tests/components/task/CallControl/call-control.tsx`, `tests/components/task/CallControl/call-control.utils.tsx` | None | -| `CC-COMPONENTS-R-007` | `tests/components/task/CallControlCAD/call-control-cad.tsx` | None | -| `CC-COMPONENTS-R-008` | `tests/components/task/IncomingTask/incoming-task.tsx`, `tests/components/task/IncomingTask/incoming-task.utils.tsx` | None | -| `CC-COMPONENTS-R-009` | `tests/components/task/TaskList/task-list.tsx`, `tests/components/task/TaskList/task-list.utils.tsx` | None | -| `CC-COMPONENTS-R-010` | `tests/components/task/OutdialCall/out-dial-call.tsx` | None | -| `CC-COMPONENTS-R-011` | `tests/components/task/RealtimeTranscript/realtime-transcript.tsx` | None | -| `CC-COMPONENTS-R-012` | `tests/components/task/CampaignTask/campaign-task.test.tsx`, `tests/components/task/CampaignErrorDialog/campaign-error-dialog.tsx`, `tests/components/task/CampaignCountdown/campaign-countdown.tsx` | None | -| `CC-COMPONENTS-R-013` | `tests/components/task/CallControl/call-control.utils.tsx`, component snapshots | No dedicated `formatTime`/`getMediaTypeInfo` unit test | -| `CC-COMPONENTS-R-014` | `tests/hooks/useIntersectionObserver.test.ts` | None | -| `CC-COMPONENTS-R-015` | None found (covered indirectly via render tests) | No explicit `withMetrics`-wrapping assertion | +| Behavior / Requirement | Existing test evidence | Gap | +| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | +| `CC-COMPONENTS-R-001` | `tests/components/StationLogin/station-login.tsx`, `tests/components/UserState/user-state.tsx`, `tests/components/task/CallControl/call-control.tsx` | None | +| `CC-COMPONENTS-R-002` | Import sites across `tests/components/**` | No single test asserting the full export list | +| `CC-COMPONENTS-R-003` | `tests/components/StationLogin/station-login.tsx` (actions + failure + save error) | None | +| `CC-COMPONENTS-R-004` | `tests/components/StationLogin/station-login.tsx` (hideDesktopLogin cases) | None | +| `CC-COMPONENTS-R-005` | `tests/components/UserState/user-state.tsx`, `tests/components/UserState/user-state.utils.tsx` | None | +| `CC-COMPONENTS-R-006` | `tests/components/task/CallControl/call-control.tsx`, `tests/components/task/CallControl/call-control.utils.tsx` | None | +| `CC-COMPONENTS-R-007` | `tests/components/task/CallControlCAD/call-control-cad.tsx` | None | +| `CC-COMPONENTS-R-008` | `tests/components/task/IncomingTask/incoming-task.tsx`, `tests/components/task/IncomingTask/incoming-task.utils.tsx` | None | +| `CC-COMPONENTS-R-009` | `tests/components/task/TaskList/task-list.tsx`, `tests/components/task/TaskList/task-list.utils.tsx` | None | +| `CC-COMPONENTS-R-010` | `tests/components/task/OutdialCall/out-dial-call.tsx` | None | +| `CC-COMPONENTS-R-011` | `tests/components/task/RealtimeTranscript/realtime-transcript.tsx` | None | +| `CC-COMPONENTS-R-012` | `tests/components/task/CampaignTask/campaign-task.test.tsx`, `tests/components/task/CampaignErrorDialog/campaign-error-dialog.tsx`, `tests/components/task/CampaignCountdown/campaign-countdown.tsx` | None | +| `CC-COMPONENTS-R-013` | `tests/components/task/CallControl/call-control.utils.tsx`, component snapshots | No dedicated `formatTime`/`getMediaTypeInfo` unit test | +| `CC-COMPONENTS-R-014` | `tests/hooks/useIntersectionObserver.test.ts` | None | +| `CC-COMPONENTS-R-015` | None found (covered indirectly via render tests) | No explicit `withMetrics`-wrapping assertion | +| `CC-COMPONENTS-R-016` | `tests/components/StationLogin/E911Modal/e911-modal.test.tsx` | None | ## Traceability + - Repo architecture: [`ARCHITECTURE.md`](../../../../ai-docs/ARCHITECTURE.md) · Registry: [`SPEC_INDEX.md`](../../../../ai-docs/SPEC_INDEX.md) · Contracts: [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) - Coverage state & contracts baseline: `.sdd/manifest.json` diff --git a/packages/contact-center/cc-components/src/components/StationLogin/E911Modal/e911-modal.constants.ts b/packages/contact-center/cc-components/src/components/StationLogin/E911Modal/e911-modal.constants.ts index 1e2afcd7a..bfa67cdc6 100644 --- a/packages/contact-center/cc-components/src/components/StationLogin/E911Modal/e911-modal.constants.ts +++ b/packages/contact-center/cc-components/src/components/StationLogin/E911Modal/e911-modal.constants.ts @@ -9,4 +9,5 @@ export const E911ModalLabels = { CHECKBOX_LABEL: 'I have read the notification', CANCEL: 'Cancel', SAVE_AND_CONTINUE: 'Save & Continue', + SAVE_ERROR_MESSAGE: 'Failed to save. Please try again.', }; diff --git a/packages/contact-center/cc-components/src/components/StationLogin/E911Modal/e911-modal.style.scss b/packages/contact-center/cc-components/src/components/StationLogin/E911Modal/e911-modal.style.scss index 059f9181b..05bd19d4f 100644 --- a/packages/contact-center/cc-components/src/components/StationLogin/E911Modal/e911-modal.style.scss +++ b/packages/contact-center/cc-components/src/components/StationLogin/E911Modal/e911-modal.style.scss @@ -46,4 +46,8 @@ cursor: pointer; } } + + .e911-save-error { + color: var(--mds-color-theme-text-error-normal); + } } diff --git a/packages/contact-center/cc-components/src/components/StationLogin/E911Modal/e911-modal.tsx b/packages/contact-center/cc-components/src/components/StationLogin/E911Modal/e911-modal.tsx index 998a91e80..15b494f7f 100644 --- a/packages/contact-center/cc-components/src/components/StationLogin/E911Modal/e911-modal.tsx +++ b/packages/contact-center/cc-components/src/components/StationLogin/E911Modal/e911-modal.tsx @@ -6,10 +6,14 @@ import './e911-modal.style.scss'; const E911Modal: React.FC = ({isOpen, onSaveAndContinue, onCancel}) => { const [isChecked, setIsChecked] = useState(false); + const [isSaving, setIsSaving] = useState(false); + const [saveError, setSaveError] = useState(''); useEffect(() => { if (!isOpen) { setIsChecked(false); + setIsSaving(false); + setSaveError(''); } }, [isOpen]); @@ -22,9 +26,20 @@ const E911Modal: React.FC = ({isOpen, onSaveAndContinue, onCance onCancel(); }; - const handleSaveAndContinue = () => { - if (isChecked) { - onSaveAndContinue(); + const handleSaveAndContinue = async () => { + if (!isChecked || isSaving) { + return; + } + + setIsSaving(true); + setSaveError(''); + + try { + await onSaveAndContinue(); + } catch { + setSaveError(E911ModalLabels.SAVE_ERROR_MESSAGE); + } finally { + setIsSaving(false); } }; @@ -61,15 +76,26 @@ const E911Modal: React.FC = ({isOpen, onSaveAndContinue, onCance label={E911ModalLabels.CHECKBOX_LABEL} /> + + {saveError && ( + + {saveError} + + )} -