diff --git a/packages/contact-center/ai-docs/migration/call-control-hook-migration.md b/packages/contact-center/ai-docs/migration/call-control-hook-migration.md index 7df987410..896a9b9ea 100644 --- a/packages/contact-center/ai-docs/migration/call-control-hook-migration.md +++ b/packages/contact-center/ai-docs/migration/call-control-hook-migration.md @@ -20,7 +20,7 @@ The following functions are deleted — their only consumer (`getControlsVisibil |----------|-------------| | `deviceType` | SDK handles via `UIControlConfig` | | `featureFlags` | SDK handles via `config.isEndTaskEnabled`, `config.isEndConsultEnabled`, `config.isRecordingEnabled` | -| `conferenceEnabled` | SDK computes conference/mergeToConference/exitConference visibility based on task state and config | +| ~~`conferenceEnabled`~~ | **RESTORED** — This is an application-level config (not a feature flag). See [Fix Log: Restore conferenceEnabled](#fix-restore-conferenceenabled-prop--application-level-conference-gating) below | ### Props retained @@ -415,7 +415,7 @@ export function calculateStateTimerData( ## Migration Gotchas -1. **`UIControlConfig` is built by SDK:** Widgets do NOT provide it. The SDK handles feature-flag gating internally via `config.isEndTaskEnabled`, `config.isEndConsultEnabled`, `config.isRecordingEnabled`. Widget props `deviceType`, `featureFlags`, and `conferenceEnabled` can be **removed**. There is no `applyFeatureGates` function. **Retain `agentId`** — timer utils need it for participant lookup. +1. **`UIControlConfig` is built by SDK:** Widgets do NOT provide it. The SDK handles feature-flag gating internally via `config.isEndTaskEnabled`, `config.isEndConsultEnabled`, `config.isRecordingEnabled`. Widget props `deviceType` and `featureFlags` can be **removed**. **`conferenceEnabled` is RETAINED** — it is an application-level config (not a feature flag) that gates conference UI at the consumer level. There is no `applyFeatureGates` function. **Retain `agentId`** — timer utils need it for participant lookup. 2. **`isHeld` derivation:** Hold control can be `VISIBLE_DISABLED` in conference/consulting states without meaning the call is held. Do NOT derive from `controls.hold.isEnabled` — it is an action flag (button clickability), not hold state. Get hold state from the task object (SDK tracks hold state internally). `findHoldStatus()` is dead code and will be removed (see [store-task-utils-migration.md](./store-task-utils-migration.md)). @@ -456,3 +456,69 @@ export function calculateStateTimerData( --- _Parent: [migration-overview.md](./migration-overview.md)_ + +--- + +## Migration Fix Log + +### Fix: `isHeld` Reactivity — Hold Button State and Multi-Login Sync + +- **Issue**: After migration, the hold button icon/tooltip did not toggle on click, and multi-login hold/resume did not sync across systems. +- **Root Cause**: The old `controlVisibility.isHeld` was removed. `controls.hold.isEnabled` is an action flag, not state. `task.data.isOnHold` is not populated by SDK at runtime. The SDK state machine also lacked `HOLD_SUCCESS`/`UNHOLD_SUCCESS` transitions for multi-login scenarios. +- **SDK Source of Truth**: `uiControlsComputer.ts` derives `isHeld` from `serverHold ?? state === TaskState.HELD`. `controls.hold` is `VISIBLE_ENABLED` in both `CONNECTED` and `HELD` states — it's an action flag, not a state indicator. +- **Fix Pattern** (in `useCallControl` hook — `helper.ts`): + ```typescript + import { isInteractionOnHold } from '@webex/cc-store'; + + const [isHeld, setIsHeld] = useState(() => + currentTask ? isInteractionOnHold(currentTask) : false + ); + + useEffect(() => { + setIsHeld(currentTask ? isInteractionOnHold(currentTask) : false); + }, [currentTask]); + + // In holdCallback: setIsHeld(true); + // In resumeCallback: setIsHeld(false); + // Return isHeld from hook + ``` +- **SDK Fix**: Added `HOLD_SUCCESS` handler to `CONNECTED` state and `UNHOLD_SUCCESS` handler to `HELD` state in `TaskStateMachine.ts` for multi-login sync. + +### Fix: Restore `conferenceEnabled` Prop — Application-Level Conference Gating + +- **Issue**: During the task-refactor migration, the `conferenceEnabled` prop was removed from the widget APIs. This prop is **not a feature flag** — it is an application-level configuration passed from `App.tsx` that controls whether conference-related UI controls should be available to the agent. Without it, applications cannot disable conference features regardless of SDK `uiControls`. +- **Root Cause**: The migration assumed all UI visibility is driven exclusively by `task.uiControls` from the SDK state machine. However, `conferenceEnabled` is an application-level override that gates conference availability at the consumer level, independent of the SDK's computed state. +- **Design Decision (Option A — Widget-Side Override at Button Level)**: `conferenceEnabled` is applied directly in the button builder functions (`buildCallControlButtons` and `createConsultButtons`) where conference-related buttons are defined. When `false`, the `isVisible` property of conference buttons (`conference`, `exitConference`, `merge`) is forced to `false` regardless of SDK `uiControls`. When `true` (default), SDK controls pass through unchanged. +- **Gating Pattern** (in button builder functions): + ```typescript + // call-control.utils.ts — buildCallControlButtons + // conferenceEnabled param defaults to true + { + id: 'conference', + isVisible: conferenceEnabled && (controls?.mergeToConference?.isVisible ?? false) && !!handleConsultConferencePress, + }, + { + id: 'exitConference', + isVisible: conferenceEnabled && (controls?.exitConference?.isVisible ?? false), + }, + + // call-control-custom.utils.ts — createConsultButtons + { + key: 'conference', + isVisible: conferenceEnabled && (controls?.mergeToConference?.isVisible ?? false), + }, + ``` +- **Prop Flow**: `App.tsx` → `CallControl`/`CallControlCAD` → `useCallControl` hook → returned as prop → `CallControlComponent` → `buildCallControlButtons()` / `CallControlConsultComponent` → `createConsultButtons()` +- **Files Changed**: + - `cc-components/…/task.types.ts`: Added `conferenceEnabled: boolean` to `ControlProps`, `CallControlComponentProps`, `CallControlConsultComponentsProps` + - `cc-components/…/call-control.utils.ts`: Added `conferenceEnabled` param to `buildCallControlButtons`, gated `conference` and `exitConference` buttons + - `cc-components/…/call-control-custom.utils.ts`: Added `conferenceEnabled` param to `createConsultButtons`, gated `conference` (merge) button + - `cc-components/…/call-control.tsx`: Destructured `conferenceEnabled`, passed to `buildCallControlButtons` + - `cc-components/…/call-control-consult.tsx`: Destructured `conferenceEnabled`, passed to `createConsultButtons` + - `cc-components/…/call-control-cad.tsx`: Destructured `conferenceEnabled`, passed to `CallControlConsultComponent` + - `task/src/task.types.ts`: Added `conferenceEnabled` to `CallControlProps` and `useCallControlProps` + - `task/src/helper.ts`: Destructured `conferenceEnabled` (default `true`), returned from hook + - `task/src/CallControl/index.tsx` and `CallControlCAD/index.tsx`: Pass `conferenceEnabled` to `useCallControl` + - `cc-widgets/src/wc.ts`: Exposed `conferenceEnabled` as r2wc `boolean` prop on `WebCallControl` and `WebCallControlCAD` +- **Consumer Usage**: Apps pass `conferenceEnabled={true|false}` as a prop to `` or ``. Web component consumers set the `conference-enabled` attribute. Defaults to `true` if not provided. +- **Result**: Conference buttons (merge, exit conference) are hidden when `conferenceEnabled` is `false`, while all other SDK-driven controls remain unaffected. diff --git a/packages/contact-center/ai-docs/migration/component-layer-migration.md b/packages/contact-center/ai-docs/migration/component-layer-migration.md index 5083a2313..266cd516b 100644 --- a/packages/contact-center/ai-docs/migration/component-layer-migration.md +++ b/packages/contact-center/ai-docs/migration/component-layer-migration.md @@ -370,11 +370,11 @@ This function builds the main call control button array. It references 12 old co - `isHeld: boolean` → get from the task object (SDK provides hold state); remove `findHoldStatus` derivation - `deviceType: string` → REMOVE (SDK handles) - `featureFlags: {[key: string]: boolean}` → REMOVE (SDK handles) -- `conferenceEnabled: boolean` → REMOVE (SDK handles) +- ~~`conferenceEnabled: boolean` → REMOVE~~ **RESTORED** — application-level config (not a feature flag), applied at button builder level - `agentId: string` → RETAIN (needed for timer participant lookup) ### `CallControlCAD` — task package and cc-components view -- **task/src/CallControlCAD/index.tsx:** `deviceType`, `featureFlags`, `conferenceEnabled` are used today in `getControlsVisibility` (task-util.ts lines 421–525). The **SDK** handles feature-flag-like gating internally via `config.isEndTaskEnabled`, `config.isEndConsultEnabled`, `config.isRecordingEnabled` from agent profile and `callProcessingDetails`. Since widgets will read `task.uiControls` instead of calling `getControlsVisibility`, these props can be **removed** — the SDK has already computed them. **Retain `agentId`** for timer participant lookup. +- **task/src/CallControlCAD/index.tsx:** `deviceType` and `featureFlags` are used today in `getControlsVisibility` (task-util.ts lines 421–525). The **SDK** handles feature-flag-like gating internally via `config.isEndTaskEnabled`, `config.isEndConsultEnabled`, `config.isRecordingEnabled` from agent profile and `callProcessingDetails`. Since widgets will read `task.uiControls` instead of calling `getControlsVisibility`, `deviceType` and `featureFlags` can be **removed** — the SDK has already computed them. **`conferenceEnabled` is RETAINED** — it is an application-level configuration (not a feature flag) passed from the consumer app. **Retain `agentId`** for timer participant lookup. - **cc-components/.../CallControlCAD/call-control-cad.tsx:** This view consumes `controlVisibility` (and related state flags such as `isConferenceInProgress`, `isHeld`, `isConsultReceived`, `recordingIndicator`, `isConsultInitiatedOrAccepted`). It must be updated to use `TaskUIControls` and the new prop shape when replacing `ControlVisibility`; otherwise migration will leave stale references and break at compile or runtime. ### Files NOT Impacted (Confirmed) @@ -581,7 +581,7 @@ const WebTaskList = r2wc(TaskListComponent, { | `cc-components/.../TaskList/task-list.utils.ts` | Update `extractTaskListItemData()`: remove `isBrowser` param and `store.isDeclineButtonEnabled` usage; use `task.uiControls?.accept` / `task.uiControls?.decline` for button text and disable state | **MEDIUM** | | `cc-components/.../CallControlCAD/call-control-cad.tsx` | Replace `ControlVisibility` / legacy control-shape usage with `TaskUIControls`; update props (`controlVisibility.isConferenceInProgress`, `isHeld`, `isConsultReceived`, `recordingIndicator`, `isConsultInitiatedOrAccepted`, etc.) | **MEDIUM** | | `cc-components/src/wc.ts` | Update Web Component prop definitions: remove `isBrowser` from `WebIncomingTask` and `WebTaskList` r2wc props when migrating to per-task uiControls; align with React prop changes so WC consumers do not pass obsolete attributes | **LOW** | -| `task/src/CallControlCAD/index.tsx` | **Remove** `deviceType`, `featureFlags`, `conferenceEnabled` (SDK handles via `task.uiControls`); retain `agentId` for timer participant lookup | **MEDIUM** | +| `task/src/CallControlCAD/index.tsx` | **Remove** `deviceType`, `featureFlags` (SDK handles via `task.uiControls`); **retain** `conferenceEnabled` (app-level config) and `agentId` (timer participant lookup) | **MEDIUM** | | All test files for above | Update mocks and assertions | **HIGH** | --- @@ -603,3 +603,50 @@ const WebTaskList = r2wc(TaskListComponent, { --- _Part of the task refactor migration doc set (overview in PR 1/4)._ + +--- + +## Migration Fix Log + +### Fix: Duplicate Transfer Button — Wrong `uiControls` Field Mapping + +- **Issue**: After accepting a call, both "Transfer" and "Transfer Call" buttons appeared simultaneously. The `transferConsult` button and the consult strip `transfer` button were both reading `controls.transfer` instead of `controls.consultTransfer`. +- **Root Cause**: Three button definitions all mapped to `controls.transfer`: + - `call-control.utils.ts` — `transferConsult` button used `controls.transfer` (should be `controls.consultTransfer`) + - `call-control-custom.utils.ts` — consult strip `transfer` button used `controls.transfer` (should be `controls.consultTransfer`) + - `call-control.utils.ts` — main `transfer` button correctly used `controls.transfer` +- **SDK Source of Truth**: `uiControlsComputer.ts` computes `consultTransfer: DISABLED` for `CONNECTED` state and only enables it during active consultation. The main `transfer` control handles the primary transfer action. +- **Fix**: + - `call-control.utils.ts` L252-258: Changed `transferConsult` button's `disabled` and `isVisible` from `controls?.transfer` to `controls?.consultTransfer` + - `call-control-custom.utils.ts` L46-54: Changed consult strip `transfer` button's `disabled` and `isVisible` from `controls?.transfer` to `controls?.consultTransfer` +- **Result**: Only the main "Transfer Call" button shows in `CONNECTED` state. The `transferConsult` button only appears when `consultTransfer` is explicitly enabled by the SDK during active consultation. + +### Fix: Hold Button Icon/Tooltip Not Toggling & Multi-Login Hold State Not Syncing + +- **Issue**: (1) After clicking Hold, the button icon stayed as pause and tooltip stayed as "Hold the call" instead of changing to play/"Resume the call". (2) In multi-login scenarios, holding/resuming on one system did not reflect on the other system. +- **Root Cause**: + - The old `controlVisibility.isHeld` was removed during migration. The replacement `controls.hold.isEnabled` is an **action flag** (can the user click hold?), not the current hold state. `task.data.isOnHold` exists in SDK types but is not populated at runtime. + - For multi-login: The SDK's `TaskStateMachine.ts` `CONNECTED` state had no handler for `HOLD_SUCCESS` (another system held), and `HELD` state had no handler for `UNHOLD_SUCCESS` (another system resumed). These events were silently dropped. +- **Fix (Widgets)**: + - `helper.ts` (`useCallControl` hook): Added `useState(isHeld)` initialized from `isInteractionOnHold(currentTask)`. Updated `holdCallback` to `setIsHeld(true)` and `resumeCallback` to `setIsHeld(false)`. Added `useEffect([currentTask])` to re-sync from `isInteractionOnHold` on task reference changes (covers multi-login `refreshTaskList`). + - `call-control.utils.ts`: Added `isHeld: boolean` parameter to `buildCallControlButtons()`. Hold button uses `isHeld ? 'play-bold' : 'pause-bold'` for icon and `isHeld ? RESUME_CALL : HOLD_CALL` for tooltip. + - `call-control.tsx`: Destructured `isHeld` from props, passed to `buildCallControlButtons()` and `handleToggleHoldUtil()`. + - `task.types.ts`: Added `'isHeld'` to `CallControlComponentProps` pick list. +- **Fix (SDK)**: Added `HOLD_SUCCESS` transition in `CONNECTED` state and `UNHOLD_SUCCESS` transition in `HELD` state of `TaskStateMachine.ts`, both with actions `['updateTaskData', 'setHoldState', 'emitTaskHold'/'emitTaskResume']`. +- **Result**: Hold button icon/tooltip toggles correctly on click. Multi-login hold/resume state syncs across systems via SDK state machine transitions. + +### Fix: Restore `conferenceEnabled` Prop — Application-Level Conference Gating + +- **Issue**: The `conferenceEnabled` prop was removed from widget APIs during migration. This is an application-level configuration (not a feature flag) passed from the consumer app that controls whether conference-related UI controls are available to the agent. +- **Root Cause**: The migration assumed all UI visibility is exclusively SDK-driven. However, `conferenceEnabled` is a consumer-level override independent of SDK state. +- **Design Decision**: Option A — widget-side override applied directly at the button builder level. When `conferenceEnabled` is `false`, the `isVisible` property of conference-related buttons (`conference`, `exitConference`, `merge`) is forced to `false` in `buildCallControlButtons()` and `createConsultButtons()`. Defaults to `true`. +- **Component-Layer Changes**: + - `task.types.ts`: Added `conferenceEnabled: boolean` to `ControlProps`, `CallControlComponentProps`, `CallControlConsultComponentsProps` + - `call-control.utils.ts`: Added `conferenceEnabled` param to `buildCallControlButtons()`, gated `conference` and `exitConference` buttons via `conferenceEnabled && (controls?.…isVisible)` + - `call-control-custom.utils.ts`: Added `conferenceEnabled` param to `createConsultButtons()`, gated `conference` (merge) button + - `call-control.tsx`: Destructured `conferenceEnabled` from props, passed to `buildCallControlButtons()` + - `call-control-consult.tsx`: Destructured `conferenceEnabled`, passed to `createConsultButtons()` + - `call-control-cad.tsx`: Destructured `conferenceEnabled`, passed to `CallControlConsultComponent` + - `cc-widgets/src/wc.ts`: Exposed `conferenceEnabled` as r2wc `boolean` prop on `WebCallControl` and `WebCallControlCAD` +- **No SDK changes required**: Gating is applied at the widget component layer directly on button definitions. +- **Result**: Conference merge and exit buttons are hidden when `conferenceEnabled={false}`. All other SDK-driven controls remain unaffected. diff --git a/packages/contact-center/ai-docs/migration/incoming-task-migration.md b/packages/contact-center/ai-docs/migration/incoming-task-migration.md index 00ac01662..c0ae60d13 100644 --- a/packages/contact-center/ai-docs/migration/incoming-task-migration.md +++ b/packages/contact-center/ai-docs/migration/incoming-task-migration.md @@ -246,3 +246,19 @@ const IncomingTaskComponent = ({ acceptControl, declineControl, onAccept, onReje --- _Parent: [migration-overview.md](./migration-overview.md)_ + +--- + +## Migration Fix Log + +### Fix: Restore `isDeclineButtonEnabled` from Store to Component Level + +- **Issue**: During the task-refactor migration, `store.isDeclineButtonEnabled` was removed from the IncomingTask and TaskList component layers. The migration docs instructed replacing it with `task.uiControls.decline.isEnabled`. However, the store property is still set by `handleAutoAnswer` in `storeEventsWrapper.ts` and needs to be kept as an additional override for the decline button enabled state. +- **Root Cause**: The migration assumed `task.uiControls.decline.isEnabled` fully replaces `store.isDeclineButtonEnabled`, but the store property provides an additional auto-answer override that the SDK state machine may not account for in all scenarios. +- **Fix**: + - `task/src/helper.ts` (`useIncomingTask`): Reads `store.isDeclineButtonEnabled` and merges it with the SDK's `declineControl` — if either the SDK or the store says decline is enabled, the button is enabled: `isEnabled: sdkDeclineControl.isEnabled || store.isDeclineButtonEnabled`. + - `task/src/TaskList/index.tsx`: Reads `store.isDeclineButtonEnabled` and passes it as a prop to `TaskListComponent`. + - `cc-components/.../task.types.ts`: Added `isDeclineButtonEnabled?: boolean` to `TaskListComponentProps`. + - `cc-components/.../task-list.tsx`: Destructures `isDeclineButtonEnabled` and passes it to `extractTaskListItemData`. + - `cc-components/.../task-list.utils.ts`: `extractTaskListItemData` accepts `isDeclineButtonEnabled` param and merges it with `task.uiControls.decline.isEnabled`: `isEnabled: sdkDecline.isEnabled || !!isDeclineButtonEnabled`. +- **Result**: The decline button is enabled when either the SDK's `task.uiControls.decline.isEnabled` is `true` OR `store.isDeclineButtonEnabled` is `true` (set by auto-answer handler). Both IncomingTask and TaskList components respect this combined logic. diff --git a/packages/contact-center/ai-docs/migration/migration-overview.md b/packages/contact-center/ai-docs/migration/migration-overview.md index 5b6097f4f..612077df7 100644 --- a/packages/contact-center/ai-docs/migration/migration-overview.md +++ b/packages/contact-center/ai-docs/migration/migration-overview.md @@ -173,5 +173,56 @@ Widgets no longer compute control visibility — `task.uiControls` is the single --- +## Migration Fix Log + +### 2026-03-30 - Dial Number Transfer Wrapup Visibility (Complete Fix) + +**Issue**: After dial number consult transfers, wrapup button not appearing. Tests in SET_6 failing with `findFirstVisibleWrapupIndex` returning -1 (timeout after 15 seconds). + +**Root Cause (Deeper Analysis)**: +1. Initial hypothesis: `shouldWrapUpOrIsInitiator` guard relied on backend `wrapUpRequired` flag which wasn't set for dial number transfers. +2. **Actual root cause**: Backend sends `AgentConsultEnded` **before** `AgentConsultTransferred` for dial number transfers. +3. Event ordering issue: CONSULT_END (clears `consultInitiator`) → TRANSFER_SUCCESS (checks `consultInitiator`, now false) → transitions to CONNECTED instead of WRAPPING_UP. + +**Fix Location**: SDK `/packages/@webex/contact-center/src/services/task/state-machine/` + +**Changes Made**: +1. **TaskStateMachine.ts** - Updated TRANSFER_SUCCESS guards (lines 256-267, 336-347, 489-505): + - Changed to directly check `consultInitiator` instead of using `guards.shouldWrapUpOrIsInitiator` + - Ensures consult initiators always wrap up regardless of backend flags + +2. **Added `transferRequested` flag** to track transfer initiation: + - **types.ts**: Added `transferRequested: boolean` to TaskContext + - **constants.ts**: Added `TRANSFER` event + - **actions.ts**: + - Initialize `transferRequested: false` in `createInitialContext` + - Added `setTransferRequested` and `clearTransferRequested` actions + - Added `clearConsultStatePreservingTransfer` action that preserves `consultInitiator` if `transferRequested` is true + - **TaskStateMachine.ts**: + - CONNECTED, HELD, CONSULTING states: Added TRANSFER event handler that sets `transferRequested` flag + - CONSULT_END in CONSULTING state: Changed to use `clearConsultStatePreservingTransfer` instead of `clearConsultState` + - TRANSFER_SUCCESS in all states (CONNECTED, HELD, CONSULTING): Added `clearTransferRequested` to ALL branches (wrapup and fallback) + - TRANSFER_FAILED in all states: Added `clearTransferRequested` action + - **Voice.ts**: `transfer()` method now dispatches TRANSFER event before API call + +**Why**: For dial number transfers, backend event ordering can vary - CONSULT_END may arrive before TRANSFER_SUCCESS. The `transferRequested` flag tracks that a transfer is in progress, preventing CONSULT_END from clearing `consultInitiator` prematurely. This ensures TRANSFER_SUCCESS can properly check `consultInitiator` for wrapup transition. + +**Impact on Widgets**: No widget changes needed. Pure SDK state machine fix. Widgets already consume `task.uiControls.wrapup.isVisible`. + +**Tests Fixed**: SET_6 Tests 1, 2, 4, 9 (all dial number transfer wrapup visibility failures) + +**Fix Iterations**: +- Iteration 1-3: Implemented transferRequested flag and preservation logic, but only added clearTransferRequested to CONSULTING state +- Iteration 4 (2026-03-31): Discovered CONNECTED and HELD states' TRANSFER_SUCCESS handlers were missing clearTransferRequested. This was critical because when CONSULT_END arrives during transfer, state transitions CONSULTING → HELD, and TRANSFER_SUCCESS is then handled in HELD state. Without cleanup in HELD state, the flag would leak. Fixed by adding clearTransferRequested to ALL TRANSFER_SUCCESS branches in ALL states +- **CRITICAL DISCOVERY (2026-03-31)**: SDK was on WRONG BRANCH (`ADD_MISSING_EVENT_EMITTER_TYPES` instead of `task-refactor`). This meant: + - stateMachineService was not initialized + - All previous fix iterations were applied to wrong branch + - Widgets were NOT using state machine at all + - All test failures were due to missing state machine, not implementation bugs + - **Resolution**: Switched SDK to `task-refactor` branch and re-applied all fixes. Tests must be re-run to validate fixes work on correct branch. + +--- + _Created: 2026-03-09_ _Updated: 2026-03-24 (added dead code removal and task-object source of truth sections; aligned with PR #648 decisions)_ +_Updated: 2026-03-30 (added dial number transfer wrapup fix log)_ diff --git a/packages/contact-center/ai-docs/migration/store-task-utils-migration.md b/packages/contact-center/ai-docs/migration/store-task-utils-migration.md index a5266735c..89a145598 100644 --- a/packages/contact-center/ai-docs/migration/store-task-utils-migration.md +++ b/packages/contact-center/ai-docs/migration/store-task-utils-migration.md @@ -152,9 +152,9 @@ The old `getControlsVisibility` applied integrator-provided widget props (`featu | `featureFlags.isEndCallEnabled` | `config.isEndTaskEnabled` | | `featureFlags.isEndConsultEnabled` | `config.isEndConsultEnabled` | | `featureFlags.webRtcEnabled` (recording gate) | `config.isRecordingEnabled` | -| `conferenceEnabled` | SDK computes conference/mergeToConference/exitConference visibility based on task state and config | +| ~~`conferenceEnabled`~~ | **RESTORED** — This is an application-level config (not a feature flag). Applied at button builder level to gate conference button visibility. See call-control-hook-migration.md and component-layer-migration.md fix logs | -Since `task.uiControls` already reflects these gates, the widget layer can **remove** the `featureFlags`, `conferenceEnabled`, and `deviceType` props — no widget-side overlay is needed. +Since `task.uiControls` already reflects these gates, the widget layer can **remove** the `featureFlags` and `deviceType` props. **`conferenceEnabled` is RETAINED** — it is an application-level configuration passed from the consumer app that controls conference UI availability independently of SDK state. ```typescript const controls = currentTask?.uiControls ?? getDefaultUIControls(); @@ -170,7 +170,7 @@ const controls = currentTask?.uiControls ?? getDefaultUIControls(); - [ ] 12 state constants deleted; 7 participant/media constants kept - [ ] `getControlsVisibility` + 22 visibility functions deleted from `task-util.ts` - [ ] `findHoldTimestamp` dual-signature (task vs interaction) not confused -- [ ] Widget props `featureFlags`, `conferenceEnabled`, `deviceType` removed (SDK handles via `UIControlConfig`) +- [ ] Widget props `featureFlags`, `deviceType` removed (SDK handles via `UIControlConfig`); `conferenceEnabled` **retained** (application-level config) - [ ] No regression in conference participant display, hold timers, or switch-call actions - [ ] Downstream (Epic) confirmed unused before removing barrel exports diff --git a/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/call-control-consult.tsx b/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/call-control-consult.tsx index 65df644b3..9785df737 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/call-control-consult.tsx +++ b/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/call-control-consult.tsx @@ -15,8 +15,9 @@ const CallControlConsultComponent: React.FC = switchToMainCall, logger, isMuted, - controlVisibility, + controls, toggleConsultMute, + conferenceEnabled = true, }) => { // Use the label and timestamp calculated in helper.ts // Stable key based on timestamp to prevent timer resets @@ -27,13 +28,14 @@ const CallControlConsultComponent: React.FC = const buttons = createConsultButtons( isMuted, - controlVisibility, + controls, consultTransfer, toggleConsultMute, endConsultCall, consultConference, switchToMainCall, - logger + logger, + conferenceEnabled ); // Filter buttons that should be shown, then map them diff --git a/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/call-control-custom.utils.ts b/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/call-control-custom.utils.ts index 3180414db..a102412a3 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/call-control-custom.utils.ts +++ b/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/call-control-custom.utils.ts @@ -1,6 +1,6 @@ -import {BuddyDetails, ContactServiceQueue, ILogger} from '@webex/cc-store'; +import {BuddyDetails, ContactServiceQueue, ILogger, TaskUIControls} from '@webex/cc-store'; import {MUTE_CALL, UNMUTE_CALL} from '../../constants'; -import {ButtonConfig, ControlVisibility} from '../../task.types'; +import {ButtonConfig} from '../../task.types'; /** * Interface for list item data @@ -15,15 +15,18 @@ export interface ListItemData { */ export const createConsultButtons = ( isMuted: boolean, - controlVisibility: ControlVisibility, + controls: TaskUIControls, consultTransfer: () => void, toggleConsultMute: () => void, endConsultCall: () => void, consultConference: () => void, switchToMainCall: () => void, - logger? + logger?, + conferenceEnabled = true ): ButtonConfig[] => { try { + const consultCtrl = controls?.consult; + const mainCtrl = controls?.main; return [ { key: 'mute', @@ -31,26 +34,26 @@ export const createConsultButtons = ( onClick: toggleConsultMute, tooltip: isMuted ? UNMUTE_CALL : MUTE_CALL, className: `${isMuted ? 'call-control-button-muted' : 'call-control-button'}`, - disabled: !controlVisibility.muteUnmuteConsult.isEnabled, - isVisible: controlVisibility.muteUnmuteConsult.isVisible, + disabled: !(consultCtrl?.mute?.isEnabled ?? false), + isVisible: consultCtrl?.mute?.isVisible ?? false, }, { key: 'switchToMainCall', icon: 'call-swap-bold', - tooltip: controlVisibility.isConferenceInProgress ? 'Switch to Conference Call' : 'Switch to Call', + tooltip: 'Switch to Call', onClick: switchToMainCall, className: 'call-control-button', - disabled: !controlVisibility.switchToMainCall.isEnabled, - isVisible: controlVisibility.switchToMainCall.isVisible, + disabled: !(consultCtrl?.switch?.isEnabled ?? false), + isVisible: consultCtrl?.switch?.isVisible ?? false, }, { key: 'transfer', icon: 'next-bold', - tooltip: controlVisibility.isConferenceInProgress ? 'Transfer Conference' : 'Transfer', + tooltip: 'Transfer', onClick: consultTransfer, className: 'call-control-button', - disabled: !controlVisibility.consultTransferConsult.isEnabled, - isVisible: controlVisibility.consultTransferConsult.isVisible, + disabled: !(consultCtrl?.transfer?.isEnabled ?? false), + isVisible: consultCtrl?.transfer?.isVisible ?? false, }, { key: 'conference', @@ -58,8 +61,8 @@ export const createConsultButtons = ( tooltip: 'Merge', onClick: consultConference, className: 'call-control-button', - disabled: !controlVisibility.mergeConferenceConsult.isEnabled, - isVisible: controlVisibility.mergeConferenceConsult.isVisible, + disabled: !(consultCtrl?.mergeToConference?.isEnabled ?? false), + isVisible: conferenceEnabled && (consultCtrl?.mergeToConference?.isVisible ?? false), }, { key: 'cancel', @@ -67,7 +70,7 @@ export const createConsultButtons = ( tooltip: 'End Consult', onClick: endConsultCall, className: 'call-control-consult-button-cancel', - isVisible: controlVisibility.endConsult.isVisible, + isVisible: (consultCtrl?.endConsult?.isVisible ?? false) || (mainCtrl?.endConsult?.isVisible ?? false), }, ]; } catch (error) { @@ -595,7 +598,7 @@ export const debounce = unknown>( logger? ): ((...args: Parameters) => void) => { try { - let timeout: NodeJS.Timeout; + let timeout: ReturnType; return (...args: Parameters) => { clearTimeout(timeout); timeout = setTimeout(() => func(...args), wait); diff --git a/packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx b/packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx index 7dfb1b137..41501e7ad 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx +++ b/packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx @@ -35,6 +35,7 @@ function CallControlComponent(props: CallControlComponentProps) { const { currentTask, + isHeld, toggleHold, toggleRecording, toggleMute, @@ -57,7 +58,7 @@ function CallControlComponent(props: CallControlComponentProps) { setConsultAgentName, allowConsultToQueue, setLastTargetType, - controlVisibility, + controls, logger, secondsUntilAutoWrapup, cancelAutoWrapup, @@ -65,6 +66,7 @@ function CallControlComponent(props: CallControlComponentProps) { getEntryPoints, getQueuesFetcher, consultTransferOptions, + conferenceEnabled = true, } = props; useEffect(() => { @@ -72,7 +74,7 @@ function CallControlComponent(props: CallControlComponentProps) { }, [currentTask, logger]); const handletoggleHold = () => { - handleToggleHoldUtil(controlVisibility.isHeld, toggleHold, logger); + handleToggleHoldUtil(isHeld, toggleHold, logger); }; const handleMuteToggle = () => { @@ -128,7 +130,8 @@ function CallControlComponent(props: CallControlComponentProps) { isRecording, isMuteButtonDisabled, currentMediaType, - controlVisibility, + controls, + isHeld, handleMuteToggle, handletoggleHold, toggleRecording, @@ -136,15 +139,13 @@ function CallControlComponent(props: CallControlComponentProps) { exitConference, switchToConsult, consultTransfer, - consultConference + consultConference, + logger, + conferenceEnabled ); - const filteredButtons = filterButtonsForConsultation( - buttons, - controlVisibility.isConsultInitiatedOrAccepted, - isTelephony, - logger - ); + const isConsulting = (controls?.consult?.endConsult?.isVisible || controls?.main?.endConsult?.isVisible) ?? false; + const filteredButtons = filterButtonsForConsultation(buttons, isConsulting, isTelephony, logger); if (!currentTask) return null; @@ -156,7 +157,7 @@ function CallControlComponent(props: CallControlComponentProps) { autoPlay >
- {!controlVisibility.isConsultReceived && !controlVisibility.wrapup.isVisible && ( + {!controls?.main?.wrapup?.isVisible && (
{filteredButtons.map((button, index) => { if (!button.isVisible) return null; @@ -249,7 +250,7 @@ function CallControlComponent(props: CallControlComponentProps) { showEntryPointTab: false, } } - isConferenceInProgress={controlVisibility.isConferenceInProgress} + isConferenceInProgress={controls?.main?.exitConference?.isVisible ?? false} logger={logger} /> ) : null} @@ -283,7 +284,7 @@ function CallControlComponent(props: CallControlComponentProps) { })}
)} - {controlVisibility.wrapup.isVisible && ( + {controls?.main?.wrapup?.isVisible && (
void, handleToggleHoldFunc: () => void, toggleRecording: () => void, @@ -203,9 +200,11 @@ export const buildCallControlButtons = ( switchToConsult: () => void, onTransferConsult: () => void, handleConsultConferencePress: () => void, - logger?: ILogger + logger?: ILogger, + conferenceEnabled = true ): CallControlButton[] => { try { + const mainCtrl = controls?.main; return [ { id: 'mute', @@ -214,7 +213,7 @@ export const buildCallControlButtons = ( tooltip: isMuted ? UNMUTE_CALL : MUTE_CALL, className: `${isMuted ? 'call-control-button-muted' : 'call-control-button'}`, disabled: isMuteButtonDisabled, - isVisible: controlVisibility.muteUnmute.isVisible, + isVisible: mainCtrl?.mute?.isVisible ?? false, dataTestId: 'call-control:mute-toggle', }, { @@ -223,19 +222,18 @@ export const buildCallControlButtons = ( tooltip: 'Switch to Consult Call', className: 'call-control-button', onClick: switchToConsult, - disabled: !controlVisibility.switchToConsult.isEnabled, - isVisible: controlVisibility.switchToConsult.isVisible, + disabled: !(mainCtrl?.switch?.isEnabled ?? false), + isVisible: mainCtrl?.switch?.isVisible ?? false, dataTestId: 'call-control:switch-to-consult', }, - { id: 'hold', - icon: controlVisibility.isHeld ? 'play-bold' : 'pause-bold', + icon: isHeld ? 'play-bold' : 'pause-bold', onClick: handleToggleHoldFunc, - tooltip: controlVisibility.isHeld ? RESUME_CALL : HOLD_CALL, + tooltip: isHeld ? RESUME_CALL : HOLD_CALL, className: 'call-control-button', - disabled: !controlVisibility.holdResume.isEnabled, - isVisible: controlVisibility.holdResume.isVisible, + disabled: !(mainCtrl?.hold?.isEnabled ?? false), + isVisible: mainCtrl?.hold?.isVisible ?? false, dataTestId: 'call-control:hold-toggle', }, { @@ -243,19 +241,22 @@ export const buildCallControlButtons = ( icon: 'headset-bold', tooltip: CONSULT_AGENT, className: 'call-control-button', - disabled: !controlVisibility.consult.isEnabled, + disabled: !(mainCtrl?.consult?.isEnabled ?? false), menuType: 'Consult', - isVisible: controlVisibility.consult.isVisible, + isVisible: mainCtrl?.consult?.isVisible ?? false, dataTestId: 'call-control:consult', }, { id: 'transferConsult', icon: 'next-bold', - tooltip: controlVisibility.isConferenceInProgress ? 'Transfer Conference' : 'Transfer', + tooltip: 'Transfer', onClick: onTransferConsult || (() => {}), className: 'call-control-button', - disabled: !controlVisibility.consultTransfer.isEnabled, - isVisible: controlVisibility.consultTransfer.isVisible && !!onTransferConsult, + disabled: !(mainCtrl?.transfer?.isEnabled ?? false), + isVisible: + (mainCtrl?.transfer?.isVisible ?? false) && + ((controls?.consult?.endConsult?.isVisible || controls?.main?.endConsult?.isVisible) ?? false) && + !!onTransferConsult, }, { id: 'conference', @@ -263,17 +264,17 @@ export const buildCallControlButtons = ( tooltip: 'conference', onClick: handleConsultConferencePress || (() => {}), className: 'call-control-button', - disabled: !controlVisibility.mergeConference.isEnabled, - isVisible: controlVisibility.mergeConference.isVisible && !!handleConsultConferencePress, + disabled: !(mainCtrl?.conference?.isEnabled ?? false), + isVisible: conferenceEnabled && (mainCtrl?.conference?.isVisible ?? false) && !!handleConsultConferencePress, }, { id: 'transfer', icon: 'next-bold', tooltip: `${TRANSFER} ${currentMediaType.labelName}`, className: 'call-control-button', - disabled: !controlVisibility.transfer.isEnabled, + disabled: !(mainCtrl?.transfer?.isEnabled ?? false), menuType: 'Transfer', - isVisible: controlVisibility.transfer.isVisible, + isVisible: mainCtrl?.transfer?.isVisible ?? false, dataTestId: 'call-control:transfer', }, { @@ -282,8 +283,8 @@ export const buildCallControlButtons = ( onClick: toggleRecording, tooltip: isRecording ? PAUSE_RECORDING : RESUME_RECORDING, className: 'call-control-button', - disabled: !controlVisibility.pauseResumeRecording.isEnabled, - isVisible: controlVisibility.pauseResumeRecording.isVisible, + disabled: !(mainCtrl?.recording?.isEnabled ?? false), + isVisible: mainCtrl?.recording?.isVisible ?? false, dataTestId: 'call-control:recording-toggle', }, { @@ -292,8 +293,8 @@ export const buildCallControlButtons = ( tooltip: 'Exit Conference', className: 'call-control-button-muted', onClick: exitConference, - disabled: !controlVisibility.exitConference.isEnabled, - isVisible: controlVisibility.exitConference.isVisible, + disabled: !(mainCtrl?.exitConference?.isEnabled ?? false), + isVisible: conferenceEnabled && (mainCtrl?.exitConference?.isVisible ?? false), dataTestId: 'call-control:exit-conference', }, { @@ -302,8 +303,8 @@ export const buildCallControlButtons = ( onClick: endCall, tooltip: `${END} ${currentMediaType.labelName}`, className: 'call-control-button-cancel', - disabled: !controlVisibility.end.isEnabled, - isVisible: controlVisibility.end.isVisible, + disabled: !(mainCtrl?.end?.isEnabled ?? false), + isVisible: mainCtrl?.end?.isVisible ?? false, dataTestId: 'call-control:end-call', }, ]; @@ -320,6 +321,11 @@ export const buildCallControlButtons = ( /** * Filters buttons based on consultation state + * During consulting: + * - Hide: hold, consult, and blind transfer buttons + * - Respect SDK enabled/disabled state for consulting buttons (transferConsult, conference) + * They will be enabled when on main call, disabled when on consult call + * - Show as-is: mute, switchToConsult, recording, exitConference, end */ export const filterButtonsForConsultation = ( buttons: CallControlButton[], @@ -328,9 +334,11 @@ export const filterButtonsForConsultation = ( logger? ): CallControlButton[] => { try { - return consultInitiated && isTelephony - ? buttons.filter((button) => !['hold', 'consult'].includes(button.id)) - : buttons; + if (!consultInitiated || !isTelephony) { + return buttons; + } + + return buttons.filter((button) => !['hold', 'consult', 'transfer', 'record'].includes(button.id)); } catch (error) { logger?.error('CC-Widgets: CallControl: Error in filterButtonsForConsultation', { module: 'cc-components#call-control.utils.ts', diff --git a/packages/contact-center/cc-components/src/components/task/CallControlCAD/call-control-cad.styles.scss b/packages/contact-center/cc-components/src/components/task/CallControlCAD/call-control-cad.styles.scss index 068a103dc..06aaad318 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControlCAD/call-control-cad.styles.scss +++ b/packages/contact-center/cc-components/src/components/task/CallControlCAD/call-control-cad.styles.scss @@ -20,6 +20,25 @@ font-weight: 250; } } + +} + +.global-variables { + display: flex; + flex-flow: row wrap; + justify-content: space-between; + width: 100%; + max-height: 11.25rem; + overflow-y: auto; + margin-top: 0.75rem; + + .global-variable-item { + flex-basis: 50%; + min-width: 0; + box-sizing: border-box; + padding: 0.125rem 0; + line-height: 1.75rem; + } } /* On-hold chip styling */ diff --git a/packages/contact-center/cc-components/src/components/task/CallControlCAD/call-control-cad.tsx b/packages/contact-center/cc-components/src/components/task/CallControlCAD/call-control-cad.tsx index b2f31072a..8cacc2093 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControlCAD/call-control-cad.tsx +++ b/packages/contact-center/cc-components/src/components/task/CallControlCAD/call-control-cad.tsx @@ -5,7 +5,8 @@ import {Brandvisual, Icon, Tooltip, Button} from '@momentum-design/components/di import './call-control-cad.styles.scss'; import TaskTimer from '../TaskTimer/index'; import CallControlConsultComponent from '../CallControl/CallControlCustom/call-control-consult'; -import {MEDIA_CHANNEL as MediaChannelType, CallControlComponentProps} from '../task.types'; +import {MEDIA_CHANNEL as MediaChannelType, CallControlComponentProps, CallAssociatedDataMap} from '../task.types'; +import {getAgentViewableGlobalVariables} from '../Task/task.utils'; import {getMediaTypeInfo} from '../../../utils'; import { @@ -24,6 +25,7 @@ const CallControlCADComponent: React.FC = (props) => const { currentTask, isRecording, + isHeld, holdTime, consultAgentName, consultTimerLabel, @@ -37,11 +39,12 @@ const CallControlCADComponent: React.FC = (props) => startTimestamp, stateTimerLabel, stateTimerTimestamp, - controlVisibility, + controls, logger, isMuted, toggleMute, conferenceParticipants, + conferenceEnabled = true, } = props; const formatTime = (time: number): string => { @@ -61,14 +64,15 @@ const CallControlCADComponent: React.FC = (props) => const participantsCount = conferenceParticipants?.length || 1; const participantsLabel = participantsCount === 1 ? 'Participant' : 'Participants'; - //@ts-expect-error To be fixed in SDK - https://jira-eng-sjc12.cisco.com/jira/browse/CAI-6762 const customerName = currentTask?.data?.interaction?.callAssociatedDetails?.customerName; - //@ts-expect-error To be fixed in SDK - https://jira-eng-sjc12.cisco.com/jira/browse/CAI-6762 const ani = currentTask?.data?.interaction?.callAssociatedDetails?.ani; - //@ts-expect-error To be fixed in SDK - https://jira-eng-sjc12.cisco.com/jira/browse/CAI-6762 const dn = currentTask?.data?.interaction?.callAssociatedDetails?.dn; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const callAssociatedData = (currentTask?.data?.interaction as any)?.callAssociatedData as CallAssociatedDataMap | undefined; + const globalVariables = getAgentViewableGlobalVariables(callAssociatedData); + // Create unique IDs for tooltips const customerNameTriggerId = `customer-name-trigger-${currentTask.data.interaction.interactionId}`; const customerNameTooltipId = `customer-name-tooltip-${currentTask.data.interaction.interactionId}`; @@ -113,7 +117,7 @@ const CallControlCADComponent: React.FC = (props) => }; const renderPhoneNumber = () => { - const phoneText = isSocial ? customerName || NO_CUSTOMER_NAME : dn || NO_PHONE_NUMBER; + const phoneText = isSocial ? customerName || NO_CUSTOMER_NAME : ani || NO_PHONE_NUMBER; const labelText = isSocial ? CUSTOMER_NAME : PHONE_NUMBER; const textComponent = ( @@ -178,7 +182,7 @@ const CallControlCADComponent: React.FC = (props) => )} - {controlVisibility.isConferenceInProgress && !controlVisibility.wrapup.isVisible && ( + {controls?.main?.exitConference?.isVisible && !controls?.main?.wrapup?.isVisible && ( <>
@@ -228,25 +232,22 @@ const CallControlCADComponent: React.FC = (props) => )}
- {!controlVisibility.wrapup.isVisible && - controlVisibility.isHeld && - !controlVisibility.isConsultReceived && - !controlVisibility.consultCallHeld && ( - <> - -
- - - {ON_HOLD} {formatTime(holdTime)} - -
- - )} + {!controls?.main?.wrapup?.isVisible && isHeld && !controls?.main?.endConsult?.isVisible && ( + <> + +
+ + + {ON_HOLD} {formatTime(holdTime)} + +
+ + )}
- {!controlVisibility.wrapup.isVisible && controlVisibility.recordingIndicator.isVisible && ( + {!controls?.main?.wrapup?.isVisible && isTelephony && (
@@ -255,18 +256,30 @@ const CallControlCADComponent: React.FC = (props) =>
{QUEUE}{' '} - - { - //@ts-expect-error To be fixed in SDK - https://jira-eng-sjc12.cisco.com/jira/browse/CAI-6762 - - currentTask?.data?.interaction?.callAssociatedDetails?.virtualTeamName || NO_TEAM_NAME - } - + {currentTask?.data?.interaction?.callAssociatedDetails?.virtualTeamName || NO_TEAM_NAME} {renderPhoneNumber()}
+ {globalVariables.length > 0 && ( +
+ {globalVariables.map((variable) => ( +
+ + {variable.displayName || variable.name} + + + {variable.value || ''} + +
+ ))} +
+ )} - {controlVisibility.isConsultInitiatedOrAccepted && !controlVisibility.wrapup.isVisible && ( + {(controls?.consult?.endConsult?.isVisible || controls?.main?.endConsult?.isVisible) && !controls?.main?.wrapup?.isVisible && (
= (props) => switchToMainCall={switchToMainCall} logger={logger} isMuted={isMuted} - controlVisibility={controlVisibility} + controls={controls} toggleConsultMute={toggleMute} + conferenceEnabled={conferenceEnabled} />
)} diff --git a/packages/contact-center/cc-components/src/components/task/IncomingTask/incoming-task.tsx b/packages/contact-center/cc-components/src/components/task/IncomingTask/incoming-task.tsx index 3b46d68c9..ec7025ab4 100644 --- a/packages/contact-center/cc-components/src/components/task/IncomingTask/incoming-task.tsx +++ b/packages/contact-center/cc-components/src/components/task/IncomingTask/incoming-task.tsx @@ -5,13 +5,13 @@ import {withMetrics} from '@webex/cc-ui-logging'; import {extractIncomingTaskData} from './incoming-task.utils'; const IncomingTaskComponent: React.FunctionComponent = (props) => { - const {incomingTask, isBrowser, accept, reject, logger, isDeclineButtonEnabled} = props; + const {incomingTask, accept, reject, logger, acceptControl, declineControl, isDeclineButtonEnabled} = props; if (!incomingTask) { return <>; // hidden component } // Extract all task data using the utility function - const taskData = extractIncomingTaskData(incomingTask, isBrowser, logger, isDeclineButtonEnabled); + const taskData = extractIncomingTaskData(incomingTask, logger, acceptControl, declineControl, isDeclineButtonEnabled); return ( { try { + const accept = acceptControl ?? incomingTask?.uiControls?.main?.accept ?? {isVisible: false, isEnabled: false}; + const sdkDecline = declineControl ?? incomingTask?.uiControls?.main?.decline ?? {isVisible: false, isEnabled: false}; + const decline = { + ...sdkDecline, + isEnabled: sdkDecline.isEnabled || !!isDeclineButtonEnabled, + }; + // Extract basic data from task - //@ts-expect-error To be fixed in SDK - https://jira-eng-sjc12.cisco.com/jira/browse/CAI-6762 const callAssociationDetails = incomingTask?.data?.interaction?.callAssociatedDetails; const ani = callAssociationDetails?.ani; const customerName = callAssociationDetails?.customerName; @@ -47,23 +54,21 @@ export const extractIncomingTaskData = ( const isSocial = mediaType === MEDIA_CHANNEL.SOCIAL; // Compute button text based on conditions - const acceptText = !incomingTask.data.wrapUpRequired - ? isTelephony && !isBrowser + // Extension mode (EPDN): accept button is visible but disabled → show "Ringing..." + // WebRTC mode (Desktop): accept button is visible and enabled → show "Accept" + const acceptText = accept.isVisible + ? isTelephony && !accept.isEnabled ? 'Ringing...' : 'Accept' : undefined; - const declineText = !incomingTask.data.wrapUpRequired && isTelephony && isBrowser ? 'Decline' : undefined; + const declineText = decline.isVisible ? 'Decline' : undefined; // Compute title based on media type const title = isSocial ? customerName : ani; - // Compute disable state for accept button when auto-answering - const isAutoAnswering = incomingTask.data.isAutoAnswering || false; - // Compute disable state for accept button - const disableAccept = (isTelephony && !isBrowser) || isAutoAnswering; - - const disableDecline = (isTelephony && !isBrowser) || (isAutoAnswering && !isDeclineButtonEnabled); + const disableAccept = !accept.isEnabled; + const disableDecline = !decline.isEnabled; return { ani, diff --git a/packages/contact-center/cc-components/src/components/task/Task/task.utils.ts b/packages/contact-center/cc-components/src/components/task/Task/task.utils.ts index 3f4b9b86b..6bec610c5 100644 --- a/packages/contact-center/cc-components/src/components/task/Task/task.utils.ts +++ b/packages/contact-center/cc-components/src/components/task/Task/task.utils.ts @@ -1,6 +1,38 @@ -import type {MEDIA_CHANNEL as MediaChannelType, TaskComponentData} from '../task.types'; +import type {MEDIA_CHANNEL as MediaChannelType, TaskComponentData, CADVariable, CallAssociatedDataMap} from '../task.types'; import {getMediaTypeInfo} from '../../../utils'; +/** System CAD variable keys that are already displayed elsewhere in the UI. */ +export const SYSTEM_CAD_KEYS = new Set([ + 'ani', + 'dn', + 'customerName', + 'virtualTeamName', + 'ronaTimeout', + 'FC-DESKTOP-VIEW', +]); + +/** + * Returns agent-viewable global variables from a callAssociatedData map, + * excluding system variables that are already rendered elsewhere. + */ +export const getAgentViewableGlobalVariables = ( + callAssociatedData: CallAssociatedDataMap | undefined +): CADVariable[] => { + if (!callAssociatedData || typeof callAssociatedData !== 'object') { + return []; + } + + return Object.entries(callAssociatedData) + .filter(([key, cadVar]) => { + if (!cadVar || !cadVar.name) return false; + if (cadVar.agentViewable === false) return false; + if (!cadVar.global) return false; + if (SYSTEM_CAD_KEYS.has(key)) return false; + return true; + }) + .map(([, cadVar]) => cadVar); +}; + /** * Capitalizes the first word of a string * @param str - The string to capitalize diff --git a/packages/contact-center/cc-components/src/components/task/TaskList/task-list.tsx b/packages/contact-center/cc-components/src/components/task/TaskList/task-list.tsx index 73da269c6..cbe0faeb4 100644 --- a/packages/contact-center/cc-components/src/components/task/TaskList/task-list.tsx +++ b/packages/contact-center/cc-components/src/components/task/TaskList/task-list.tsx @@ -12,7 +12,7 @@ import './styles.scss'; import {withMetrics} from '@webex/cc-ui-logging'; const TaskListComponent: React.FunctionComponent = (props) => { - const {currentTask, taskList, acceptTask, declineTask, isBrowser, onTaskSelect, logger, agentId} = props; + const {currentTask, taskList, acceptTask, declineTask, onTaskSelect, logger, agentId, isDeclineButtonEnabled} = props; // Early return for empty task list if (isTaskListEmpty(taskList)) { @@ -25,7 +25,7 @@ const TaskListComponent: React.FunctionComponent = (prop
    {tasks.map((task, index) => { // Extract all task data using the utility function - const taskData = extractTaskListItemData(task, isBrowser, agentId, logger); + const taskData = extractTaskListItemData(task, agentId, logger, isDeclineButtonEnabled); // Log task rendering logger.info('CC-Widgets: TaskList: rendering task list', { diff --git a/packages/contact-center/cc-components/src/components/task/TaskList/task-list.utils.ts b/packages/contact-center/cc-components/src/components/task/TaskList/task-list.utils.ts index aa1591ffc..bc797b5fc 100644 --- a/packages/contact-center/cc-components/src/components/task/TaskList/task-list.utils.ts +++ b/packages/contact-center/cc-components/src/components/task/TaskList/task-list.utils.ts @@ -1,5 +1,5 @@ import {MEDIA_CHANNEL, TaskListItemData} from '../task.types'; -import store, {isIncomingTask, ILogger, ITask} from '@webex/cc-store'; +import {isIncomingTask, ILogger, ITask} from '@webex/cc-store'; /** * Extracts and processes data from a task for rendering in the task list * @param task - The task object @@ -8,13 +8,19 @@ import store, {isIncomingTask, ILogger, ITask} from '@webex/cc-store'; */ export const extractTaskListItemData = ( task: ITask, - isBrowser: boolean, agentId: string, - logger?: ILogger + logger?: ILogger, + isDeclineButtonEnabled?: boolean ): TaskListItemData => { try { + const accept = task.uiControls?.main?.accept ?? {isVisible: false, isEnabled: false}; + const sdkDecline = task.uiControls?.main?.decline ?? {isVisible: false, isEnabled: false}; + const decline = { + ...sdkDecline, + isEnabled: sdkDecline.isEnabled || !!isDeclineButtonEnabled, + }; + // Extract basic data from task - //@ts-expect-error To be fixed in SDK - https://jira-eng-sjc12.cisco.com/jira/browse/CAI-6762 const callAssociationDetails = task?.data?.interaction?.callAssociatedDetails; const ani = callAssociationDetails?.ani; const customerName = callAssociationDetails?.customerName; @@ -34,20 +40,18 @@ export const extractTaskListItemData = ( const isSocial = mediaType === MEDIA_CHANNEL.SOCIAL; // Compute button text based on conditions - const acceptText = isTaskIncoming ? (isTelephony && !isBrowser ? 'Ringing...' : 'Accept') : undefined; + // Extension mode (EPDN): accept button is visible but disabled → show "Ringing..." + // WebRTC mode (Desktop): accept button is visible and enabled → show "Accept" + const acceptText = + accept.isVisible && isTaskIncoming ? (isTelephony && !accept.isEnabled ? 'Ringing...' : 'Accept') : undefined; - const declineText = isTaskIncoming && isTelephony && isBrowser ? 'Decline' : undefined; + const declineText = decline.isVisible && isTaskIncoming ? 'Decline' : undefined; // Compute title based on media type const title = isSocial ? customerName : ani; - const isAutoAnswering = task.data.isAutoAnswering || false; - - // Compute disable state for accept button - const disableAccept = (isTaskIncoming && isTelephony && !isBrowser) || isAutoAnswering; - - const disableDecline = - (isTaskIncoming && isTelephony && !isBrowser) || (isAutoAnswering && !store.isDeclineButtonEnabled); + const disableAccept = !accept.isEnabled; + const disableDecline = !decline.isEnabled; const ronaTimeout = isTaskIncoming ? rawRonaTimeout : null; @@ -210,7 +214,7 @@ export const createTaskSelectHandler = ( return () => { try { // Logging moved to helper.ts - const taskData = extractTaskListItemData(task, true, agentId, logger); // Use browser=true for selection logic + const taskData = extractTaskListItemData(task, agentId, logger); if (isTaskSelectable(task, currentTask, taskData, logger)) { onTaskSelect(task); diff --git a/packages/contact-center/cc-components/src/components/task/task.types.ts b/packages/contact-center/cc-components/src/components/task/task.types.ts index aac63d7ca..c8d3e5cd4 100644 --- a/packages/contact-center/cc-components/src/components/task/task.types.ts +++ b/packages/contact-center/cc-components/src/components/task/task.types.ts @@ -12,10 +12,34 @@ import { Participant, AddressBookEntrySearchParams, AddressBookEntriesResponse, + TaskUIControls, } from '@webex/cc-store'; type Enum> = T[keyof T]; +/** + * Represents a single Call Associated Data (CAD) variable on an interaction. + * Global variables have `global: true` and are set by flow control. + */ +export interface CADVariable { + name: string; + displayName: string; + value: string; + type: string; + agentEditable: boolean; + agentViewable: boolean; + global: boolean; + isSecure: boolean; + secureKeyId: string; + secureKeyVersion: number; +} + +/** + * Record of CAD variables keyed by variable name. + * This is the shape of `callAssociatedData` on the interaction at runtime. + */ +export type CallAssociatedDataMap = Record; + /** * Target types for consult/transfer operations */ @@ -104,11 +128,6 @@ export interface TaskProps { * Function to handle task selection */ onTaskSelect: (task: ITask) => void; - /** - * Flag to determine if the user is logged in with a browser option - */ - isBrowser: boolean; - /** * Flag to determine if the task is answered */ @@ -119,11 +138,6 @@ export interface TaskProps { */ isEnded: boolean; - /** - * Selected login option - */ - deviceType: string; - /** * List of tasks */ @@ -138,20 +152,22 @@ export interface TaskProps { * Agent ID of the logged-in user */ agentId: string; - /** - * Flag to enable decline button on incoming task component - */ - isDeclineButtonEnabled?: boolean; } -export type IncomingTaskComponentProps = Pick & - Partial>; +export type IncomingTaskComponentProps = Pick & + Partial> & { + acceptControl?: {isVisible: boolean; isEnabled: boolean}; + declineControl?: {isVisible: boolean; isEnabled: boolean}; + isDeclineButtonEnabled?: boolean; + }; export type TaskListComponentProps = Pick< TaskProps, - 'isBrowser' | 'acceptTask' | 'declineTask' | 'onTaskSelect' | 'logger' | 'agentId' + 'acceptTask' | 'declineTask' | 'onTaskSelect' | 'logger' | 'agentId' > & - Partial>; + Partial> & { + isDeclineButtonEnabled?: boolean; + }; /** * Interface representing the properties for control actions on a task. @@ -245,11 +261,6 @@ export interface ControlProps { */ wrapupCall: (wrapupReason: string, wrapupId: string) => void; - /** - * Selected login option - */ - deviceType: string; - /** * Flag to determine if the task is held */ @@ -383,11 +394,6 @@ export interface ControlProps { */ holdTime: number; - /** - * Feature flags for the task. - */ - featureFlags: {[key: string]: boolean}; - /** * Custom CSS ClassName for CallControlCAD component. */ @@ -438,7 +444,7 @@ export interface ControlProps { */ setLastTargetType: (targetType: TargetType) => void; - controlVisibility: ControlVisibility; + controls: TaskUIControls; secondsUntilAutoWrapup?: number; @@ -475,6 +481,7 @@ export interface ControlProps { export type CallControlComponentProps = Pick< ControlProps, | 'currentTask' + | 'isHeld' | 'wrapupCodes' | 'toggleHold' | 'toggleRecording' @@ -509,7 +516,7 @@ export type CallControlComponentProps = Pick< | 'allowConsultToQueue' | 'lastTargetType' | 'setLastTargetType' - | 'controlVisibility' + | 'controls' | 'logger' | 'secondsUntilAutoWrapup' | 'cancelAutoWrapup' @@ -518,6 +525,7 @@ export type CallControlComponentProps = Pick< | 'getEntryPoints' | 'getQueuesFetcher' | 'consultTransferOptions' + | 'conferenceEnabled' >; export type OutdialAniEntry = { @@ -647,8 +655,9 @@ export interface CallControlConsultComponentsProps { switchToMainCall: () => void; logger: ILogger; isMuted: boolean; - controlVisibility: ControlVisibility; + controls: TaskUIControls; toggleConsultMute: () => void; + conferenceEnabled: boolean; } /** diff --git a/packages/contact-center/cc-components/src/wc.ts b/packages/contact-center/cc-components/src/wc.ts index 1553aab77..3f3d81b2a 100644 --- a/packages/contact-center/cc-components/src/wc.ts +++ b/packages/contact-center/cc-components/src/wc.ts @@ -79,7 +79,6 @@ if (!customElements.get('component-cc-call-control')) { const WebIncomingTask = r2wc(IncomingTaskComponent, { props: { incomingTask: 'json', - isBrowser: 'boolean', accept: 'function', reject: 'function', }, @@ -92,7 +91,6 @@ const WebTaskList = r2wc(TaskListComponent, { props: { currentTask: 'json', taskList: 'json', - isBrowser: 'boolean', acceptTask: 'function', declineTask: 'function', logger: 'function', diff --git a/packages/contact-center/cc-widgets/src/wc.ts b/packages/contact-center/cc-widgets/src/wc.ts index 5fd608367..e1fde50ab 100644 --- a/packages/contact-center/cc-widgets/src/wc.ts +++ b/packages/contact-center/cc-widgets/src/wc.ts @@ -40,6 +40,7 @@ const WebCallControl = r2wc(CallControl, { onEnd: 'function', onWrapUp: 'function', onRecordingToggle: 'function', + conferenceEnabled: 'boolean', }, }); @@ -49,6 +50,7 @@ const WebCallControlCAD = r2wc(CallControlCAD, { onEnd: 'function', onWrapUp: 'function', onRecordingToggle: 'function', + conferenceEnabled: 'boolean', }, }); diff --git a/packages/contact-center/store/package.json b/packages/contact-center/store/package.json index aacf58607..4c6e5d789 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.11.0-next.20", + "@webex/contact-center": "3.12.0-task-refactor.2", "mobx": "6.13.5", "typescript": "5.6.3" }, diff --git a/packages/contact-center/store/src/constants.ts b/packages/contact-center/store/src/constants.ts index 09ac07ed6..91b05d475 100644 --- a/packages/contact-center/store/src/constants.ts +++ b/packages/contact-center/store/src/constants.ts @@ -1,19 +1,3 @@ -// Task States -export const TASK_STATE_CONSULT = 'consult'; -export const TASK_STATE_CONSULTING = 'consulting'; -export const TASK_STATE_CONSULT_COMPLETED = 'consultCompleted'; - -// Interaction States -export const INTERACTION_STATE_WRAPUP = 'wrapUp'; -export const INTERACTION_STATE_POST_CALL = 'post_call'; -export const INTERACTION_STATE_CONNECTED = 'connected'; -export const INTERACTION_STATE_CONFERENCE = 'conference'; - -// Consult States (participant.consultState) -export const CONSULT_STATE_INITIATED = 'consultInitiated'; -export const CONSULT_STATE_COMPLETED = 'consultCompleted'; -export const CONSULT_STATE_CONFERENCING = 'conferencing'; - // Relationship Types export const RELATIONSHIP_TYPE_CONSULT = 'consult'; diff --git a/packages/contact-center/store/src/store.ts b/packages/contact-center/store/src/store.ts index 5ade1368c..e87e1a2dc 100644 --- a/packages/contact-center/store/src/store.ts +++ b/packages/contact-center/store/src/store.ts @@ -38,6 +38,7 @@ class Store implements IStore { isQueueConsultInProgress = false; isDeclineButtonEnabled = false; currentConsultQueueId: string = ''; + lastConsultDestination: {to: string; destinationType: string} | null = null; consultStartTimeStamp = undefined; lastStateChangeTimestamp?: number; lastIdleCodeChangeTimestamp?: number; diff --git a/packages/contact-center/store/src/store.types.ts b/packages/contact-center/store/src/store.types.ts index 0d3d6a2ae..2acc72151 100644 --- a/packages/contact-center/store/src/store.types.ts +++ b/packages/contact-center/store/src/store.types.ts @@ -17,6 +17,12 @@ import { ContactServiceQueuesResponse, ContactServiceQueueSearchParams, AddressBook, + TASK_EVENTS, + TaskUIControls, + TaskUIControlState, + InteractionUIControls, + TaskUILeg, + getDefaultUIControls, } from '@webex/contact-center'; import { OutdialAniEntriesResponse, @@ -119,6 +125,7 @@ interface IStore { isQueueConsultInProgress: boolean; isDeclineButtonEnabled: boolean; currentConsultQueueId: string; + lastConsultDestination: {to: string; destinationType: DestinationType} | null; consultStartTimeStamp?: number; callControlAudio: MediaStream | null; isEndConsultEnabled: boolean; @@ -167,47 +174,7 @@ interface IWrapupCode { name: string; } -enum TASK_EVENTS { - TASK_INCOMING = 'task:incoming', - TASK_ASSIGNED = 'task:assigned', - TASK_MEDIA = 'task:media', - TASK_HOLD = 'task:hold', - TASK_UNHOLD = 'task:unhold', - TASK_CONSULT = 'task:consult', - TASK_CONSULT_END = 'task:consultEnd', - TASK_CONSULT_ACCEPTED = 'task:consultAccepted', - TASK_PAUSE = 'task:pause', - TASK_RESUME = 'task:resume', - TASK_END = 'task:end', - TASK_WRAPUP = 'task:wrapup', - TASK_REJECT = 'task:rejected', - TASK_HYDRATE = 'task:hydrate', - TASK_CONSULTING = 'task:consulting', - TASK_CONSULT_QUEUE_CANCELLED = 'task:consultQueueCancelled', - AGENT_CONTACT_ASSIGNED = 'AgentContactAssigned', - CONTACT_RECORDING_PAUSED = 'ContactRecordingPaused', - CONTACT_RECORDING_RESUMED = 'ContactRecordingResumed', - AGENT_WRAPPEDUP = 'AgentWrappedUp', - AGENT_OFFER_CONTACT = 'AgentOfferContact', - AGENT_CONSULT_CREATED = 'AgentConsultCreated', - TASK_RECORDING_PAUSED = 'task:recordingPaused', - TASK_RECORDING_RESUMED = 'task:recordingResumed', - TASK_OFFER_CONSULT = 'task:offerConsult', - TASK_AUTO_ANSWERED = 'task:autoAnswered', - TASK_CONFERENCE_ESTABLISHING = 'task:conferenceEstablishing', - TASK_CONFERENCE_STARTED = 'task:conferenceStarted', - TASK_CONFERENCE_FAILED = 'task:conferenceFailed', - TASK_CONFERENCE_ENDED = 'task:conferenceEnded', - TASK_PARTICIPANT_JOINED = 'task:participantJoined', - TASK_PARTICIPANT_LEFT = 'task:participantLeft', - TASK_CONFERENCE_TRANSFERRED = 'task:conferenceTransferred', - TASK_CONFERENCE_TRANSFER_FAILED = 'task:conferenceTransferFailed', - TASK_CONFERENCE_END_FAILED = 'task:conferenceEndFailed', - TASK_PARTICIPANT_LEFT_FAILED = 'task:participantLeftFailed', - TASK_MERGED = 'task:merged', - TASK_POST_CALL_ACTIVITY = 'task:postCallActivity', - TASK_OUTDIAL_FAILED = 'task:outdialFailed', -} // TODO: remove this once cc sdk exports this enum +// TASK_EVENTS is now imported from @webex/contact-center SDK // Events that are received on the contact center SDK // TODO: Export & Import these constants from SDK @@ -246,6 +213,7 @@ type AgentLoginProfile = { social: number; telephony: number; }; + agentProfileID?: string; }; // Generic pagination params for list-fetching APIs @@ -319,6 +287,10 @@ export type { PaginatedListParams, FetchPaginatedList, TransformPaginatedData, + TaskUIControls, + TaskUIControlState, + InteractionUIControls, + TaskUILeg, }; export { @@ -335,18 +307,10 @@ export { AGENT_STATE_AVAILABLE, LoginOptions, ERROR_TRIGGERING_IDLE_CODES, + getDefaultUIControls, }; -export enum ConsultStatus { - NO_CONSULTATION_IN_PROGRESS = 'No consultation in progress', - BEING_CONSULTED = 'beingConsulted', - CONSULT_INITIATED = 'consultInitiated', - BEING_CONSULTED_ACCEPTED = 'beingConsultedAccepted', - CONSULT_ACCEPTED = 'consultAccepted', - CONNECTED = 'connected', - CONFERENCE = 'conference', - CONSULT_COMPLETED = 'consultCompleted', -} +// ConsultStatus enum removed — use task.data.consultStatus from SDK instead export type Participant = { id: string; diff --git a/packages/contact-center/store/src/storeEventsWrapper.ts b/packages/contact-center/store/src/storeEventsWrapper.ts index 87aeb93f4..9eb586ee5 100644 --- a/packages/contact-center/store/src/storeEventsWrapper.ts +++ b/packages/contact-center/store/src/storeEventsWrapper.ts @@ -152,6 +152,10 @@ class StoreWrapper implements IStoreWrapper { return this.store.currentConsultQueueId; } + get lastConsultDestination() { + return this.store.lastConsultDestination; + } + get isEndConsultEnabled() { return this.store.isEndConsultEnabled; } @@ -275,6 +279,9 @@ class StoreWrapper implements IStoreWrapper { } this.setCurrentTask(null); this.setState({reset: true}); + // Ensure agent state is set to Available (auxCodeId '0') when no tasks remain + // The backend should send AGENT_STATE_CHANGE, but in test environments it may not + this.setCurrentState('0'); } else if (this.currentTask && this.store.taskList[this.currentTask.data.interactionId]) { this.setCurrentTask(this.store.taskList[this.currentTask?.data?.interactionId]); } else if (taskListKeys.length > 0) { @@ -316,6 +323,12 @@ class StoreWrapper implements IStoreWrapper { }); }; + setLastConsultDestination = (destination: {to: string; destinationType: string} | null): void => { + runInAction(() => { + this.store.lastConsultDestination = destination; + }); + }; + setState = (state: ICustomState | IdleCode): void => { if ('reset' in state) { runInAction(() => { @@ -382,6 +395,7 @@ class StoreWrapper implements IStoreWrapper { orgId: profile.orgId || undefined, roles: profile.roles || undefined, deviceType: profile.deviceType || undefined, + agentProfileID: profile.agentProfileID || undefined, }; }); }; @@ -423,15 +437,19 @@ class StoreWrapper implements IStoreWrapper { taskToRemove.off(TASK_EVENTS.TASK_END, this.handleTaskEnd); taskToRemove.off(TASK_EVENTS.TASK_REJECT, (reason) => this.handleTaskReject(taskToRemove, reason)); taskToRemove.off(TASK_EVENTS.TASK_OUTDIAL_FAILED, (reason) => this.handleOutdialFailed(reason)); - taskToRemove.off(TASK_EVENTS.AGENT_WRAPPEDUP, this.refreshTaskList); + taskToRemove.off(TASK_EVENTS.TASK_UI_CONTROLS_UPDATED, this.handleUIControlsUpdated); + taskToRemove.off(TASK_EVENTS.TASK_WRAPPEDUP, this.refreshTaskList); + taskToRemove.off(TASK_EVENTS.TASK_CONSULT_CREATED, this.handleConsultCreated); + taskToRemove.off(TASK_EVENTS.TASK_OFFER_CONTACT, this.refreshTaskList); + taskToRemove.off(TASK_EVENTS.TASK_CONSULT_END, this.handleConsultEnd); + taskToRemove.off(TASK_EVENTS.TASK_RECORDING_PAUSED, this.refreshTaskList); + taskToRemove.off(TASK_EVENTS.TASK_RECORDING_RESUMED, this.refreshTaskList); taskToRemove.off(TASK_EVENTS.TASK_CONSULTING, this.handleConsulting); taskToRemove.off(TASK_EVENTS.TASK_OFFER_CONSULT, this.handleConsultOffer); taskToRemove.off(TASK_EVENTS.TASK_AUTO_ANSWERED, this.handleAutoAnswer); - taskToRemove.off(TASK_EVENTS.TASK_CONSULT_END, this.refreshTaskList); taskToRemove.off(TASK_EVENTS.TASK_CONSULT_ACCEPTED, this.handleConsultAccepted); - taskToRemove.off(TASK_EVENTS.AGENT_CONSULT_CREATED, this.handleConsultCreated); taskToRemove.off(TASK_EVENTS.TASK_CONSULT_QUEUE_CANCELLED, this.handleConsultQueueCancelled); - taskToRemove.off(TASK_EVENTS.AGENT_OFFER_CONTACT, this.refreshTaskList); + taskToRemove.off(TASK_EVENTS.TASK_SWITCH_CALL, this.handleSwitchCall); taskToRemove.off(TASK_EVENTS.TASK_HOLD, this.refreshTaskList); taskToRemove.off(TASK_EVENTS.TASK_RESUME, this.refreshTaskList); taskToRemove.off(TASK_EVENTS.TASK_CONFERENCE_ENDED, this.handleConferenceEnded); @@ -442,7 +460,7 @@ class StoreWrapper implements IStoreWrapper { taskToRemove.off(TASK_EVENTS.TASK_PARTICIPANT_LEFT, this.handleConferenceEnded); taskToRemove.off(TASK_EVENTS.TASK_PARTICIPANT_LEFT_FAILED, this.refreshTaskList); taskToRemove.off(TASK_EVENTS.TASK_CONFERENCE_STARTED, this.handleConferenceStarted); - taskToRemove.off(TASK_EVENTS.TASK_CONFERENCE_TRANSFERRED, this.handleConferenceEnded); + taskToRemove.off(TASK_EVENTS.TASK_CONFERENCE_TRANSFERRED, this.refreshTaskList); taskToRemove.off(TASK_EVENTS.TASK_CONFERENCE_TRANSFER_FAILED, this.refreshTaskList); taskToRemove.off(TASK_EVENTS.TASK_POST_CALL_ACTIVITY, this.refreshTaskList); if (this.deviceType === DEVICE_TYPE_BROWSER) { @@ -510,6 +528,7 @@ class StoreWrapper implements IStoreWrapper { handleConsultEnd = () => { this.setIsQueueConsultInProgress(false); this.setCurrentConsultQueueId(null); + this.setLastConsultDestination(null); this.refreshTaskList(); this.setConsultStartTimeStamp(null); }; @@ -541,6 +560,7 @@ class StoreWrapper implements IStoreWrapper { handleConsultQueueCancelled = () => { this.setIsQueueConsultInProgress(false); this.setCurrentConsultQueueId(null); + this.setLastConsultDestination(null); this.setConsultStartTimeStamp(null); this.refreshTaskList(); }; @@ -549,6 +569,7 @@ class StoreWrapper implements IStoreWrapper { runInAction(() => { this.setIsQueueConsultInProgress(false); this.setCurrentConsultQueueId(null); + this.setLastConsultDestination(null); this.setConsultStartTimeStamp(null); }); this.refreshTaskList(); @@ -562,45 +583,56 @@ class StoreWrapper implements IStoreWrapper { * Register all task event listeners * @param task - The task to register event listeners for */ + handleUIControlsUpdated = () => { + this.refreshTaskList(); + }; + + handleSwitchCall = () => { + this.refreshTaskList(); + }; + private registerTaskEventListeners = (task: ITask): void => { - // Attach event listeners to the task task.on(TASK_EVENTS.TASK_END, this.handleTaskEnd); - - // When we receive TASK_ASSIGNED the task was accepted by the agent and we need wrap up task.on(TASK_EVENTS.TASK_ASSIGNED, this.handleTaskAssigned); - task.on(TASK_EVENTS.AGENT_OFFER_CONTACT, this.refreshTaskList); - task.on(TASK_EVENTS.AGENT_CONSULT_CREATED, this.handleConsultCreated); - task.on(TASK_EVENTS.TASK_CONSULT_QUEUE_CANCELLED, this.handleConsultQueueCancelled); - - // When we receive TASK_REJECT sdk changes the agent status - // When we receive TASK_REJECT that means the task was not accepted by the agent and we wont need wrap up task.on(TASK_EVENTS.TASK_REJECT, (reason) => this.handleTaskReject(task, reason)); - - // When we receive TASK_OUTDIAL_FAILED the outdial call failed task.on(TASK_EVENTS.TASK_OUTDIAL_FAILED, (reason) => this.handleOutdialFailed(reason)); - task.on(TASK_EVENTS.AGENT_WRAPPEDUP, this.refreshTaskList); + // SDK-computed UI control updates + task.on(TASK_EVENTS.TASK_UI_CONTROLS_UPDATED, this.handleUIControlsUpdated); + + // Renamed events (SDK names) + task.on(TASK_EVENTS.TASK_WRAPPEDUP, this.refreshTaskList); + task.on(TASK_EVENTS.TASK_CONSULT_CREATED, this.handleConsultCreated); + task.on(TASK_EVENTS.TASK_OFFER_CONTACT, this.refreshTaskList); + + // Fix: wire handleConsultEnd (was dead code — previously wired to refreshTaskList) + task.on(TASK_EVENTS.TASK_CONSULT_END, this.handleConsultEnd); + + // Fix: correct event names + task.on(TASK_EVENTS.TASK_RECORDING_PAUSED, this.refreshTaskList); + task.on(TASK_EVENTS.TASK_RECORDING_RESUMED, this.refreshTaskList); + task.on(TASK_EVENTS.TASK_AUTO_ANSWERED, this.handleAutoAnswer); task.on(TASK_EVENTS.TASK_CONSULTING, this.handleConsulting); task.on(TASK_EVENTS.TASK_CONSULT_ACCEPTED, this.handleConsultAccepted); + task.on(TASK_EVENTS.TASK_CONSULT_QUEUE_CANCELLED, this.handleConsultQueueCancelled); + task.on(TASK_EVENTS.TASK_PARTICIPANT_JOINED, this.handleConferenceStarted); + task.on(TASK_EVENTS.TASK_CONFERENCE_STARTED, this.handleConferenceStarted); + task.on(TASK_EVENTS.TASK_CONFERENCE_ENDED, this.handleConferenceEnded); + task.on(TASK_EVENTS.TASK_PARTICIPANT_LEFT, this.handleConferenceEnded); task.on(TASK_EVENTS.TASK_OFFER_CONSULT, this.handleConsultOffer); - task.on(TASK_EVENTS.TASK_AUTO_ANSWERED, this.handleAutoAnswer); - task.on(TASK_EVENTS.TASK_CONSULT_END, this.refreshTaskList); + + task.on(TASK_EVENTS.TASK_SWITCH_CALL, this.handleSwitchCall); task.on(TASK_EVENTS.TASK_HOLD, this.refreshTaskList); task.on(TASK_EVENTS.TASK_RESUME, this.refreshTaskList); - task.on(TASK_EVENTS.TASK_CONFERENCE_ENDED, this.handleConferenceEnded); - task.on(TASK_EVENTS.TASK_CONFERENCE_END_FAILED, this.refreshTaskList); + task.on(TASK_EVENTS.TASK_POST_CALL_ACTIVITY, this.refreshTaskList); task.on(TASK_EVENTS.TASK_CONFERENCE_ESTABLISHING, this.refreshTaskList); task.on(TASK_EVENTS.TASK_CONFERENCE_FAILED, this.refreshTaskList); - task.on(TASK_EVENTS.TASK_PARTICIPANT_JOINED, this.handleConferenceStarted); - task.on(TASK_EVENTS.TASK_PARTICIPANT_LEFT, this.handleConferenceEnded); + task.on(TASK_EVENTS.TASK_CONFERENCE_END_FAILED, this.refreshTaskList); task.on(TASK_EVENTS.TASK_PARTICIPANT_LEFT_FAILED, this.refreshTaskList); - task.on(TASK_EVENTS.TASK_CONFERENCE_STARTED, this.handleConferenceStarted); task.on(TASK_EVENTS.TASK_CONFERENCE_TRANSFERRED, this.refreshTaskList); task.on(TASK_EVENTS.TASK_CONFERENCE_TRANSFER_FAILED, this.refreshTaskList); - task.on(TASK_EVENTS.TASK_POST_CALL_ACTIVITY, this.refreshTaskList); - // Register media event listener for browser devices if (this.deviceType === DEVICE_TYPE_BROWSER) { task.on(TASK_EVENTS.TASK_MEDIA, this.handleTaskMedia); } @@ -643,6 +675,15 @@ class StoreWrapper implements IStoreWrapper { method: 'handleMultiLoginCloseSession', }); if (data && typeof data === 'object' && data.type === 'AgentMultiLoginCloseSession') { + // Don't show the multi-login modal if there's an active task + // The modal blocks UI interactions and should not interfere with task handling + if (this.currentTask) { + this.store.logger.info('CC-Widgets: handleMultiLoginCloseSession(): skipping alert due to active task', { + module: 'storeEventsWrapper.ts', + method: 'handleMultiLoginCloseSession', + }); + return; + } this.setShowMultipleLoginAlert(true); } }; @@ -801,6 +842,7 @@ class StoreWrapper implements IStoreWrapper { this.setConsultStartTimeStamp(undefined); this.setTeamId(''); this.setDigitalChannelsInitialized(false); + this.setLastConsultDestination(null); }); }; diff --git a/packages/contact-center/store/src/task-utils.ts b/packages/contact-center/store/src/task-utils.ts index 0f7cf870b..6f982f8fa 100644 --- a/packages/contact-center/store/src/task-utils.ts +++ b/packages/contact-center/store/src/task-utils.ts @@ -1,22 +1,5 @@ -import { - CONSULT_STATE_COMPLETED, - CONSULT_STATE_CONFERENCING, - CONSULT_STATE_INITIATED, - CUSTOMER, - EXCLUDED_PARTICIPANT_TYPES, - INTERACTION_STATE_CONFERENCE, - INTERACTION_STATE_CONNECTED, - INTERACTION_STATE_POST_CALL, - INTERACTION_STATE_WRAPUP, - MEDIA_TYPE_CONSULT, - RELATIONSHIP_TYPE_CONSULT, - SUPERVISOR, - TASK_STATE_CONSULT, - TASK_STATE_CONSULT_COMPLETED, - TASK_STATE_CONSULTING, - VVA, -} from './constants'; -import {ConsultStatus, ITask, MEDIA_TYPE_TELEPHONY_LOWER, Participant} from './store.types'; +import {EXCLUDED_PARTICIPANT_TYPES, MEDIA_TYPE_CONSULT, RELATIONSHIP_TYPE_CONSULT} from './constants'; +import {ITask, MEDIA_TYPE_TELEPHONY_LOWER, Participant} from './store.types'; /** * Determines if a task is an incoming task @@ -36,36 +19,6 @@ export const isIncomingTask = (task: ITask, agentId: string): boolean => { ); }; -export function getConsultMPCState(task: ITask, agentId: string): string { - const consultMediaResourceId = findMediaResourceId(task, 'consult'); - - const interaction = task.data.interaction; - if ( - (!!consultMediaResourceId && - !!interaction.participants[agentId]?.consultState && - task.data.interaction.state !== INTERACTION_STATE_WRAPUP) || - (!consultMediaResourceId && interaction.participants[agentId]?.consultState === CONSULT_STATE_COMPLETED) - // revisit below condition if needed for post_call scenarios in future - //&& task.data.interaction.state !== INTERACTION_STATE_POST_CALL // If interaction.state is post_call, we want to return post_call. - ) { - // interaction state for all agents when consult is going on - switch (interaction.participants[agentId]?.consultState) { - case CONSULT_STATE_INITIATED: - return TASK_STATE_CONSULT; - case CONSULT_STATE_COMPLETED: - return interaction.state === INTERACTION_STATE_CONNECTED - ? INTERACTION_STATE_CONNECTED - : TASK_STATE_CONSULT_COMPLETED; - case CONSULT_STATE_CONFERENCING: - return INTERACTION_STATE_CONFERENCE; - default: - return TASK_STATE_CONSULTING; - } - } - - return interaction?.state; -} - /** * Checks if the current agent is a secondary agent in a consultation scenario. * Secondary agents are those who were consulted (not the original call owner). @@ -86,88 +39,11 @@ export function isSecondaryAgent(task: ITask): boolean { /** * Checks if the current agent is a secondary EP-DN (Entry Point Dial Number) agent. * This is specifically for telephony consultations to external numbers/entry points. - * @param {Object} task - The task object containing interaction details - * @returns {boolean} True if this is a secondary EP-DN agent in telephony consultation */ export function isSecondaryEpDnAgent(task: ITask): boolean { return task.data.interaction.mediaType === MEDIA_TYPE_TELEPHONY_LOWER && isSecondaryAgent(task); } -export function getTaskStatus(task: ITask, agentId: string): string { - const interaction = task.data.interaction; - if (isSecondaryEpDnAgent(task)) { - if (interaction.state === INTERACTION_STATE_CONFERENCE) { - return INTERACTION_STATE_CONFERENCE; - } - return TASK_STATE_CONSULTING; // handle state of child agent case as we cant rely on interaction state. - } - if ( - (task.data.interaction.state === INTERACTION_STATE_WRAPUP || - task.data.interaction.state === INTERACTION_STATE_POST_CALL) && - interaction.participants[agentId]?.consultState === CONSULT_STATE_COMPLETED - ) { - return TASK_STATE_CONSULT_COMPLETED; - } - - return getConsultMPCState(task, agentId); -} - -export function getConsultStatus(task: ITask, agentId: string): string { - if (!task || !task.data) { - return ConsultStatus.NO_CONSULTATION_IN_PROGRESS; - } - - const state = getTaskStatus(task, agentId); - - const {interaction} = task.data; - const participants = interaction?.participants || {}; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const participant: any = Object.values(participants).find((p: any) => p.pType === 'Agent' && p.id === agentId); - - if (state === TASK_STATE_CONSULT) { - if ((participant && participant.isConsulted) || isSecondaryEpDnAgent(task)) { - return ConsultStatus.BEING_CONSULTED; - } - return ConsultStatus.CONSULT_INITIATED; - } else if (state === TASK_STATE_CONSULTING) { - if ((participant && participant.isConsulted) || isSecondaryEpDnAgent(task)) { - return ConsultStatus.BEING_CONSULTED_ACCEPTED; - } - return ConsultStatus.CONSULT_ACCEPTED; - } else if (state === INTERACTION_STATE_CONNECTED) { - return ConsultStatus.CONNECTED; - } else if (state === INTERACTION_STATE_CONFERENCE) { - return ConsultStatus.CONFERENCE; - } else if (state === TASK_STATE_CONSULT_COMPLETED) { - return ConsultStatus.CONSULT_COMPLETED; - } - // Default return for states that don't match any condition (e.g., chat, email initial states) - return state || ConsultStatus.NO_CONSULTATION_IN_PROGRESS; -} - -export function getIsConferenceInProgress(task: ITask): boolean { - // Early return if required data is missing - if (!task?.data?.interaction?.media || !task?.data?.interactionId) { - return false; - } - - const mediaMainCall = task.data.interaction.media[task.data.interactionId]; - const participantsInMainCall = new Set(mediaMainCall?.participants); - const participants = task?.data?.interaction?.participants; - - const agentParticipants = new Set(); - if (participantsInMainCall.size > 0 && participants) { - participantsInMainCall.forEach((participantId: string) => { - const participant = participants[participantId]; - if (participant && ![CUSTOMER, SUPERVISOR, VVA].includes(participant.pType) && !participant.hasLeft) { - agentParticipants.add(participantId); - } - }); - } - - return agentParticipants.size >= 2; -} - /** * Retrieves the list of active conference participants excluding the current agent * Filters out customers, supervisors, VVAs, and participants who have left @@ -210,67 +86,6 @@ export const getConferenceParticipants = (task: ITask, agentId: string): Partici return participantsList; }; -/** - * Counts the number of active agent participants in the conference - * Excludes customers, supervisors, VVAs, and participants who have left - * - * @param task - The task object containing interaction data - * @returns Count of active agent participants - */ -export function getConferenceParticipantsCount(task: ITask): number { - const participantsList: Participant[] = []; - - // Early return if required data is missing - if (!task?.data?.interaction?.media || !task?.data?.interactionId) { - return 0; - } - - const mediaMainCall = task.data.interaction.media?.[task.data.interactionId]; - const participantsInMainCall = new Set(mediaMainCall?.participants ?? []); - const participants = task.data.interaction.participants ?? {}; - - if (participantsInMainCall.size > 0 && participants) { - participantsInMainCall.forEach((participantId: string) => { - const participant = participants[participantId]; - // Count only active agent participants (excluding customers, supervisors, and VVAs) - if (participant && !EXCLUDED_PARTICIPANT_TYPES.includes(participant.pType) && !participant.hasLeft) { - participantsList.push({ - id: participant.id, - pType: participant.pType, - name: participant.name, - }); - } - }); - } - - return participantsList.length; -} - -export function getIsCustomerInCall(task: ITask): boolean { - // Early return if required data is missing - if (!task?.data?.interaction?.media || !task?.data?.interactionId) { - return false; - } - - const mediaMainCall = task.data.interaction.media[task.data.interactionId]; - const participantsInMainCall = new Set(mediaMainCall?.participants); - const participants = task?.data?.interaction?.participants; - - if (participantsInMainCall.size > 0 && participants) { - return Array.from(participantsInMainCall).some((participantId: string) => { - const participant = participants[participantId]; - return participant && participant.pType === CUSTOMER && !participant.hasLeft; - }); - } - - return false; -} - -export function getIsConsultInProgress(task: ITask): boolean { - const mediaObject = task.data.interaction.media; - return Object.values(mediaObject).some((media) => media.mType === MEDIA_TYPE_CONSULT); -} - export function isInteractionOnHold(task: ITask): boolean { if (!task || !task.data || !task.data.interaction) { return false; @@ -279,7 +94,11 @@ export function isInteractionOnHold(task: ITask): boolean { if (!interaction.media) { return false; } - return Object.values(interaction.media).some((media) => media.isHold); + // Only check the main call media — consult hold is handled separately + // in the consulting section UI. Without this filter, switching to + // main call during a consult would incorrectly show the hold indicator + // because the consult media has isHold: true. + return Object.values(interaction.media).some((media) => media.mType === 'mainCall' && media.isHold); } export const setmTypeForEPDN = (task: ITask, mType: string) => { @@ -299,39 +118,6 @@ export const findMediaResourceId = (task: ITask, mType: string) => { return ''; }; -const isConsultOnHoldMPC = (task: ITask, agentId: string): boolean => { - const isInConsultState = [TASK_STATE_CONSULT, TASK_STATE_CONSULTING].includes(getConsultMPCState(task, agentId)); - const consultMediaResourceId = task.data.consultMediaResourceId; - const isConsultHold = consultMediaResourceId && task.data.interaction.media[consultMediaResourceId]?.isHold; - - return isInConsultState && !isConsultHold; -}; - -export const findHoldStatus = (task: ITask, mType: string, agentId: string): boolean => { - const interaction = task.data.interaction; - if (!interaction) { - return false; - } - mType = setmTypeForEPDN(task, mType); // set mType if agent is secondary EPDN agent - const mediaId = findMediaResourceId(task, mType); - // custom mainCall hold status for agent who initiated the consult. - if ( - mType === 'mainCall' && - interaction.media[mediaId]?.participants.includes(agentId) && - (isConsultOnHoldMPC(task, agentId) || [TASK_STATE_CONSULT_COMPLETED].includes(getConsultMPCState(task, agentId))) - ) { - return true; - } - - // hold status for agents who are in consulting call(consulting agent | consulted agent) - - return mType === TASK_STATE_CONSULT && interaction.media[mediaId] - ? interaction.media[mediaId].participants.includes(agentId) - ? interaction.media[mediaId].isHold - : false - : (interaction.media[mediaId] && interaction.media[mediaId].isHold) || false; // For all the other agent for main whatever is the status of main call hold -}; - /** * Finds the hold timestamp for a specific media type (mainCall, consult, etc.) * Used for timer alignment in Consult & Conference scenarios to match Agent Desktop behavior. diff --git a/packages/contact-center/task/src/CallControl/index.tsx b/packages/contact-center/task/src/CallControl/index.tsx index 022629749..90c62c93f 100644 --- a/packages/contact-center/task/src/CallControl/index.tsx +++ b/packages/contact-center/task/src/CallControl/index.tsx @@ -15,8 +15,6 @@ const CallControlInternal: React.FunctionComponent = observer( wrapupCodes, consultStartTimeStamp, callControlAudio, - deviceType, - featureFlags, allowConsultToQueue, isMuted, agentId, @@ -31,11 +29,9 @@ const CallControlInternal: React.FunctionComponent = observer( onRecordingToggle, onToggleMute, logger, - deviceType, - featureFlags, isMuted, - conferenceEnabled, agentId, + conferenceEnabled, }), wrapupCodes, consultStartTimeStamp, @@ -57,7 +53,7 @@ const CallControl: React.FunctionComponent = (props) => { if (store.onErrorCallback) store.onErrorCallback('CallControl', error); }} > - + ); }; diff --git a/packages/contact-center/task/src/CallControlCAD/index.tsx b/packages/contact-center/task/src/CallControlCAD/index.tsx index df353bd8b..4a9e67f3b 100644 --- a/packages/contact-center/task/src/CallControlCAD/index.tsx +++ b/packages/contact-center/task/src/CallControlCAD/index.tsx @@ -16,8 +16,8 @@ const CallControlCADInternal: React.FunctionComponent = observ onToggleMute, callControlClassName, callControlConsultClassName, - conferenceEnabled, consultTransferOptions, + conferenceEnabled, }) => { const { logger, @@ -26,8 +26,6 @@ const CallControlCADInternal: React.FunctionComponent = observ consultStartTimeStamp, callControlAudio, allowConsultToQueue, - featureFlags, - deviceType, isMuted, agentId, } = store; @@ -40,11 +38,9 @@ const CallControlCADInternal: React.FunctionComponent = observ onRecordingToggle, onToggleMute, logger, - deviceType, - featureFlags, isMuted, - conferenceEnabled, agentId, + conferenceEnabled, }), wrapupCodes, consultStartTimeStamp, @@ -68,7 +64,7 @@ const CallControlCAD: React.FunctionComponent = (props) => { if (store.onErrorCallback) store.onErrorCallback('CallControlCAD', error); }} > - + ); }; diff --git a/packages/contact-center/task/src/IncomingTask/index.tsx b/packages/contact-center/task/src/IncomingTask/index.tsx index 146686e71..496a257d0 100644 --- a/packages/contact-center/task/src/IncomingTask/index.tsx +++ b/packages/contact-center/task/src/IncomingTask/index.tsx @@ -9,12 +9,13 @@ import {IncomingTaskProps} from '../task.types'; const IncomingTaskInternal: React.FunctionComponent = observer( ({incomingTask, onAccepted, onRejected}) => { - const {deviceType, logger} = store; - const result = useIncomingTask({incomingTask, onAccepted, onRejected, deviceType, logger}); + const {logger, isDeclineButtonEnabled} = store; + const result = useIncomingTask({incomingTask, onAccepted, onRejected, logger}); const props = { ...result, logger, + isDeclineButtonEnabled, }; return ; diff --git a/packages/contact-center/task/src/TaskList/index.tsx b/packages/contact-center/task/src/TaskList/index.tsx index 3cd675c52..8393ce914 100644 --- a/packages/contact-center/task/src/TaskList/index.tsx +++ b/packages/contact-center/task/src/TaskList/index.tsx @@ -9,14 +9,15 @@ import {TaskListProps} from '../task.types'; const TaskListInternal: React.FunctionComponent = observer( ({onTaskAccepted, onTaskDeclined, onTaskSelected}) => { - const {cc, taskList, currentTask, deviceType, logger, agentId} = store; + const {cc, taskList, currentTask, logger, agentId, isDeclineButtonEnabled} = store; - const result = useTaskList({cc, deviceType, logger, taskList, onTaskAccepted, onTaskDeclined, onTaskSelected}); + const result = useTaskList({cc, logger, taskList, onTaskAccepted, onTaskDeclined, onTaskSelected}); const props = { ...result, currentTask, logger, agentId, + isDeclineButtonEnabled, }; return ; diff --git a/packages/contact-center/task/src/Utils/task-util.ts b/packages/contact-center/task/src/Utils/task-util.ts index b5b3dea5c..5c869a286 100644 --- a/packages/contact-center/task/src/Utils/task-util.ts +++ b/packages/contact-center/task/src/Utils/task-util.ts @@ -1,79 +1,12 @@ -import { - ILogger, - DIAL_NUMBER, - EXTENSION, - DESKTOP, - ConsultStatus, - getConsultStatus, - getIsConsultInProgress, - getIsCustomerInCall, - getConferenceParticipantsCount, - findHoldStatus, -} from '@webex/cc-store'; -import {ITask, Interaction} from '@webex/contact-center'; -import {Visibility} from '@webex/cc-components'; -import { - MEDIA_TYPE_TELEPHONY, - MEDIA_TYPE_CHAT, - MEDIA_TYPE_EMAIL, - MAX_PARTICIPANTS_IN_MULTIPARTY_CONFERENCE, - DestinationAgentType, -} from './constants'; -import {DeviceTypeFlags} from '../task.types'; - -// ==================== UTILITY FUNCTIONS ==================== - -/** - * Helper function to get device type flags to avoid repetition - */ -function getDeviceTypeFlags(deviceType: string): DeviceTypeFlags { - return { - isBrowser: deviceType === DESKTOP, - isAgentDN: deviceType === DIAL_NUMBER, - isExtension: deviceType === EXTENSION, - }; -} +import {Interaction} from '@webex/contact-center'; /** - * Helper function to check if telephony is supported for the device + * Finds the hold timestamp for a specific media type from an interaction. + * Used by useHoldTimer for hold duration display. + * + * Note: There is a separate findHoldTimestamp in @webex/cc-store that takes ITask. + * This one takes Interaction directly. */ -function isTelephonySupported(deviceType: string, webRtcEnabled: boolean): boolean { - const {isBrowser, isAgentDN, isExtension} = getDeviceTypeFlags(deviceType); - return (isBrowser && webRtcEnabled) || isAgentDN || isExtension; -} - -/** - * Check if consulting with an EP_DN agent (Entry Point Dial Number) - * This function looks for EP-DN participants in the consult media - */ -function isConsultingWithEpDnAgent(task: ITask): boolean { - if (!task?.data?.interaction?.media || !task?.data?.interaction?.participants) { - return false; - } - - // Find the consult media - const consultMedia = Object.values(task.data.interaction.media).find((media) => media.mType === 'consult'); - - if (!consultMedia || !consultMedia.participants) { - return false; - } - - // Check if any participant in the consult media is an EP-DN - const participants = task.data.interaction.participants; - return consultMedia.participants.some((participantId: string) => { - const participant = participants[participantId]; - if (!participant) return false; - - // Check for EP-DN participant types using the type field - return ( - participant.type === DestinationAgentType.EP_DN || - participant.type === DestinationAgentType.EPDN || - participant.type === DestinationAgentType.ENTRY_POINT || - participant.type === DestinationAgentType.EP - ); - }); -} - export function findHoldTimestamp(interaction: Interaction, mType = 'mainCall'): number | null { if (interaction?.media) { const media = Object.values(interaction.media).find((m) => m.mType === mType); @@ -81,574 +14,3 @@ export function findHoldTimestamp(interaction: Interaction, mType = 'mainCall'): } return null; } - -// ==================== CALL CONTROL BUTTON VISIBILITY FUNCTIONS ==================== - -/** - * Get visibility for Accept button - */ -export function getAcceptButtonVisibility( - isBrowser: boolean, - isPhoneDevice: boolean, - webRtcEnabled: boolean, - isCall: boolean, - isDigitalChannel: boolean -): Visibility { - const isVisible = - (isBrowser && ((webRtcEnabled && isCall) || isDigitalChannel)) || (isPhoneDevice && isDigitalChannel); - - return {isVisible, isEnabled: true}; -} - -/** - * Get visibility for Decline button - */ -export function getDeclineButtonVisibility(isBrowser: boolean, webRtcEnabled: boolean, isCall: boolean): Visibility { - const isVisible = isBrowser && webRtcEnabled && isCall; - - return {isVisible, isEnabled: true}; -} - -/** - * Get visibility for End button (matches Agent Desktop behavior) - */ -export function getEndButtonVisibility( - isBrowser: boolean, - isEndCallEnabled: boolean, - isCall: boolean, - isConsultInitiatedOrAcceptedOrBeingConsulted: boolean, - isConferenceInProgress: boolean, - isConsultCompleted: boolean, - isHeld: boolean, - consultCallHeld: boolean, - task?: ITask, - agentId?: string -): Visibility { - const isVisible = isBrowser || (isEndCallEnabled && isCall) || !isCall; - const isEpDnConsult = task && agentId ? isConsultingWithEpDnAgent(task) : false; - - if (isConsultInitiatedOrAcceptedOrBeingConsulted) { - let isEnabled = false; - if (isEpDnConsult) { - // EP-DN consult: enabled when on main call OR during conference when main not held - isEnabled = consultCallHeld || (!isHeld && isConferenceInProgress && !isConsultCompleted); - } - return {isVisible, isEnabled}; - } - - // Default logic for other states - const isEnabled = - (!isHeld || (isConferenceInProgress && !isConsultCompleted)) && - (!isConsultInitiatedOrAcceptedOrBeingConsulted || consultCallHeld); - - return {isVisible, isEnabled}; -} - -/** - * Get visibility for Mute/Unmute button - */ -export function getMuteUnmuteButtonVisibility( - isBrowser: boolean, - webRtcEnabled: boolean, - isCall: boolean, - isBeingConsulted: boolean -): Visibility { - const isVisible = isBrowser && webRtcEnabled && isCall && !isBeingConsulted; - - return {isVisible, isEnabled: true}; -} - -/** - * Get visibility for Hold/Resume button - */ -export function getHoldResumeButtonVisibility( - isTelephonySupported: boolean, - isCall: boolean, - isConferenceInProgress: boolean, - isConsultInProgress: boolean, - isHeld: boolean, - isBeingConsulted: boolean, - isConsultCompleted: boolean -): Visibility { - const isVisible = isCall && isTelephonySupported && !isBeingConsulted; - // Enable if: (NOT in conference AND NOT in consult) OR (in conference AND consult completed AND held) - const isEnabled = - (!isConferenceInProgress && !isConsultInProgress) || (isConferenceInProgress && isConsultCompleted && isHeld); - - return {isVisible, isEnabled}; -} - -// ==================== RECORDING FUNCTIONS ==================== - -/** - * Get visibility for Pause/Resume Recording button - */ -export function getPauseResumeRecordingButtonVisibility( - isTelephonySupported: boolean, - isCall: boolean, - isConferenceInProgress: boolean, - isConsultInitiatedOrAccepted: boolean -): Visibility { - const isVisible = isCall && isTelephonySupported && !isConferenceInProgress && !isConsultInitiatedOrAccepted; - - return {isVisible, isEnabled: true}; -} - -/** - * Get visibility for Recording Indicator - */ -export function getRecordingIndicatorVisibility(isCall: boolean): Visibility { - return {isVisible: isCall, isEnabled: true}; -} - -// ==================== TRANSFER AND CONFERENCE FUNCTIONS ==================== - -/** - * Get visibility for Transfer button - */ -export function getTransferButtonVisibility( - isTransferVisibility: boolean, - isConferenceInProgress: boolean, - isConsultInitiatedOrAccepted: boolean -): Visibility { - const isVisible = isTransferVisibility && !isConferenceInProgress && !isConsultInitiatedOrAccepted; - - return {isVisible, isEnabled: true}; -} - -/** - * Get visibility for Conference button - */ -export function getConferenceButtonVisibility( - isBrowser: boolean, - webRtcEnabled: boolean, - isCall: boolean, - isChat: boolean, - isBeingConsulted: boolean, - conferenceEnabled: boolean -): Visibility { - const isVisible = ((isBrowser && isCall && webRtcEnabled) || isChat) && !isBeingConsulted && conferenceEnabled; - - return {isVisible, isEnabled: true}; -} - -/** - * Get visibility for Exit Conference button - */ -export function getExitConferenceButtonVisibility( - isConferenceInProgress: boolean, - isConsultInitiatedOrAccepted: boolean, - consultCallHeld: boolean, - isHeld: boolean, - isConsultCompleted: boolean, - conferenceEnabled: boolean -): Visibility { - const isVisible = isConferenceInProgress && !isConsultInitiatedOrAccepted && conferenceEnabled; - const isConferenceWithConsultNotHeld = isConferenceInProgress && isConsultInitiatedOrAccepted && !consultCallHeld; - // Disable if: conference with consult not held OR (held AND in conference AND consult completed) - const isEnabled = !isConferenceWithConsultNotHeld && !(isHeld && isConferenceInProgress && isConsultCompleted); - - return {isVisible, isEnabled}; -} - -/** - * Get visibility for Merge Conference button - */ -export function getMergeConferenceButtonVisibility( - isConsultInitiatedOrAccepted: boolean, - isConsultAccepted: boolean, - consultCallHeld: boolean, - isConferenceInProgress: boolean, - isCustomerInCall: boolean, - conferenceEnabled: boolean -): Visibility { - const isVisible = isConsultInitiatedOrAccepted && isCustomerInCall && conferenceEnabled; - const isConferenceWithConsultNotHeld = isConferenceInProgress && isConsultInitiatedOrAccepted && !consultCallHeld; - const isEnabled = isConsultAccepted && consultCallHeld && !isConferenceWithConsultNotHeld; - - return {isVisible, isEnabled}; -} - -// ==================== CONSULT FUNCTIONS ==================== - -/** - * Get visibility for Consult button - */ -export function getConsultButtonVisibility( - isTelephonySupported: boolean, - isCall: boolean, - isConsultInProgress: boolean, - isCustomerInCall: boolean, - conferenceParticipantsCount: number, - maxParticipantsInConference: number, - isBeingConsulted: boolean, - isHeld: boolean, - isConsultCompleted: boolean, - isConferenceInProgress: boolean -): Visibility { - const isVisible = isCall && isTelephonySupported && !isBeingConsulted; - const isEnabled = - conferenceParticipantsCount < maxParticipantsInConference && - !isConsultInProgress && - isCustomerInCall && - !(isHeld && isConferenceInProgress && !isConsultCompleted); - - return {isVisible, isEnabled}; -} - -/** - * Get visibility for End Consult button - */ -export function getEndConsultButtonVisibility( - isEndConsultEnabled: boolean, - isTelephonySupported: boolean, - isCall: boolean, - isConsultInitiatedOrAccepted: boolean -): Visibility { - const isVisible = isEndConsultEnabled && isCall && isTelephonySupported && isConsultInitiatedOrAccepted; - - return {isVisible, isEnabled: true}; -} - -/** - * Get visibility for Consult Transfer button - */ -export function getConsultTransferButtonVisibility( - isConsultInitiatedOrAccepted: boolean, - isConsultAccepted: boolean, - consultCallHeld: boolean, - isConferenceInProgress: boolean, - isCustomerInCall: boolean -): Visibility { - const isVisible = isConsultInitiatedOrAccepted && isCustomerInCall; - const isConferenceWithConsultNotHeld = isConferenceInProgress && isConsultInitiatedOrAccepted && !consultCallHeld; - const isEnabled = isConsultAccepted && consultCallHeld && !isConferenceWithConsultNotHeld; - - return {isVisible, isEnabled}; -} - -/** - * Get visibility for Merge Conference Consult button - */ -export function getMergeConferenceConsultButtonVisibility( - isConsultAccepted: boolean, - isConsultInitiated: boolean, - consultCallHeld: boolean, - isCustomerInCall: boolean, - conferenceEnabled: boolean -): Visibility { - const isVisible = (isConsultAccepted || isConsultInitiated) && conferenceEnabled; - const isEnabled = !consultCallHeld && isConsultAccepted && isCustomerInCall; - - return {isVisible, isEnabled}; -} - -/** - * Get visibility for Consult Transfer Consult button - */ -export function getConsultTransferConsultButtonVisibility( - isConsultAccepted: boolean, - isConsultInitiated: boolean, - consultCallHeld: boolean, - isCustomerInCall: boolean -): Visibility { - const isVisible = isConsultAccepted || isConsultInitiated; - const isEnabled = !consultCallHeld && isConsultAccepted && isCustomerInCall; - - return {isVisible, isEnabled}; -} - -/** - * Get visibility for Mute/Unmute Consult button - */ -export function getMuteUnmuteConsultButtonVisibility( - isBrowser: boolean, - webRtcEnabled: boolean, - isCall: boolean, - isConsultInitiated: boolean, - isBeingConsulted: boolean -): Visibility { - const isVisible = isBrowser && webRtcEnabled && isCall && (isConsultInitiated || isBeingConsulted); - - return {isVisible, isEnabled: true}; -} - -// ==================== SWITCH CALL FUNCTIONS ==================== - -/** - * Get visibility for Switch to Main Call button - */ -export function getSwitchToMainCallButtonVisibility( - isBeingConsulted: boolean, - isConsultAccepted: boolean, - isConsultInitiated: boolean, - consultCallHeld: boolean, - isCustomerInCall: boolean, - isConferenceInProgress: boolean -): Visibility { - const isVisible = !isBeingConsulted && (isConsultAccepted || isConsultInitiated) && !consultCallHeld; - const isEnabled = isConsultAccepted && (isCustomerInCall || (!isCustomerInCall && isConferenceInProgress)); - - return {isVisible, isEnabled}; -} - -/** - * Get visibility for Switch to Consult button - */ -export function getSwitchToConsultButtonVisibility(isBeingConsulted: boolean, consultCallHeld: boolean): Visibility { - const isVisible = !isBeingConsulted && consultCallHeld; - // const isConferenceWithConsultNotHeld = isConferenceInProgress && isConsultAccepted && !consultCallHeld; - const isEnabled = true; - - return {isVisible, isEnabled}; -} - -// ==================== OTHER FUNCTIONS ==================== - -/** - * Get visibility for Wrapup button - */ -export function getWrapupButtonVisibility(task: ITask): Visibility { - const isVisible = task?.data?.wrapUpRequired ?? false; - - return {isVisible, isEnabled: true}; -} -// ==================== MAIN AGGREGATOR FUNCTION ==================== - -/** - * This function determines the visibility of various controls based on the task's data. - * @param deviceType The device type (Browser, Extension, AgentDN) - * @param featureFlags Feature flags configuration object - * @param task The task object - * @param agentId The agent ID - * @param conferenceEnabled Whether conference is enabled - * @param logger Optional logger instance - * @returns An object containing the visibility and state of various controls - */ -export function getControlsVisibility( - deviceType: string, - featureFlags: {[key: string]: boolean}, - task: ITask, - agentId: string, - conferenceEnabled: boolean, - logger?: ILogger -) { - try { - // Extract media type and related flags - const {mediaType} = task?.data?.interaction || {}; - const isCall = mediaType === MEDIA_TYPE_TELEPHONY; - const isChat = mediaType === MEDIA_TYPE_CHAT; - const isEmail = mediaType === MEDIA_TYPE_EMAIL; - const isDigitalChannel = isChat || isEmail; - - // Extract device type flags - const {isBrowser, isAgentDN, isExtension} = getDeviceTypeFlags(deviceType); - const isPhoneDevice = isAgentDN || isExtension; - - // Extract feature flags - const {isEndCallEnabled, isEndConsultEnabled, webRtcEnabled} = featureFlags; - - // Calculate telephony support - const telephonySupported = isTelephonySupported(deviceType, webRtcEnabled); - - // Calculate task state flags - const isTransferVisibility = isBrowser ? webRtcEnabled : true; - const isConferenceInProgress = (task?.data?.isConferenceInProgress && conferenceEnabled) ?? false; - const isConsultInProgress = getIsConsultInProgress(task); - const isHeld = findHoldStatus(task, 'mainCall', agentId); - const isCustomerInCall = getIsCustomerInCall(task); - // const mainCallHeld = findHoldStatus(task, 'mainCall', agentId); - const consultCallHeld = findHoldStatus(task, 'consult', agentId); - const taskConsultStatus = getConsultStatus(task, agentId); - - // Calculate conference participants count - const conferenceParticipantsCount = getConferenceParticipantsCount(task); - - // Calculate consult status flags (REUSED CONDITIONS) - const isConsultInitiated = taskConsultStatus === ConsultStatus.CONSULT_INITIATED; - const isConsultAccepted = taskConsultStatus === ConsultStatus.CONSULT_ACCEPTED; - const isBeingConsulted = taskConsultStatus === ConsultStatus.BEING_CONSULTED_ACCEPTED; - const isConsultCompleted = taskConsultStatus === ConsultStatus.CONSULT_COMPLETED; - const isConsultInitiatedOrAccepted = isConsultInitiated || isConsultAccepted || isBeingConsulted; - const isConsultInitiatedOrAcceptedOnly = isConsultInitiated || isConsultAccepted; - const isConsultInitiatedOrAcceptedOrBeingConsulted = - isConsultInitiated || - isConsultAccepted || - taskConsultStatus === ConsultStatus.BEING_CONSULTED || - isBeingConsulted; - - // Build controls visibility object - const controls = { - // Basic call controls - accept: getAcceptButtonVisibility(isBrowser, isPhoneDevice, webRtcEnabled, isCall, isDigitalChannel), - decline: getDeclineButtonVisibility(isBrowser, webRtcEnabled, isCall), - end: getEndButtonVisibility( - isBrowser, - isEndCallEnabled, - isCall, - isConsultInitiatedOrAcceptedOrBeingConsulted, - isConferenceInProgress, - isConsultCompleted, - isHeld, - consultCallHeld, - task, - agentId - ), - muteUnmute: getMuteUnmuteButtonVisibility(isBrowser, webRtcEnabled, isCall, isBeingConsulted), - holdResume: getHoldResumeButtonVisibility( - telephonySupported, - isCall, - isConferenceInProgress, - isConsultInProgress, - isHeld, - isBeingConsulted, - isConsultCompleted - ), - - // Recording controls - pauseResumeRecording: getPauseResumeRecordingButtonVisibility( - telephonySupported, - isCall, - isConferenceInProgress, - isConsultInitiatedOrAccepted - ), - recordingIndicator: getRecordingIndicatorVisibility(isCall), - - // Transfer and conference controls - transfer: getTransferButtonVisibility(isTransferVisibility, isConferenceInProgress, isConsultInitiatedOrAccepted), - conference: getConferenceButtonVisibility( - isBrowser, - webRtcEnabled, - isCall, - isChat, - isBeingConsulted, - conferenceEnabled - ), - exitConference: getExitConferenceButtonVisibility( - isConferenceInProgress, - isConsultInitiatedOrAccepted, - consultCallHeld, - isHeld, - isConsultCompleted, - conferenceEnabled - ), - mergeConference: getMergeConferenceButtonVisibility( - isConsultInitiatedOrAcceptedOnly, - isConsultAccepted, - consultCallHeld, - isConferenceInProgress, - isCustomerInCall, - conferenceEnabled - ), - - // Consult controls - consult: getConsultButtonVisibility( - telephonySupported, - isCall, - isConsultInProgress, - isCustomerInCall, - conferenceParticipantsCount, - MAX_PARTICIPANTS_IN_MULTIPARTY_CONFERENCE, - isBeingConsulted, - isHeld, - isConsultCompleted, - isConferenceInProgress - ), - endConsult: getEndConsultButtonVisibility( - isEndConsultEnabled, - telephonySupported, - isCall, - isConsultInitiatedOrAccepted - ), - consultTransfer: getConsultTransferButtonVisibility( - isConsultInitiatedOrAcceptedOnly, - isConsultAccepted, - consultCallHeld, - isConferenceInProgress, - isCustomerInCall - ), - consultTransferConsult: getConsultTransferConsultButtonVisibility( - isConsultAccepted, - isConsultInitiated, - consultCallHeld, - isCustomerInCall - ), - mergeConferenceConsult: getMergeConferenceConsultButtonVisibility( - isConsultAccepted, - isConsultInitiated, - consultCallHeld, - isCustomerInCall, - conferenceEnabled - ), - muteUnmuteConsult: getMuteUnmuteConsultButtonVisibility( - isBrowser, - webRtcEnabled, - isCall, - isConsultInitiated, - isBeingConsulted - ), - - // Switch call controls - switchToMainCall: getSwitchToMainCallButtonVisibility( - isBeingConsulted, - isConsultAccepted, - isConsultInitiated, - consultCallHeld, - isCustomerInCall, - isConferenceInProgress - ), - switchToConsult: getSwitchToConsultButtonVisibility(isBeingConsulted, consultCallHeld), - - // Other controls - wrapup: getWrapupButtonVisibility(task), - - // State flags - isConferenceInProgress, - isConsultInitiated, - isConsultInitiatedAndAccepted: isConsultAccepted, - isConsultReceived: isBeingConsulted, - isConsultInitiatedOrAccepted: isConsultInitiatedOrAccepted, - isHeld, - consultCallHeld, - }; - - return controls; - } catch (error) { - logger?.error(`CC-Widgets: Task: Error in getControlsVisibility - ${error.message}`, { - module: 'task-util', - method: 'getControlsVisibility', - }); - - // Return safe default controls - const defaultVisibility: Visibility = {isVisible: false, isEnabled: false}; - return { - accept: defaultVisibility, - decline: defaultVisibility, - end: defaultVisibility, - muteUnmute: defaultVisibility, - holdResume: defaultVisibility, - pauseResumeRecording: defaultVisibility, - recordingIndicator: defaultVisibility, - transfer: defaultVisibility, - conference: defaultVisibility, - exitConference: defaultVisibility, - mergeConference: defaultVisibility, - consult: defaultVisibility, - endConsult: defaultVisibility, - consultTransfer: defaultVisibility, - consultTransferConsult: defaultVisibility, - mergeConferenceConsult: defaultVisibility, - muteUnmuteConsult: defaultVisibility, - switchToMainCall: defaultVisibility, - switchToConsult: defaultVisibility, - wrapup: {isVisible: false, isEnabled: true}, - isConferenceInProgress: false, - isConsultInitiated: false, - isConsultInitiatedAndAccepted: false, - isConsultReceived: false, - isConsultInitiatedOrAccepted: false, - isHeld: false, - consultCallHeld: false, - }; - } -} diff --git a/packages/contact-center/task/src/Utils/timer-utils.ts b/packages/contact-center/task/src/Utils/timer-utils.ts index 9e3cf15cf..73699816a 100644 --- a/packages/contact-center/task/src/Utils/timer-utils.ts +++ b/packages/contact-center/task/src/Utils/timer-utils.ts @@ -1,5 +1,4 @@ -import {ITask, findHoldTimestamp} from '@webex/cc-store'; -import {ControlVisibility} from '@webex/cc-components'; +import {ITask, TaskUIControls} from '@webex/cc-store'; import { TIMER_LABEL_WRAP_UP, TIMER_LABEL_POST_CALL, @@ -16,24 +15,38 @@ export interface TimerData { timestamp: number; } +/** + * Find the latest (most recently added) consult media from the interaction. + * + * After transfer → re-consult the backend may leave the OLD consult media + * in the interaction alongside the NEW one. Using Array.find() would return + * the first (stale) entry; we need the last one which is the active consult. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function findLatestConsultMedia(interaction: any): any { + if (!interaction?.media) return null; + const allMedia = Object.values(interaction.media); + let latest = null; + for (const m of allMedia) { + if ((m as {mType: string}).mType === 'consult') { + latest = m; + } + } + return latest; +} + /** * Calculate state timer label and timestamp based on task state. * Priority: Wrap Up > Post Call - * - * @param currentTask - The current task object - * @param controlVisibility - Control visibility flags - * @param agentId - The current agent ID - * @returns TimerData object with label and timestamp */ export function calculateStateTimerData( currentTask: ITask | null, - controlVisibility: ControlVisibility | null, + controls: TaskUIControls | null, agentId: string ): TimerData { - // Default return value const defaultTimer: TimerData = {label: null, timestamp: 0}; - if (!currentTask || !controlVisibility) { + if (!currentTask || !controls) { return defaultTimer; } @@ -44,29 +57,27 @@ export function calculateStateTimerData( return defaultTimer; } - // Extract timestamps from participant data let wrapUpTimestamp = 0; let postCallTimestamp = 0; - // Wrap-up timestamp: use lastUpdated if currently in wrap-up, otherwise use wrapUpTimestamp if (participant.isWrapUp) { wrapUpTimestamp = participant.lastUpdated || 0; } else { wrapUpTimestamp = participant.wrapUpTimestamp || 0; } - // Post-call timestamp: use currentStateTimestamp postCallTimestamp = participant.currentStateTimestamp || 0; - // Priority 1: Wrap-up state (highest priority) - if (controlVisibility.wrapup?.isVisible && wrapUpTimestamp) { - return { - label: TIMER_LABEL_WRAP_UP, - timestamp: wrapUpTimestamp, - }; + if (controls.main?.wrapup?.isVisible) { + const effectiveWrapUpTimestamp = wrapUpTimestamp || currentTask.data?.eventTime || 0; + if (effectiveWrapUpTimestamp) { + return { + label: TIMER_LABEL_WRAP_UP, + timestamp: effectiveWrapUpTimestamp, + }; + } } - // Priority 2: Post-call state (only if not in wrap-up) const isInPostCall = interaction?.state === 'post_call' || participant?.currentState === 'post_call'; if (isInPostCall && postCallTimestamp) { return { @@ -82,20 +93,20 @@ export function calculateStateTimerData( * Calculate consult timer label and timestamp based on consult state. * Handles consult on hold vs active consulting states. * - * @param currentTask - The current task object - * @param controlVisibility - Control visibility flags - * @param agentId - The current agent ID - * @returns TimerData object with label and timestamp + * Approach mirrors the original next-branch pattern: derive consultCallHeld + * from the consult media's isHold flag (task data), NOT from SDK uiControls + * properties like activeLeg or switch button visibility. Those UI properties + * have different lifecycle timing and broader semantics that cause false + * positives (e.g., switch.isVisible is true during CONSULT_INITIATING). */ export function calculateConsultTimerData( currentTask: ITask | null, - controlVisibility: ControlVisibility | null, + controls: TaskUIControls | null, agentId: string ): TimerData { - // Default return value const defaultTimer: TimerData = {label: TIMER_LABEL_CONSULTING, timestamp: 0}; - if (!currentTask || !controlVisibility) { + if (!currentTask || !controls) { return defaultTimer; } @@ -106,7 +117,6 @@ export function calculateConsultTimerData( return defaultTimer; } - // Extract consult start timestamp let consultStartTimeStamp = 0; if (participant.consultTimestamp) { consultStartTimeStamp = participant.consultTimestamp; @@ -114,25 +124,42 @@ export function calculateConsultTimerData( consultStartTimeStamp = participant.lastUpdated; } - // If no consult timestamp, return default if (!consultStartTimeStamp) { return defaultTimer; } - // Check if consult call is on hold - if (controlVisibility.consultCallHeld) { - // Extract consult hold timestamp - const consultHoldTimestamp = findHoldTimestamp(currentTask, 'consult'); + // Use the LATEST consult media, not the first. After transfer → re-consult + // the backend keeps the old consult media (with stale isHold=true) alongside + // the new one. Array.find() would return the old stale entry. + let consultMedia = findLatestConsultMedia(interaction); + + // Consulted agent (Agent 2): their call is mType "mainCall" not "consult". + // When the initiator switches away, Agent 2's mainCall is put on hold. + if (!consultMedia && interaction?.media) { + const mainMedia = Object.values(interaction.media).find( + (m: any) => m?.mType === 'mainCall' + ) as any; + if (mainMedia) { + consultMedia = mainMedia; + } + } + + const isConsultMediaHeld = consultMedia?.isHold === true; + const consultHoldTimestamp = consultMedia?.holdTimestamp ?? null; + const consultCallHeld = isConsultMediaHeld && consultHoldTimestamp !== null && consultHoldTimestamp > 0; + if (consultCallHeld) { return { label: TIMER_LABEL_CONSULT_ON_HOLD, - // Use consultHoldTimestamp when on hold, fallback to consult start time - timestamp: consultHoldTimestamp && consultHoldTimestamp > 0 ? consultHoldTimestamp : consultStartTimeStamp, + timestamp: consultHoldTimestamp, }; } - // Active consulting - determine label based on consult state - const label = controlVisibility.isConsultInitiated ? TIMER_LABEL_CONSULT_REQUESTED : TIMER_LABEL_CONSULTING; + // Distinguish "Consult Requested" from "Consulting" using participant data. + const isConsultInitiated = + participant?.consultState === 'consultInitiated' || + currentTask.data?.consultStatus === 'consultInitiated'; + const label = isConsultInitiated ? TIMER_LABEL_CONSULT_REQUESTED : TIMER_LABEL_CONSULTING; return { label, diff --git a/packages/contact-center/task/src/Utils/useHoldTimer.ts b/packages/contact-center/task/src/Utils/useHoldTimer.ts index eea2fd758..8573fa2bf 100644 --- a/packages/contact-center/task/src/Utils/useHoldTimer.ts +++ b/packages/contact-center/task/src/Utils/useHoldTimer.ts @@ -1,8 +1,8 @@ import {useEffect, useRef, useState} from 'react'; -import {ITask} from '@webex/cc-store'; +import {ITask, isInteractionOnHold} from '@webex/cc-store'; +import {TaskUIControls} from '@webex/contact-center'; import {findHoldTimestamp} from './task-util'; -// Worker script for hold timer - defined at module level as it never changes const HOLD_TIMER_WORKER_SCRIPT = ` let intervalId = null; self.onmessage = function(e) { @@ -22,60 +22,80 @@ const HOLD_TIMER_WORKER_SCRIPT = ` `; /** - * Custom hook to manage hold timer using a Web Worker - * Prioritizes consult hold over main call hold + * Custom hook to manage hold timer using a Web Worker. + * + * Derives two stable primitives from props — a boolean hold flag and a + * numeric timestamp — and uses them as the sole effect dependencies. + * This prevents the worker from being killed/recreated on every + * currentTask or controls reference change. * * @param currentTask - The current task object + * @param controls - SDK-computed UI controls with activeLeg * @returns holdTime - The elapsed time in seconds since the call was put on hold */ -export const useHoldTimer = (currentTask: ITask | null): number => { +export const useHoldTimer = (currentTask: ITask | null, controls?: TaskUIControls): number => { const [holdTime, setHoldTime] = useState(0); const workerRef = useRef(null); + // --- Derive stable primitives (compared by value, not reference) --- + + const isConsulting = controls?.consult?.endConsult?.isVisible || controls?.main?.endConsult?.isVisible; + + const customerPresent = Boolean( + currentTask?.data?.interaction?.participants && + Object.values(currentTask.data.interaction.participants).some( + (p: any) => p?.pType === 'Customer' && !p?.hasLeft + ) + ); + + // During consulting, activeLeg='consult' means the main call is on hold. + // Outside consulting, fall back to the actual media hold state. + // When customer has left, never show the hold timer (follows Agent Desktop behavior). + const mainCallOnHold = isConsulting && customerPresent + ? controls?.activeLeg === 'consult' + : currentTask + ? isInteractionOnHold(currentTask) + : false; + + const rawTs = currentTask?.data?.interaction + ? findHoldTimestamp(currentTask.data.interaction, 'mainCall') + : null; + const holdTimestampMs: number | null = rawTs + ? (rawTs < 10000000000 ? rawTs * 1000 : rawTs) + : null; + + // --- Effect: only re-runs when the boolean or timestamp actually change --- + useEffect(() => { - // Clean up previous worker if any if (workerRef.current) { - if (typeof workerRef.current.postMessage === 'function') { - workerRef.current.postMessage({type: 'stop'}); - } - if (typeof workerRef.current.terminate === 'function') { - workerRef.current.terminate(); - } + workerRef.current.postMessage({type: 'stop'}); + workerRef.current.terminate(); workerRef.current = null; } - // Get holdTimestamp - prioritize consult hold over main call hold - // This ensures the hold timer shows the correct time for whichever call is currently on hold - const consultHoldTs = currentTask?.data?.interaction - ? findHoldTimestamp(currentTask.data.interaction, 'consult') - : null; - const mainCallHoldTs = currentTask?.data?.interaction - ? findHoldTimestamp(currentTask.data.interaction, 'mainCall') - : null; - - // Use consult hold timestamp if available, otherwise use main call hold timestamp - const activeHoldTimestamp = consultHoldTs || mainCallHoldTs; - - if (activeHoldTimestamp) { - const holdTimeMs = activeHoldTimestamp < 10000000000 ? activeHoldTimestamp * 1000 : activeHoldTimestamp; - const blob = new Blob([HOLD_TIMER_WORKER_SCRIPT], {type: 'application/javascript'}); - const workerUrl = URL.createObjectURL(blob); - workerRef.current = new Worker(workerUrl); - - // Set initial holdTime immediately for instant UI update - setHoldTime(Math.floor((Date.now() - holdTimeMs) / 1000)); - - workerRef.current.onmessage = (e) => { - if (e.data.type === 'elapsedTime') setHoldTime(e.data.elapsed); - if (e.data.type === 'stop') setHoldTime(0); - }; - - workerRef.current.postMessage({type: 'start', eventTime: holdTimeMs}); - } else { + if (!mainCallOnHold) { setHoldTime(0); + return; } - // Cleanup on unmount or when dependencies change + // Use real backend timestamp when available, otherwise Date.now() so the + // timer starts immediately (backend AgentContactHeld arrives ~100-200ms + // later and triggers a re-run via the holdTimestampMs dependency). + const eventTime = holdTimestampMs || Date.now(); + + const blob = new Blob([HOLD_TIMER_WORKER_SCRIPT], {type: 'application/javascript'}); + const workerUrl = URL.createObjectURL(blob); + workerRef.current = new Worker(workerUrl); + + setHoldTime(Math.floor((Date.now() - eventTime) / 1000)); + + workerRef.current.onmessage = (e) => { + if (e.data.type === 'elapsedTime') setHoldTime(e.data.elapsed); + if (e.data.type === 'stop') setHoldTime(0); + }; + + workerRef.current.postMessage({type: 'start', eventTime}); + return () => { if (workerRef.current) { workerRef.current.postMessage({type: 'stop'}); @@ -83,7 +103,7 @@ export const useHoldTimer = (currentTask: ITask | null): number => { workerRef.current = null; } }; - }, [currentTask]); + }, [mainCallOnHold, holdTimestampMs]); return holdTime; }; diff --git a/packages/contact-center/task/src/helper.ts b/packages/contact-center/task/src/helper.ts index c3e2b6129..f48ec7d65 100644 --- a/packages/contact-center/task/src/helper.ts +++ b/packages/contact-center/task/src/helper.ts @@ -1,5 +1,11 @@ -import {useEffect, useCallback, useState, useMemo} from 'react'; -import {AddressBookEntriesResponse, AddressBookEntrySearchParams, ITask} from '@webex/contact-center'; +import {useEffect, useCallback, useState, useMemo, useRef} from 'react'; +import { + AddressBookEntriesResponse, + AddressBookEntrySearchParams, + ITask, + TaskUIControls, + getDefaultUIControls, +} from '@webex/contact-center'; import { useCallControlProps, UseTaskListProps, @@ -16,11 +22,11 @@ import store, { getConferenceParticipants, Participant, findMediaResourceId, + isInteractionOnHold, MEDIA_TYPE_TELEPHONY_LOWER, } from '@webex/cc-store'; -import {getControlsVisibility} from './Utils/task-util'; -import {TIMER_LABEL_CONSULTING} from './Utils/constants'; -import {calculateStateTimerData, calculateConsultTimerData} from './Utils/timer-utils'; +import {TIMER_LABEL_CONSULTING, TIMER_LABEL_CONSULT_REQUESTED, TIMER_LABEL_CONSULT_ON_HOLD, TIMER_LABEL_WRAP_UP} from './Utils/constants'; +import {calculateStateTimerData, calculateConsultTimerData, findLatestConsultMedia} from './Utils/timer-utils'; import {useHoldTimer} from './Utils/useHoldTimer'; import {OutdialAniEntriesResponse} from '@webex/contact-center/dist/types/services/config/types'; @@ -29,8 +35,7 @@ const ENGAGED_USERNAME = 'Engaged'; // Hook for managing the task list export const useTaskList = (props: UseTaskListProps) => { - const {deviceType, onTaskAccepted, onTaskDeclined, onTaskSelected, logger, taskList} = props; - const isBrowser = deviceType === 'BROWSER'; + const {onTaskAccepted, onTaskDeclined, onTaskSelected, logger, taskList} = props; const logError = (message: string, method: string) => { logger.error(message, { @@ -143,15 +148,20 @@ export const useTaskList = (props: UseTaskListProps) => { } }; - return {taskList, acceptTask, declineTask, onTaskSelect, isBrowser}; + return {taskList, acceptTask, declineTask, onTaskSelect}; }; export const useIncomingTask = (props: UseTaskProps) => { - const {onAccepted, onRejected, deviceType, incomingTask, logger} = props; - const isBrowser = deviceType === 'BROWSER'; - const isDeclineButtonEnabled = store.isDeclineButtonEnabled; + const {onAccepted, onRejected, incomingTask, logger} = props; + + const acceptControl = incomingTask?.uiControls?.main?.accept ?? {isVisible: false, isEnabled: false}; + const sdkDeclineControl = incomingTask?.uiControls?.main?.decline ?? {isVisible: false, isEnabled: false}; + const declineControl = { + ...sdkDeclineControl, + isEnabled: sdkDeclineControl.isEnabled || store.isDeclineButtonEnabled, + }; - const taskAssignCallback = () => { + const taskAssignCallback = useCallback(() => { try { if (onAccepted) onAccepted({task: incomingTask}); } catch (error) { @@ -160,9 +170,9 @@ export const useIncomingTask = (props: UseTaskProps) => { method: 'taskAssignCallback', }); } - }; + }, [onAccepted, incomingTask, logger]); - const taskRejectCallback = () => { + const taskRejectCallback = useCallback(() => { try { if (onRejected) onRejected({task: incomingTask}); } catch (error) { @@ -171,25 +181,12 @@ export const useIncomingTask = (props: UseTaskProps) => { method: 'taskRejectCallback', }); } - }; + }, [onRejected, incomingTask, logger]); useEffect(() => { try { if (!incomingTask) return; - store.setTaskCallback( - TASK_EVENTS.TASK_ASSIGNED, - () => { - try { - if (onAccepted) onAccepted({task: incomingTask}); - } catch (error) { - logger?.error(`CC-Widgets: Task: Error in TASK_ASSIGNED callback - ${error.message}`, { - module: 'useIncomingTask', - method: 'TASK_ASSIGNED_callback', - }); - } - }, - incomingTask.data.interactionId - ); + store.setTaskCallback(TASK_EVENTS.TASK_ASSIGNED, taskAssignCallback, incomingTask.data.interactionId); store.setTaskCallback(TASK_EVENTS.TASK_CONSULT_ACCEPTED, taskAssignCallback, incomingTask?.data.interactionId); store.setTaskCallback(TASK_EVENTS.TASK_END, taskRejectCallback, incomingTask?.data.interactionId); store.setTaskCallback(TASK_EVENTS.TASK_REJECT, taskRejectCallback, incomingTask?.data.interactionId); @@ -219,7 +216,7 @@ export const useIncomingTask = (props: UseTaskProps) => { method: 'useEffect', }); } - }, [incomingTask]); + }, [incomingTask, taskAssignCallback, taskRejectCallback]); const logError = (message: string, method: string) => { logger.error(message, { @@ -276,8 +273,8 @@ export const useIncomingTask = (props: UseTaskProps) => { incomingTask, accept, reject, - isBrowser, - isDeclineButtonEnabled, + acceptControl, + declineControl, }; }; @@ -290,13 +287,13 @@ export const useCallControl = (props: useCallControlProps) => { onRecordingToggle, onToggleMute, logger, - deviceType, - featureFlags, isMuted, - conferenceEnabled, agentId, + conferenceEnabled = true, } = props; const [isRecording, setIsRecording] = useState(true); + const [controls, setControls] = useState(currentTask?.uiControls ?? getDefaultUIControls()); + const [isHeld, setIsHeld] = useState(() => (currentTask ? isInteractionOnHold(currentTask) : false)); const [buddyAgents, setBuddyAgents] = useState([]); const [loadingBuddyAgents, setLoadingBuddyAgents] = useState(false); const [consultAgentName, setConsultAgentName] = useState('Consult Agent'); @@ -310,18 +307,60 @@ export const useCallControl = (props: useCallControlProps) => { // Consult timer labels and timestamps const [consultTimerLabel, setConsultTimerLabel] = useState(TIMER_LABEL_CONSULTING); const [consultTimerTimestamp, setConsultTimerTimestamp] = useState(0); + const initialControls = currentTask?.uiControls; + const prevIsConsultingRef = useRef( + !!(initialControls?.consult?.endConsult?.isVisible || initialControls?.main?.endConsult?.isVisible) + ); const [lastTargetType, setLastTargetType] = useState(TARGET_TYPE.AGENT); const [conferenceParticipants, setConferenceParticipants] = useState([]); + const lastWrapupAuxCodeIdRef = useRef(null); + + // Subscribe to SDK-computed UI control updates + useEffect(() => { + if (!currentTask) { + setControls(getDefaultUIControls()); + return; + } + setControls(currentTask.uiControls ?? getDefaultUIControls()); + const onControlsUpdated = (updatedControls: TaskUIControls) => { + setControls(updatedControls); + }; + currentTask.on(TASK_EVENTS.TASK_UI_CONTROLS_UPDATED, onControlsUpdated); + return () => { + currentTask.off(TASK_EVENTS.TASK_UI_CONTROLS_UPDATED, onControlsUpdated); + }; + }, [currentTask]); + + useEffect(() => { + // During conference, the call is never on hold + const isInConference = + controls?.main?.exitConference?.isVisible || + currentTask?.data?.interaction?.state === 'conference'; + if (isInConference) { + setIsHeld(false); + return; + } + // During consulting, derive hold state from activeLeg (set synchronously + // by the SDK on switch). Raw media data has a timing gap — the backend + // hold/unhold response arrives after the switch event, so media.isHold + // is stale at the time the controls update. + const isConsulting = controls?.consult?.endConsult?.isVisible || controls?.main?.endConsult?.isVisible; + if (isConsulting) { + setIsHeld(controls?.activeLeg === 'consult'); + } else { + setIsHeld(currentTask ? isInteractionOnHold(currentTask) : false); + } + }, [currentTask, controls]); // Use custom hook for hold timer management - const holdTime = useHoldTimer(currentTask); + const holdTime = useHoldTimer(currentTask, controls); useEffect(() => { if (currentTask && store?.cc?.agentConfig?.agentId) { const participants = getConferenceParticipants(currentTask, store.cc.agentConfig.agentId); setConferenceParticipants(participants); } - }, [currentTask]); + }, [currentTask, controls]); // Function to extract consulting agent information const extractConsultingAgent = useCallback(() => { try { @@ -393,8 +432,7 @@ export const useCallControl = (props: useCallControlProps) => { } else { // Fallback: Use old logic if consult media not found const otherAgents = Object.values(interaction.participants || {}).filter( - (participant): participant is Participant => - (participant as Participant).pType === 'Agent' && (participant as Participant).id !== myAgentId + (participant) => participant.pType === 'Agent' && participant.id !== myAgentId ); // In a conference with multiple agents, find the agent currently being consulted @@ -530,6 +568,7 @@ export const useCallControl = (props: useCallControlProps) => { const holdCallback = () => { try { + setIsHeld(true); if (onHoldResume) { onHoldResume({ isHeld: true, @@ -546,6 +585,7 @@ export const useCallControl = (props: useCallControlProps) => { const resumeCallback = () => { try { + setIsHeld(false); if (onHoldResume) { onHoldResume({ isHeld: false, @@ -575,14 +615,16 @@ export const useCallControl = (props: useCallControlProps) => { } }; - const wrapupCallCallback = ({wrapUpAuxCodeId}) => { + const wrapupCallCallback = () => { try { - const wrapUpReason = store.wrapupCodes.find((code) => code.id === wrapUpAuxCodeId)?.name; - if (onWrapUp) { - onWrapUp({ - task: currentTask, - wrapUpReason: wrapUpReason, - }); + if (lastWrapupAuxCodeIdRef.current) { + const wrapUpReason = store.wrapupCodes.find((code) => code.id === lastWrapupAuxCodeIdRef.current)?.name; + if (onWrapUp) { + onWrapUp({ + task: currentTask, + wrapUpReason: wrapUpReason, + }); + } } } catch (error) { logger?.error(`CC-Widgets: Task: Error in wrapupCallCallback - ${error.message}`, { @@ -639,7 +681,8 @@ export const useCallControl = (props: useCallControlProps) => { ); store.setTaskCallback(TASK_EVENTS.TASK_RESUME, resumeCallback, interactionId); store.setTaskCallback(TASK_EVENTS.TASK_END, endCallCallback, interactionId); - store.setTaskCallback(TASK_EVENTS.AGENT_WRAPPEDUP, wrapupCallCallback, interactionId); + store.setTaskCallback(TASK_EVENTS.TASK_WRAPUP, endCallCallback, interactionId); // Also call onEnd when entering wrapup + store.setTaskCallback(TASK_EVENTS.TASK_WRAPPEDUP, wrapupCallCallback, interactionId); store.setTaskCallback(TASK_EVENTS.TASK_RECORDING_PAUSED, pauseRecordingCallback, interactionId); store.setTaskCallback(TASK_EVENTS.TASK_RECORDING_RESUMED, resumeRecordingCallback, interactionId); @@ -647,9 +690,10 @@ export const useCallControl = (props: useCallControlProps) => { store.removeTaskCallback(TASK_EVENTS.TASK_HOLD, holdCallback, interactionId); store.removeTaskCallback(TASK_EVENTS.TASK_RESUME, resumeCallback, interactionId); store.removeTaskCallback(TASK_EVENTS.TASK_END, endCallCallback, interactionId); - store.removeTaskCallback(TASK_EVENTS.AGENT_WRAPPEDUP, wrapupCallCallback, interactionId); - store.removeTaskCallback(TASK_EVENTS.CONTACT_RECORDING_PAUSED, pauseRecordingCallback, interactionId); - store.removeTaskCallback(TASK_EVENTS.CONTACT_RECORDING_RESUMED, resumeRecordingCallback, interactionId); + store.removeTaskCallback(TASK_EVENTS.TASK_WRAPUP, endCallCallback, interactionId); + store.removeTaskCallback(TASK_EVENTS.TASK_WRAPPEDUP, wrapupCallCallback, interactionId); + store.removeTaskCallback(TASK_EVENTS.TASK_RECORDING_PAUSED, pauseRecordingCallback, interactionId); + store.removeTaskCallback(TASK_EVENTS.TASK_RECORDING_RESUMED, resumeRecordingCallback, interactionId); }; }, [currentTask]); @@ -701,8 +745,7 @@ export const useCallControl = (props: useCallControlProps) => { const toggleMute = async () => { try { - console.log('Mute control not available', controlVisibility); - if (!controlVisibility?.muteUnmute) { + if (!controls?.main?.mute?.isVisible) { logger.warn('Mute control not available', {module: 'useCallControl', method: 'toggleMute'}); return; } @@ -760,6 +803,9 @@ export const useCallControl = (props: useCallControlProps) => { const wrapupCall = (wrapUpReason: string, auxCodeId: string) => { try { + // Store auxCodeId for use in wrapupCallCallback + lastWrapupAuxCodeIdRef.current = auxCodeId; + currentTask .wrapup({wrapUpReason: wrapUpReason, auxCodeId: auxCodeId}) .then(() => { @@ -808,7 +854,7 @@ export const useCallControl = (props: useCallControlProps) => { const switchToMainCall = async () => { try { - await currentTask.resume(findMediaResourceId(currentTask, 'consult')); + await currentTask.switchCall(); logger.info('switchToMainCall success', {module: 'useCallControl', method: 'switchToMainCall'}); } catch (error) { logger.error(`Error switchToMainCall: ${error}`, {module: 'useCallControl', method: 'switchToMainCall'}); @@ -818,7 +864,7 @@ export const useCallControl = (props: useCallControlProps) => { const switchToConsult = async () => { try { - await currentTask.hold(findMediaResourceId(currentTask, 'mainCall')); + await currentTask.switchCall(); logger.info('switchToConsult success', {module: 'useCallControl', method: 'switchToConsult'}); } catch (error) { logger.error(`Error switching to consult: ${error}`, {module: 'useCallControl', method: 'switchToConsult'}); @@ -847,6 +893,8 @@ export const useCallControl = (props: useCallControlProps) => { holdParticipants: !allowParticipantsToInteract, }; + store.setLastConsultDestination({to: consultDestination, destinationType}); + if (destinationType === 'queue') { store.setIsQueueConsultInProgress(true); store.setCurrentConsultQueueId(consultDestination); @@ -883,8 +931,10 @@ export const useCallControl = (props: useCallControlProps) => { try { await currentTask.endConsult(consultEndPayload); } catch (error) { - logError(`Error ending consult call: ${error}`, 'endConsultCall'); - throw error; + // Log error but don't throw - SDK retry mechanism will handle timing issues + // If endConsult fails due to backend timing (called before CONSULTING_ACTIVE), + // the SDK's requestEndConsultRetry will automatically retry when ready + logError(`Error ending consult call (will retry automatically): ${error}`, 'endConsultCall'); } }; @@ -895,15 +945,61 @@ export const useCallControl = (props: useCallControlProps) => { } try { - if (currentTask.data.isConferenceInProgress) { + const currentState = currentTask.state?.value; + const isCurrentlyConsulting = currentState === 'CONSULTING'; + + if (!isCurrentlyConsulting && currentTask.data.isConferenceInProgress) { logger.info('Conference in progress, using transferConference', { module: 'useCallControl', - method: 'transferCall', + method: 'consultTransfer', }); await currentTask.transferConference(); } else { + let destination = store.lastConsultDestination; + + if (!destination?.to) { + // After page refresh, lastConsultDestination is lost (in-memory only). + // Recover the transfer target from the consult media's participants. + const myAgentId = store.cc.agentConfig?.agentId; + const {interaction} = currentTask.data; + const consultMediaId = findMediaResourceId(currentTask, 'consult'); + const consultMedia = consultMediaId ? interaction?.media?.[consultMediaId] : null; + + let recoveredTo: string | null = null; + let recoveredDestinationType: DestinationType = 'agent' as DestinationType; + if (consultMedia?.participants) { + for (const pid of consultMedia.participants) { + const p = interaction?.participants?.[pid] as any; + if (!p || p.id === myAgentId) continue; + if (p.pType === 'Agent') { + recoveredTo = pid; + recoveredDestinationType = 'agent' as DestinationType; + break; + } + if (p.pType === 'EP-DN' && p.epId) { + recoveredTo = p.epId; + recoveredDestinationType = 'entryPoint' as DestinationType; + break; + } + } + } + + if (recoveredTo) { + destination = {to: recoveredTo, destinationType: recoveredDestinationType}; + logger.info(`Recovered consult destination from interaction data: ${recoveredTo}`, { + module: 'useCallControl', + method: 'consultTransfer', + }); + } + } + + if (!destination?.to) { + logError('Cannot transfer: consult destination not found', 'consultTransfer'); + return; + } + logger.info('Consult transfer initiated', {module: 'useCallControl', method: 'consultTransfer'}); - await currentTask.consultTransfer(); + await currentTask.transfer(destination); } } catch (error) { logError(`Error transferring consult call: ${error}`, 'consultTransfer'); @@ -927,22 +1023,39 @@ export const useCallControl = (props: useCallControlProps) => { currentTask.cancelAutoWrapupTimer(); }; - const controlVisibility = useMemo( - () => getControlsVisibility(deviceType, featureFlags, currentTask, agentId, conferenceEnabled, logger), - [deviceType, featureFlags, currentTask, agentId, conferenceEnabled, logger] - ); - - // Add useEffect for auto wrap-up timer + // Derive stable primitives from MobX-observed task data so that effects + // re-fire when the backend pushes fresh interaction/participant state — + // not only when controls change. `currentTask` is a MobX proxy whose + // reference never changes, so effects would otherwise miss data-only + // updates. + const _interaction = currentTask?.data?.interaction; + const _participant = _interaction?.participants?.[agentId]; + + // Consult-timer primitives + const _consultMedia = findLatestConsultMedia(_interaction); + const consultMediaIsHold = !!_consultMedia?.isHold; + const consultMediaId = _consultMedia?.mediaResourceId ?? ''; + const participantConsultState = _participant?.consultState ?? null; + + // State-timer (wrap-up / post-call) primitives + const participantIsWrapUp = !!_participant?.isWrapUp; + const participantWrapUpTimestamp = _participant?.wrapUpTimestamp ?? 0; + const participantLastUpdated = _participant?.lastUpdated ?? 0; + const participantCurrentState = _participant?.currentState ?? null; + const interactionState = _interaction?.state ?? null; + + // Auto wrap-up timer. + // `currentTask.autoWrapup` must remain a useEffect dependency so that when + // the SDK sets it (after the initial wrapup render), React detects the + // change on the next re-render and re-fires the effect. useEffect(() => { - let timerId: NodeJS.Timeout; + let timerId: ReturnType; - if (currentTask?.autoWrapup && controlVisibility?.wrapup) { + if (currentTask?.autoWrapup && controls?.main?.wrapup) { try { - // Initialize time left from the autoWrapup object const initialTimeLeft = currentTask.autoWrapup.getTimeLeftSeconds(); setsecondsUntilAutoWrapup(initialTimeLeft); - // Update timer every second timerId = setInterval(() => { setsecondsUntilAutoWrapup((prevTime) => { if (prevTime && prevTime > 0) { @@ -960,31 +1073,72 @@ export const useCallControl = (props: useCallControlProps) => { } } - // Clear the interval when component unmounts or when auto wrap-up is no longer active return () => { if (timerId) { clearInterval(timerId); } }; - }, [currentTask?.autoWrapup, controlVisibility?.wrapup]); - - // Calculate state timer label and timestamp using utils - // Priority: Wrap Up > Post Call + }, [currentTask?.autoWrapup, controls?.main?.wrapup]); + + // Calculate state timer label and timestamp (Wrap Up / Post Call). + // When the SDK sets wrapup controls visible (ContactEnded event), the + // participant data may not yet contain the wrapup timestamp (it arrives + // in the subsequent AgentWrapup event). Bridge this gap by showing the + // "Wrap Up" label immediately with Date.now() as a close approximation; + // the timer auto-corrects when the real timestamp arrives. useEffect(() => { - const stateTimerData = calculateStateTimerData(currentTask, controlVisibility, agentId); - setStateTimerLabel(stateTimerData.label); - setStateTimerTimestamp(stateTimerData.timestamp); - }, [currentTask, controlVisibility, agentId]); - - // Calculate consult timer label and timestamp using utils + const stateTimerData = calculateStateTimerData(currentTask, controls, agentId); + + if (stateTimerData.label && stateTimerData.timestamp) { + setStateTimerLabel(stateTimerData.label); + setStateTimerTimestamp(stateTimerData.timestamp); + } else if (controls?.main?.wrapup?.isVisible) { + setStateTimerLabel(TIMER_LABEL_WRAP_UP); + setStateTimerTimestamp((prev) => prev || Date.now()); + } else { + setStateTimerLabel(stateTimerData.label); + setStateTimerTimestamp(stateTimerData.timestamp); + } + }, [ + currentTask, controls, agentId, + participantIsWrapUp, participantWrapUpTimestamp, participantLastUpdated, + participantCurrentState, interactionState, + ]); + + // Calculate consult timer label and timestamp. + // The calculation relies on consult media's isHold + holdTimestamp as the + // sole source of truth for "Consult on Hold" (same as the next branch). + // + // On hidden→visible transition (new consult starts), stale data from the + // previous flow may produce "Consult on Hold". Override to safe defaults. + // We never early-return — the calculation always runs — so that when data + // is already fresh (e.g., Agent 1 accepts a consult and data says + // "Consulting"), the correct label is applied immediately. useEffect(() => { - const consultTimerData = calculateConsultTimerData(currentTask, controlVisibility, agentId); - setConsultTimerLabel(consultTimerData.label); - setConsultTimerTimestamp(consultTimerData.timestamp); - }, [currentTask, controlVisibility, agentId]); + const isConsulting = controls?.consult?.endConsult?.isVisible || controls?.main?.endConsult?.isVisible; + const wasConsulting = prevIsConsultingRef.current; + prevIsConsultingRef.current = !!isConsulting; + + const consultTimerData = calculateConsultTimerData(currentTask, controls, agentId); + const justBecameConsulting = isConsulting && !wasConsulting; + + if (justBecameConsulting && consultTimerData.label === TIMER_LABEL_CONSULT_ON_HOLD) { + setConsultTimerLabel(TIMER_LABEL_CONSULT_REQUESTED); + setConsultTimerTimestamp(0); + } else { + setConsultTimerLabel(consultTimerData.label); + setConsultTimerTimestamp(consultTimerData.timestamp); + } + }, [currentTask, controls, agentId, consultMediaIsHold, consultMediaId, participantConsultState]); + + const isInConferenceState = + controls?.main?.exitConference?.isVisible || + currentTask?.data?.interaction?.state === 'conference'; + const effectiveIsHeld = isInConferenceState ? false : isHeld; return { currentTask, + isHeld: effectiveIsHeld, endCall, toggleHold, toggleRecording, @@ -1014,7 +1168,8 @@ export const useCallControl = (props: useCallControlProps) => { consultTimerTimestamp, lastTargetType, setLastTargetType, - controlVisibility, + controls, + conferenceEnabled, secondsUntilAutoWrapup, cancelAutoWrapup, conferenceParticipants, diff --git a/packages/contact-center/task/src/task.types.ts b/packages/contact-center/task/src/task.types.ts index c0c759382..446b9d965 100644 --- a/packages/contact-center/task/src/task.types.ts +++ b/packages/contact-center/task/src/task.types.ts @@ -1,9 +1,9 @@ import {TaskProps, ControlProps, OutdialCallProps} from '@webex/cc-components'; -export type UseTaskProps = Pick & +export type UseTaskProps = Pick & Partial>; -export type UseTaskListProps = Pick & +export type UseTaskListProps = Pick & Partial>; export type IncomingTaskProps = Pick & Partial>; @@ -27,7 +27,7 @@ export type CallControlProps = Partial< export type useCallControlProps = Pick< ControlProps, - 'currentTask' | 'logger' | 'deviceType' | 'featureFlags' | 'isMuted' | 'conferenceEnabled' | 'agentId' + 'currentTask' | 'logger' | 'isMuted' | 'conferenceEnabled' | 'agentId' > & Partial>; @@ -40,15 +40,6 @@ export interface OutdialProps { isAddressBookEnabled?: boolean; } -/** - * Helper interface for device type checks - */ -export interface DeviceTypeFlags { - isBrowser: boolean; - isAgentDN: boolean; - isExtension: boolean; -} - /** * Target types for consult/transfer operations */ diff --git a/packages/contact-center/test-fixtures/src/fixtures.ts b/packages/contact-center/test-fixtures/src/fixtures.ts index 643eddf9c..70eebbd2d 100644 --- a/packages/contact-center/test-fixtures/src/fixtures.ts +++ b/packages/contact-center/test-fixtures/src/fixtures.ts @@ -54,7 +54,7 @@ const mockProfile: Profile = { isAgentAvailableAfterOutdial: false, isCampaignManagementEnabled: true, outDialEp: '', - isEndCallEnabled: true, + isEndTaskEnabled: true, isEndConsultEnabled: true, agentDbId: 'agentDb123', allowConsultToQueue: true, @@ -71,7 +71,6 @@ const mockProfile: Profile = { lastStateAuxCodeId: 'auxCodeId', lastStateChangeTimestamp: 123456789, lastIdleCodeChangeTimestamp: 123456789, - environment: 'produs1', }; const mockEntryPointsResponse: EntryPointListResponse = { @@ -111,7 +110,7 @@ const makeMockAddressBook = (getEntriesMock?: AddressBook['getEntries']): Addres const mockAddressBook = makeMockAddressBook(); -const mockTask: ITask = { +const mockTask = { data: { interaction: { mediaType: 'telephony', @@ -120,6 +119,7 @@ const mockTask: ITask = { callProcessingDetails: { relationshipType: 'primary', parentInteractionId: null, + pauseResumeEnabled: true, }, participants: { agent1: { @@ -198,7 +198,7 @@ const mockTask: ITask = { transferConference: jest.fn(), exitConference: jest.fn(), toggleMute: jest.fn(), -}; +} as unknown as ITask; const mockQueueDetails = [ { diff --git a/yarn.lock b/yarn.lock index f3ca3f2b3..0b64ba9fe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9369,6 +9369,24 @@ __metadata: languageName: node linkType: hard +"@webex/calling@npm:3.12.0-task-refactor.1": + version: 3.12.0-task-refactor.1 + resolution: "@webex/calling@npm:3.12.0-task-refactor.1" + dependencies: + "@types/platform": "npm:1.3.4" + "@webex/internal-media-core": "npm:2.20.3" + "@webex/internal-plugin-metrics": "npm:3.12.0-task-refactor.1" + "@webex/media-helpers": "npm:3.12.0-task-refactor.1" + async-mutex: "npm:0.4.0" + buffer: "npm:6.0.3" + jest-html-reporters: "npm:3.0.11" + platform: "npm:1.3.6" + uuid: "npm:8.3.2" + xstate: "npm:4.30.6" + checksum: 10c0/993e8c6bd577d598e32c3ed8246a24762666d6d5fa729c6fadf5b46623632823879f11767d22528bda6f2db02d433e36c92b2a21932083ab071523f0b02f1043 + languageName: node + linkType: hard + "@webex/cc-components@workspace:*, @webex/cc-components@workspace:packages/contact-center/cc-components": version: 0.0.0-use.local resolution: "@webex/cc-components@workspace:packages/contact-center/cc-components" @@ -9526,7 +9544,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.11.0-next.20" + "@webex/contact-center": "npm:3.12.0-task-refactor.2" "@webex/test-fixtures": "workspace:*" babel-jest: "npm:29.7.0" babel-loader: "npm:9.2.1" @@ -9765,6 +9783,13 @@ __metadata: languageName: node linkType: hard +"@webex/common-timers@npm:3.12.0-task-refactor.1": + version: 3.12.0-task-refactor.1 + resolution: "@webex/common-timers@npm:3.12.0-task-refactor.1" + checksum: 10c0/6264f88ebe0a9036b0ec486623e3a0edec50c3211355dc958295e464012326f6572ba5ebe7ee4f492fee9d1e5b0530110d0719a775e17ace5c219c13c40d2b39 + languageName: node + linkType: hard + "@webex/common@npm:1.161.0": version: 1.161.0 resolution: "@webex/common@npm:1.161.0" @@ -9843,6 +9868,21 @@ __metadata: languageName: node linkType: hard +"@webex/common@npm:3.12.0-task-refactor.1": + version: 3.12.0-task-refactor.1 + resolution: "@webex/common@npm:3.12.0-task-refactor.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/9ff3225e48ca105d8455f171e946b72ba4fe94754c00d3022c365ff93c706237e8a839cab9c300c56837138d9a16be17274909500f2bf9651a3af43990ef3bc0 + languageName: node + linkType: hard + "@webex/component-adapter-interfaces@npm:^1.28.0, @webex/component-adapter-interfaces@npm:^1.30.5": version: 1.30.18 resolution: "@webex/component-adapter-interfaces@npm:1.30.18" @@ -9897,6 +9937,26 @@ __metadata: languageName: node linkType: hard +"@webex/contact-center@npm:3.12.0-task-refactor.2": + version: 3.12.0-task-refactor.2 + resolution: "@webex/contact-center@npm:3.12.0-task-refactor.2" + dependencies: + "@types/platform": "npm:1.3.4" + "@webex/calling": "npm:3.12.0-task-refactor.1" + "@webex/internal-plugin-mercury": "npm:3.12.0-task-refactor.1" + "@webex/internal-plugin-metrics": "npm:3.12.0-task-refactor.1" + "@webex/internal-plugin-support": "npm:3.12.0-task-refactor.1" + "@webex/plugin-authorization": "npm:3.12.0-task-refactor.1" + "@webex/plugin-logger": "npm:3.12.0-task-refactor.1" + "@webex/webex-core": "npm:3.12.0-task-refactor.1" + jest-html-reporters: "npm:3.0.11" + lodash: "npm:^4.17.21" + uuid: "npm:^3.3.2" + xstate: "npm:5.24.0" + checksum: 10c0/b27f63bb5c7629d5ab1e4ec4cf3623c09b860dbf25b83ef57592c59d3e5c6aa7520a9c99bdfb15b6a181770db48dd97d4ac0c69a9a4ff62c5ba32b6bbe528967 + languageName: node + linkType: hard + "@webex/event-dictionary-ts@npm:^1.0.1930": version: 1.0.2091 resolution: "@webex/event-dictionary-ts@npm:1.0.2091" @@ -9946,6 +10006,15 @@ __metadata: languageName: node linkType: hard +"@webex/helper-html@npm:3.12.0-task-refactor.1": + version: 3.12.0-task-refactor.1 + resolution: "@webex/helper-html@npm:3.12.0-task-refactor.1" + dependencies: + lodash: "npm:^4.17.21" + checksum: 10c0/71c134f6eb34be5beb64d19bd5248ec6529f0c266c3b7c787e4811a754196da9c972aa0e240e25ff8be54f21a540734d01424bdff648609a67d82b0a1ac15aff + languageName: node + linkType: hard + "@webex/helper-image@npm:2.60.2": version: 2.60.2 resolution: "@webex/helper-image@npm:2.60.2" @@ -10014,6 +10083,23 @@ __metadata: languageName: node linkType: hard +"@webex/helper-image@npm:3.12.0-task-refactor.1": + version: 3.12.0-task-refactor.1 + resolution: "@webex/helper-image@npm:3.12.0-task-refactor.1" + dependencies: + "@webex/http-core": "npm:3.12.0-task-refactor.1" + "@webex/test-helper-chai": "npm:3.12.0-task-refactor.1" + "@webex/test-helper-file": "npm:3.12.0-task-refactor.1" + "@webex/test-helper-mocha": "npm:3.12.0-task-refactor.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/05eec930d3c2d7798a97f831a4c71b82712e56382ca32d595ef90f3f5a44c8460331df6f566841f96c9f77e6cf7d1838b86d4c82ff5e8a3bd30dc44ba82a3718 + languageName: node + linkType: hard + "@webex/http-core@npm:1.161.0": version: 1.161.0 resolution: "@webex/http-core@npm:1.161.0" @@ -10112,6 +10198,24 @@ __metadata: languageName: node linkType: hard +"@webex/http-core@npm:3.12.0-task-refactor.1": + version: 3.12.0-task-refactor.1 + resolution: "@webex/http-core@npm:3.12.0-task-refactor.1" + dependencies: + "@webex/common": "npm:3.12.0-task-refactor.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/3c99e0f1f4338b63118c35573c663435c9ed22d722ac05d21b9340d7864ebc83e3919dbfce997b3b4b075519427fdbd78815707bb9a07b76374c55509260b5bc + languageName: node + linkType: hard + "@webex/internal-media-core@npm:0.0.7-beta": version: 0.0.7-beta resolution: "@webex/internal-media-core@npm:0.0.7-beta" @@ -10129,6 +10233,26 @@ __metadata: languageName: node linkType: hard +"@webex/internal-media-core@npm:2.20.3": + version: 2.20.3 + resolution: "@webex/internal-media-core@npm:2.20.3" + 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.7.1" + "@webex/web-client-media-engine": "npm:3.35.2" + 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/d732c404420613e49a53b2ad764d56d81e1f8a3d126ee3516c8b163e93b4c683d1dcc84666079523d9d4c389c186c7b95efbdad9a6aae6a614cce0e6380e91dc + languageName: node + linkType: hard + "@webex/internal-media-core@npm:2.22.1": version: 2.22.1 resolution: "@webex/internal-media-core@npm:2.22.1" @@ -10263,6 +10387,24 @@ __metadata: languageName: node linkType: hard +"@webex/internal-plugin-conversation@npm:3.12.0-task-refactor.1": + version: 3.12.0-task-refactor.1 + resolution: "@webex/internal-plugin-conversation@npm:3.12.0-task-refactor.1" + dependencies: + "@webex/common": "npm:3.12.0-task-refactor.1" + "@webex/helper-html": "npm:3.12.0-task-refactor.1" + "@webex/helper-image": "npm:3.12.0-task-refactor.1" + "@webex/internal-plugin-encryption": "npm:3.12.0-task-refactor.1" + "@webex/internal-plugin-user": "npm:3.12.0-task-refactor.1" + "@webex/webex-core": "npm:3.12.0-task-refactor.1" + crypto-js: "npm:^4.1.1" + lodash: "npm:^4.17.21" + node-scr: "npm:^0.3.0" + uuid: "npm:^3.3.2" + checksum: 10c0/97229f150a126d488e903b5ba454cc2f1f603bc3305ca5d7851c0a9aca3cc7c65184defab16271db80bb641633fae960c29eef18ca7928cbc9097cb0f766fe19 + languageName: node + linkType: hard + "@webex/internal-plugin-device@npm:2.60.2": version: 2.60.2 resolution: "@webex/internal-plugin-device@npm:2.60.2" @@ -10329,6 +10471,23 @@ __metadata: languageName: node linkType: hard +"@webex/internal-plugin-device@npm:3.12.0-task-refactor.1": + version: 3.12.0-task-refactor.1 + resolution: "@webex/internal-plugin-device@npm:3.12.0-task-refactor.1" + dependencies: + "@webex/common": "npm:3.12.0-task-refactor.1" + "@webex/common-timers": "npm:3.12.0-task-refactor.1" + "@webex/http-core": "npm:3.12.0-task-refactor.1" + "@webex/internal-plugin-metrics": "npm:3.12.0-task-refactor.1" + "@webex/webex-core": "npm:3.12.0-task-refactor.1" + ampersand-collection: "npm:^2.0.2" + ampersand-state: "npm:^5.0.3" + lodash: "npm:^4.17.21" + uuid: "npm:^3.3.2" + checksum: 10c0/3551aae993f732990d4f8736854bbf3f9608184763cfc8a56b9ce2d6b3609f78001aae26a1b5577ddde2697fa4f74aa83dae062275a0269f944ed2d285fd2268 + languageName: node + linkType: hard + "@webex/internal-plugin-dss@npm:3.11.0-next.9": version: 3.11.0-next.9 resolution: "@webex/internal-plugin-dss@npm:3.11.0-next.9" @@ -10447,6 +10606,32 @@ __metadata: languageName: node linkType: hard +"@webex/internal-plugin-encryption@npm:3.12.0-task-refactor.1": + version: 3.12.0-task-refactor.1 + resolution: "@webex/internal-plugin-encryption@npm:3.12.0-task-refactor.1" + dependencies: + "@webex/common": "npm:3.12.0-task-refactor.1" + "@webex/common-timers": "npm:3.12.0-task-refactor.1" + "@webex/http-core": "npm:3.12.0-task-refactor.1" + "@webex/internal-plugin-device": "npm:3.12.0-task-refactor.1" + "@webex/internal-plugin-mercury": "npm:3.12.0-task-refactor.1" + "@webex/test-helper-file": "npm:3.12.0-task-refactor.1" + "@webex/webex-core": "npm:3.12.0-task-refactor.1" + 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/aba93b91055d2cf9fb29b1f98498bcb16d6df9f7573454a5f8c017069c02f5b9f03c9dc46194a78eac495927e7e70a3feeb63e3d6d57dc89db9daa3c52318610 + languageName: node + linkType: hard + "@webex/internal-plugin-feature@npm:2.60.2": version: 2.60.2 resolution: "@webex/internal-plugin-feature@npm:2.60.2" @@ -10493,6 +10678,17 @@ __metadata: languageName: node linkType: hard +"@webex/internal-plugin-feature@npm:3.12.0-task-refactor.1": + version: 3.12.0-task-refactor.1 + resolution: "@webex/internal-plugin-feature@npm:3.12.0-task-refactor.1" + dependencies: + "@webex/internal-plugin-device": "npm:3.12.0-task-refactor.1" + "@webex/webex-core": "npm:3.12.0-task-refactor.1" + lodash: "npm:^4.17.21" + checksum: 10c0/1a5f7a45b9db54d4be502b2550806a3b5141c54d92fec3aa72d2b1df2012555185b23ed1ff5f611038f065dfb344ea19cb62db481a4847bba7aee3c824f8c94f + languageName: node + linkType: hard + "@webex/internal-plugin-llm@npm:3.11.0-next.9": version: 3.11.0-next.9 resolution: "@webex/internal-plugin-llm@npm:3.11.0-next.9" @@ -10691,6 +10887,30 @@ __metadata: languageName: node linkType: hard +"@webex/internal-plugin-mercury@npm:3.12.0-task-refactor.1": + version: 3.12.0-task-refactor.1 + resolution: "@webex/internal-plugin-mercury@npm:3.12.0-task-refactor.1" + dependencies: + "@webex/common": "npm:3.12.0-task-refactor.1" + "@webex/common-timers": "npm:3.12.0-task-refactor.1" + "@webex/internal-plugin-device": "npm:3.12.0-task-refactor.1" + "@webex/internal-plugin-feature": "npm:3.12.0-task-refactor.1" + "@webex/internal-plugin-metrics": "npm:3.12.0-task-refactor.1" + "@webex/test-helper-chai": "npm:3.12.0-task-refactor.1" + "@webex/test-helper-mocha": "npm:3.12.0-task-refactor.1" + "@webex/test-helper-mock-web-socket": "npm:3.12.0-task-refactor.1" + "@webex/test-helper-mock-webex": "npm:3.12.0-task-refactor.1" + "@webex/test-helper-refresh-callback": "npm:3.12.0-task-refactor.1" + "@webex/test-helper-test-users": "npm:3.12.0-task-refactor.1" + "@webex/webex-core": "npm:3.12.0-task-refactor.1" + backoff: "npm:^2.5.0" + lodash: "npm:^4.17.21" + uuid: "npm:^3.3.2" + ws: "npm:^8.17.1" + checksum: 10c0/7c1bdf0800f335c33370b68e4dd85ca9d0102a3fdb19243769a1b5beddf51685145f2a32716f61336804d5b223eb8f09df50b0ad2f2f17a5c628151f35f9db41 + languageName: node + linkType: hard + "@webex/internal-plugin-metrics@npm:2.60.2": version: 2.60.2 resolution: "@webex/internal-plugin-metrics@npm:2.60.2" @@ -10750,6 +10970,23 @@ __metadata: languageName: node linkType: hard +"@webex/internal-plugin-metrics@npm:3.12.0-task-refactor.1": + version: 3.12.0-task-refactor.1 + resolution: "@webex/internal-plugin-metrics@npm:3.12.0-task-refactor.1" + dependencies: + "@webex/common": "npm:3.12.0-task-refactor.1" + "@webex/common-timers": "npm:3.12.0-task-refactor.1" + "@webex/event-dictionary-ts": "npm:^1.0.1930" + "@webex/test-helper-chai": "npm:3.12.0-task-refactor.1" + "@webex/test-helper-mock-webex": "npm:3.12.0-task-refactor.1" + "@webex/webex-core": "npm:3.12.0-task-refactor.1" + ip-anonymize: "npm:^0.1.0" + lodash: "npm:^4.17.21" + uuid: "npm:^3.3.2" + checksum: 10c0/ee87f4fae6ceab5d0c1310be10f26926fa624230a02a7614389cecaa41cb8cfde9eec7928737446b01075cc8a3f7cf6f676b993df54d81d70b3083cef6f4a12e + languageName: node + linkType: hard + "@webex/internal-plugin-presence@npm:2.60.2": version: 2.60.2 resolution: "@webex/internal-plugin-presence@npm:2.60.2" @@ -10843,6 +11080,21 @@ __metadata: languageName: node linkType: hard +"@webex/internal-plugin-search@npm:3.12.0-task-refactor.1": + version: 3.12.0-task-refactor.1 + resolution: "@webex/internal-plugin-search@npm:3.12.0-task-refactor.1" + dependencies: + "@webex/common": "npm:3.12.0-task-refactor.1" + "@webex/internal-plugin-conversation": "npm:3.12.0-task-refactor.1" + "@webex/internal-plugin-device": "npm:3.12.0-task-refactor.1" + "@webex/internal-plugin-encryption": "npm:3.12.0-task-refactor.1" + "@webex/webex-core": "npm:3.12.0-task-refactor.1" + lodash: "npm:^4.17.21" + uuid: "npm:^3.3.2" + checksum: 10c0/03b5b55b06ed3d6105d9555b5d2e0bb2e0f97f4eed27c62cdcfd386019c4213d85d6d845e4084f440c45a44b5c04b4a0d9f48912c61c7af077e93ca421e9abf8 + languageName: node + linkType: hard + "@webex/internal-plugin-support@npm:2.60.2": version: 2.60.2 resolution: "@webex/internal-plugin-support@npm:2.60.2" @@ -10894,6 +11146,23 @@ __metadata: languageName: node linkType: hard +"@webex/internal-plugin-support@npm:3.12.0-task-refactor.1": + version: 3.12.0-task-refactor.1 + resolution: "@webex/internal-plugin-support@npm:3.12.0-task-refactor.1" + dependencies: + "@webex/internal-plugin-device": "npm:3.12.0-task-refactor.1" + "@webex/internal-plugin-search": "npm:3.12.0-task-refactor.1" + "@webex/test-helper-chai": "npm:3.12.0-task-refactor.1" + "@webex/test-helper-file": "npm:3.12.0-task-refactor.1" + "@webex/test-helper-mock-webex": "npm:3.12.0-task-refactor.1" + "@webex/test-helper-test-users": "npm:3.12.0-task-refactor.1" + "@webex/webex-core": "npm:3.12.0-task-refactor.1" + lodash: "npm:^4.17.21" + uuid: "npm:^3.3.2" + checksum: 10c0/14be632f65b3c26ac9609054f98d6da285a11de33b2587dda52329da7c7c1cd098195fa69e9e13e6ef4b8e175c5b1ba55a08c598d7bf7789f4b0ecf26facca3d + languageName: node + linkType: hard + "@webex/internal-plugin-task@npm:^3.11.0-next.8": version: 3.11.0 resolution: "@webex/internal-plugin-task@npm:3.11.0" @@ -10972,6 +11241,22 @@ __metadata: languageName: node linkType: hard +"@webex/internal-plugin-user@npm:3.12.0-task-refactor.1": + version: 3.12.0-task-refactor.1 + resolution: "@webex/internal-plugin-user@npm:3.12.0-task-refactor.1" + dependencies: + "@webex/common": "npm:3.12.0-task-refactor.1" + "@webex/internal-plugin-device": "npm:3.12.0-task-refactor.1" + "@webex/test-helper-chai": "npm:3.12.0-task-refactor.1" + "@webex/test-helper-mock-webex": "npm:3.12.0-task-refactor.1" + "@webex/test-helper-test-users": "npm:3.12.0-task-refactor.1" + "@webex/webex-core": "npm:3.12.0-task-refactor.1" + lodash: "npm:^4.17.21" + uuid: "npm:^3.3.2" + checksum: 10c0/36a097e8e8ffa6e940c2279460b65e7a3e2ac7e89f1c9b73b0e52eef6f17142e65b7a36a4a3de5cdf8afcbd28f50a3b9fbb2c8d052fe889f72f51297a4c19bea + languageName: node + linkType: hard + "@webex/internal-plugin-voicea@npm:3.11.0-next.9": version: 3.11.0-next.9 resolution: "@webex/internal-plugin-voicea@npm:3.11.0-next.9" @@ -11015,6 +11300,17 @@ __metadata: languageName: node linkType: hard +"@webex/media-helpers@npm:3.12.0-task-refactor.1": + version: 3.12.0-task-refactor.1 + resolution: "@webex/media-helpers@npm:3.12.0-task-refactor.1" + dependencies: + "@webex/internal-media-core": "npm:2.20.3" + "@webex/ts-events": "npm:^1.1.0" + "@webex/web-media-effects": "npm:2.32.1" + checksum: 10c0/8f53474fedd9df4686f78b3749cea76ebef8ee665c7e0613a9b190a846d61213b5b96156fbec9a3e8f7fbd2f00ac63fb42bd74ebc3954d40b6b8b0d92971788e + 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" @@ -11133,6 +11429,23 @@ __metadata: languageName: node linkType: hard +"@webex/plugin-authorization-browser@npm:3.12.0-task-refactor.1": + version: 3.12.0-task-refactor.1 + resolution: "@webex/plugin-authorization-browser@npm:3.12.0-task-refactor.1" + dependencies: + "@webex/common": "npm:3.12.0-task-refactor.1" + "@webex/internal-plugin-device": "npm:3.12.0-task-refactor.1" + "@webex/plugin-authorization-node": "npm:3.12.0-task-refactor.1" + "@webex/storage-adapter-local-storage": "npm:3.12.0-task-refactor.1" + "@webex/storage-adapter-spec": "npm:3.12.0-task-refactor.1" + "@webex/webex-core": "npm:3.12.0-task-refactor.1" + jsonwebtoken: "npm:^9.0.2" + lodash: "npm:^4.17.21" + uuid: "npm:^3.3.2" + checksum: 10c0/971ba30e66592018416a7ffb7231f1d089c847d8deae40fe38cdbf7b16f457246525ee5894bb3d289aac6e4f4eae020a98c0027da51458d5bf2553ebea377490 + languageName: node + linkType: hard + "@webex/plugin-authorization-node@npm:2.60.2": version: 2.60.2 resolution: "@webex/plugin-authorization-node@npm:2.60.2" @@ -11172,6 +11485,19 @@ __metadata: languageName: node linkType: hard +"@webex/plugin-authorization-node@npm:3.12.0-task-refactor.1": + version: 3.12.0-task-refactor.1 + resolution: "@webex/plugin-authorization-node@npm:3.12.0-task-refactor.1" + dependencies: + "@webex/common": "npm:3.12.0-task-refactor.1" + "@webex/internal-plugin-device": "npm:3.12.0-task-refactor.1" + "@webex/webex-core": "npm:3.12.0-task-refactor.1" + jsonwebtoken: "npm:^9.0.0" + uuid: "npm:^3.3.2" + checksum: 10c0/df7f42b524edfb3086116f53bae69cabd7301898c6865dae61b42cfb509388407eee5a55764a4d89be2bf36a157e070aac0aed0fceeb59e73725232975b0f172 + languageName: node + linkType: hard + "@webex/plugin-authorization@npm:2.60.2": version: 2.60.2 resolution: "@webex/plugin-authorization@npm:2.60.2" @@ -11202,6 +11528,16 @@ __metadata: languageName: node linkType: hard +"@webex/plugin-authorization@npm:3.12.0-task-refactor.1": + version: 3.12.0-task-refactor.1 + resolution: "@webex/plugin-authorization@npm:3.12.0-task-refactor.1" + dependencies: + "@webex/plugin-authorization-browser": "npm:3.12.0-task-refactor.1" + "@webex/plugin-authorization-node": "npm:3.12.0-task-refactor.1" + checksum: 10c0/0c16b23d762f738fa87a40051f9c25af820e8c6efaa2cfc0ebf73c73adfd50ce35e2f272b6c76010fb12b3dbe1da8ceb9d6577a1b78846afcab8118e6b977c5c + languageName: node + linkType: hard + "@webex/plugin-device-manager@npm:2.60.2": version: 2.60.2 resolution: "@webex/plugin-device-manager@npm:2.60.2" @@ -11308,6 +11644,20 @@ __metadata: languageName: node linkType: hard +"@webex/plugin-logger@npm:3.12.0-task-refactor.1": + version: 3.12.0-task-refactor.1 + resolution: "@webex/plugin-logger@npm:3.12.0-task-refactor.1" + dependencies: + "@webex/common": "npm:3.12.0-task-refactor.1" + "@webex/test-helper-chai": "npm:3.12.0-task-refactor.1" + "@webex/test-helper-mocha": "npm:3.12.0-task-refactor.1" + "@webex/test-helper-mock-webex": "npm:3.12.0-task-refactor.1" + "@webex/webex-core": "npm:3.12.0-task-refactor.1" + lodash: "npm:^4.17.21" + checksum: 10c0/b01f2aad9d15fd6bcb8dbbcd4d13b06b2eacdd6b408e826cdcfe71b2a1490f49b4ab720f8528c6a10fc790719f7020e3e000c3fade0d0ecaf1647f629293d651 + languageName: node + linkType: hard + "@webex/plugin-meetings@npm:2.60.2": version: 2.60.2 resolution: "@webex/plugin-meetings@npm:2.60.2" @@ -11776,6 +12126,17 @@ __metadata: languageName: node linkType: hard +"@webex/storage-adapter-local-storage@npm:3.12.0-task-refactor.1": + version: 3.12.0-task-refactor.1 + resolution: "@webex/storage-adapter-local-storage@npm:3.12.0-task-refactor.1" + dependencies: + "@webex/storage-adapter-spec": "npm:3.12.0-task-refactor.1" + "@webex/test-helper-mocha": "npm:3.12.0-task-refactor.1" + "@webex/webex-core": "npm:3.12.0-task-refactor.1" + checksum: 10c0/ea4563d5584c04d155bdad72772762d45782354ca5ddb9c8cf6ed92dc600becdea9c48f4a12dc28d43a98a2ca2b5659db494408d2b51da93bbade65b920ac3a6 + languageName: node + linkType: hard + "@webex/storage-adapter-spec@npm:2.60.2": version: 2.60.2 resolution: "@webex/storage-adapter-spec@npm:2.60.2" @@ -11812,6 +12173,15 @@ __metadata: languageName: node linkType: hard +"@webex/storage-adapter-spec@npm:3.12.0-task-refactor.1": + version: 3.12.0-task-refactor.1 + resolution: "@webex/storage-adapter-spec@npm:3.12.0-task-refactor.1" + dependencies: + "@webex/test-helper-chai": "npm:3.12.0-task-refactor.1" + checksum: 10c0/70a0c1aae71c21f697b54cdaf6996dcfe5532915b7ee74fa6ec9098ab910f1290809d7a49964f08edb0e92efa6f5ed28e36ae0a7e64436e04b4bf5484f3ad6a4 + languageName: node + linkType: hard + "@webex/test-fixtures@workspace:*, @webex/test-fixtures@workspace:packages/contact-center/test-fixtures": version: 0.0.0-use.local resolution: "@webex/test-fixtures@workspace:packages/contact-center/test-fixtures" @@ -11887,6 +12257,17 @@ __metadata: languageName: node linkType: hard +"@webex/test-helper-chai@npm:3.12.0-task-refactor.1": + version: 3.12.0-task-refactor.1 + resolution: "@webex/test-helper-chai@npm:3.12.0-task-refactor.1" + dependencies: + "@webex/test-helper-file": "npm:3.12.0-task-refactor.1" + check-error: "npm:^1.0.2" + lodash: "npm:^4.17.21" + checksum: 10c0/8a08b35202b983304759b05f75563dbf4ec91b25a8f80dd5aa755d79424107ecd6d4f5da8dae054d9b53bfd2a8941f3e75d9cbf39f7434b43e4bb596509676a7 + languageName: node + linkType: hard + "@webex/test-helper-file@npm:2.60.2": version: 2.60.2 resolution: "@webex/test-helper-file@npm:2.60.2" @@ -11939,6 +12320,19 @@ __metadata: languageName: node linkType: hard +"@webex/test-helper-file@npm:3.12.0-task-refactor.1": + version: 3.12.0-task-refactor.1 + resolution: "@webex/test-helper-file@npm:3.12.0-task-refactor.1" + dependencies: + "@webex/common": "npm:3.12.0-task-refactor.1" + "@webex/test-helper-make-local-url": "npm:3.12.0-task-refactor.1" + es6-promise: "npm:^4.2.8" + file-type: "npm:^16.0.1" + xhr: "npm:^2.5.0" + checksum: 10c0/9132a8f97f82bd15fe27ae2dfba6705ebb193e1da2c83aea6ba869a96320e35bed1d3ec06f3c70ee82897cba861e526a4a983f070e4c66be18b029b520ab869b + languageName: node + linkType: hard + "@webex/test-helper-make-local-url@npm:2.60.2": version: 2.60.2 resolution: "@webex/test-helper-make-local-url@npm:2.60.2" @@ -11967,6 +12361,13 @@ __metadata: languageName: node linkType: hard +"@webex/test-helper-make-local-url@npm:3.12.0-task-refactor.1": + version: 3.12.0-task-refactor.1 + resolution: "@webex/test-helper-make-local-url@npm:3.12.0-task-refactor.1" + checksum: 10c0/02822d4ff6b4ac0b9ae61c3a85ab1ec87e124f0a85e233b99da825c121a793a2140658b27008185d83ac87d8e4dd6aa375322be27fdb8aba98d831ea1f6eb428 + languageName: node + linkType: hard + "@webex/test-helper-mocha@npm:2.60.2": version: 2.60.2 resolution: "@webex/test-helper-mocha@npm:2.60.2" @@ -12003,6 +12404,15 @@ __metadata: languageName: node linkType: hard +"@webex/test-helper-mocha@npm:3.12.0-task-refactor.1": + version: 3.12.0-task-refactor.1 + resolution: "@webex/test-helper-mocha@npm:3.12.0-task-refactor.1" + dependencies: + bowser: "npm:^2.11.0" + checksum: 10c0/95f96a5659f0537b955e8b458568b25d0feb4c6ca93f7a06180adf70d3d902329baff158de7888c1257d8948f6dd1090ba6a6dd9715ba2f5909fea9e3a7f8b92 + languageName: node + linkType: hard + "@webex/test-helper-mock-web-socket@npm:2.60.2": version: 2.60.2 resolution: "@webex/test-helper-mock-web-socket@npm:2.60.2" @@ -12031,6 +12441,13 @@ __metadata: languageName: node linkType: hard +"@webex/test-helper-mock-web-socket@npm:3.12.0-task-refactor.1": + version: 3.12.0-task-refactor.1 + resolution: "@webex/test-helper-mock-web-socket@npm:3.12.0-task-refactor.1" + checksum: 10c0/d667c28e3802e74323194981d590b79b0fc846e0c52efa44b1d5532c578c1ddeb4169ec0fcadb20395d7934b3ccb976eb6a2c3c396fb59935b2cf86478bc6052 + languageName: node + linkType: hard + "@webex/test-helper-mock-webex@npm:2.60.2": version: 2.60.2 resolution: "@webex/test-helper-mock-webex@npm:2.60.2" @@ -12075,6 +12492,17 @@ __metadata: languageName: node linkType: hard +"@webex/test-helper-mock-webex@npm:3.12.0-task-refactor.1": + version: 3.12.0-task-refactor.1 + resolution: "@webex/test-helper-mock-webex@npm:3.12.0-task-refactor.1" + dependencies: + ampersand-state: "npm:^5.0.3" + es6-promise: "npm:^4.2.8" + lodash: "npm:^4.17.21" + checksum: 10c0/491761aa4c61bf94e0f4d39a98d1f3caf5a7171714ecbc1fdc628a9d8d232f89237885da59e991b348b5406f0d191cf5d0a95230c7fc9b46ce8b4d237486be10 + languageName: node + linkType: hard + "@webex/test-helper-refresh-callback@npm:2.60.2": version: 2.60.2 resolution: "@webex/test-helper-refresh-callback@npm:2.60.2" @@ -12103,6 +12531,13 @@ __metadata: languageName: node linkType: hard +"@webex/test-helper-refresh-callback@npm:3.12.0-task-refactor.1": + version: 3.12.0-task-refactor.1 + resolution: "@webex/test-helper-refresh-callback@npm:3.12.0-task-refactor.1" + checksum: 10c0/12a3235fabdd8154b62cbb4a5d42e50c17b5b467abf5f6ce209fbabc27f1eedb037b1eeb95d5a45740735c70503781350fadd3741421106bb8134f10fdcd802a + languageName: node + linkType: hard + "@webex/test-helper-retry@npm:2.60.2": version: 2.60.2 resolution: "@webex/test-helper-retry@npm:2.60.2" @@ -12139,6 +12574,15 @@ __metadata: languageName: node linkType: hard +"@webex/test-helper-retry@npm:3.12.0-task-refactor.1": + version: 3.12.0-task-refactor.1 + resolution: "@webex/test-helper-retry@npm:3.12.0-task-refactor.1" + dependencies: + es6-promise: "npm:^4.2.8" + checksum: 10c0/df001337ea08508cb4c801cd8849933173038dd2c3e748528ff2bb5790c978523b015c0a643e0940bfb01eb9c197f610693eb16f97f5085779119d1ea3bab797 + languageName: node + linkType: hard + "@webex/test-helper-test-users@npm:2.60.2": version: 2.60.2 resolution: "@webex/test-helper-test-users@npm:2.60.2" @@ -12195,6 +12639,21 @@ __metadata: languageName: node linkType: hard +"@webex/test-helper-test-users@npm:3.12.0-task-refactor.1": + version: 3.12.0-task-refactor.1 + resolution: "@webex/test-helper-test-users@npm:3.12.0-task-refactor.1" + dependencies: + "@ciscospark/test-users-legacy": "npm:^1.0.2" + "@webex/test-helper-retry": "npm:3.12.0-task-refactor.1" + "@webex/test-users": "npm:3.12.0-task-refactor.1" + lodash: "npm:^4.17.21" + dependenciesMeta: + "@ciscospark/test-users-legacy": + optional: true + checksum: 10c0/b82c486882c5e48b1cd1967e42c83d7cc04c5bb733dfd7cc93354ff8d77e9c755b880244d016ba0568a5ee638bc3df91f8243cb7c465d5ddf9cbb32b381dc763 + languageName: node + linkType: hard + "@webex/test-users@npm:2.60.2": version: 2.60.2 resolution: "@webex/test-users@npm:2.60.2" @@ -12251,6 +12710,20 @@ __metadata: languageName: node linkType: hard +"@webex/test-users@npm:3.12.0-task-refactor.1": + version: 3.12.0-task-refactor.1 + resolution: "@webex/test-users@npm:3.12.0-task-refactor.1" + dependencies: + "@webex/http-core": "npm:3.12.0-task-refactor.1" + "@webex/test-helper-mocha": "npm:3.12.0-task-refactor.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/bc64c972d287b0c6f4f23d6218ba1433b55dbeccea836cb9a8dd007fbf22ce3c4c894a7660b354242a23915fae9bcbed507a03184e242093f8b6a540aca178fb + languageName: node + linkType: hard + "@webex/test-users@npm:^1.157.0": version: 1.161.0 resolution: "@webex/test-users@npm:1.161.0" @@ -12334,6 +12807,25 @@ __metadata: languageName: node linkType: hard +"@webex/web-client-media-engine@npm:3.35.2": + version: 3.35.2 + resolution: "@webex/web-client-media-engine@npm:3.35.2" + 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.7.1" + "@webex/web-media-effects": "npm:2.32.1" + "@webex/webrtc-core": "npm:2.13.4" + async: "npm:^3.2.4" + js-logger: "npm:^1.6.1" + typed-emitter: "npm:^2.1.0" + uuid: "npm:^8.3.2" + checksum: 10c0/113b6171cbff94b18f58b29c8eff55c5545903ebc9f5d95f84427eb020254296df7d8d998348f5e56ac19387b77cce2cbfc92a7a5ea4aaeb047b17d44ea9b941 + languageName: node + linkType: hard + "@webex/web-client-media-engine@npm:3.37.1": version: 3.37.1 resolution: "@webex/web-client-media-engine@npm:3.37.1" @@ -12353,6 +12845,21 @@ __metadata: languageName: node linkType: hard +"@webex/web-media-effects@npm:2.32.1": + version: 2.32.1 + resolution: "@webex/web-media-effects@npm:2.32.1" + dependencies: + "@webex/ladon-ts": "npm:^5.10.0" + events: "npm:^3.3.0" + js-logger: "npm:^1.6.1" + typed-emitter: "npm:^1.4.0" + uuid: "npm:^9.0.1" + worker-timers: "npm:^8.0.21" + yarn: "npm:^1.22.22" + checksum: 10c0/84a49ab9b880fc163bfa2098114097704eb69e174ce2e12c43634215e0be1682a7117e008f51b1f56b5370c6576ff905af17ad5ecf8bd280cc1ed2bdf01cde6d + languageName: node + linkType: hard + "@webex/web-media-effects@npm:2.33.0": version: 2.33.0 resolution: "@webex/web-media-effects@npm:2.33.0" @@ -12452,6 +12959,41 @@ __metadata: languageName: node linkType: hard +"@webex/webex-core@npm:3.12.0-task-refactor.1": + version: 3.12.0-task-refactor.1 + resolution: "@webex/webex-core@npm:3.12.0-task-refactor.1" + dependencies: + "@webex/common": "npm:3.12.0-task-refactor.1" + "@webex/common-timers": "npm:3.12.0-task-refactor.1" + "@webex/http-core": "npm:3.12.0-task-refactor.1" + "@webex/storage-adapter-spec": "npm:3.12.0-task-refactor.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/0214351ce3d5f83b0630ebb72d9a86bab046360fabe98326bd2d57f28f99e067e459109dc3b2edca35c6e98f275e990d3c26b094167aea7ff52bdd709a00a335 + languageName: node + linkType: hard + +"@webex/webrtc-core@npm:2.13.4": + version: 2.13.4 + resolution: "@webex/webrtc-core@npm:2.13.4" + dependencies: + "@webex/ts-events": "npm:^1.2.1" + "@webex/web-capabilities": "npm:^1.6.1" + "@webex/web-media-effects": "npm:2.32.1" + 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/5c1c06b8828cccda14af719935abfb70737346c4ed9e1f24f088edcffcc5a732928523e369bd82b48991252ea0fcb045b0792021710b6a335eec15510068a7a9 + languageName: node + linkType: hard + "@webex/webrtc-core@npm:2.13.5": version: 2.13.5 resolution: "@webex/webrtc-core@npm:2.13.5" @@ -36082,6 +36624,13 @@ __metadata: languageName: node linkType: hard +"xstate@npm:5.24.0": + version: 5.24.0 + resolution: "xstate@npm:5.24.0" + checksum: 10c0/1ecd564493560a6ab0412b5b0edc9e4ed484a681941bb1ce47712f74cf7e418f3239153d6c80a322f15968284937ba098865015ac3cfb76d33ffffaf4cebbcd5 + languageName: node + linkType: hard + "xstate@npm:^4.30.6": version: 4.38.3 resolution: "xstate@npm:4.38.3"