Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ const CallControlConsultComponent: React.FC<CallControlConsultComponentsProps> =
controls,
toggleConsultMute,
conferenceEnabled = true,
enableWxBetterTogether = false,
currentTask = null,
}) => {
// Use the label and timestamp calculated in helper.ts
// Stable key based on timestamp to prevent timer resets
Expand All @@ -35,7 +37,9 @@ const CallControlConsultComponent: React.FC<CallControlConsultComponentsProps> =
consultConference,
switchToMainCall,
logger,
conferenceEnabled
conferenceEnabled,
enableWxBetterTogether,
currentTask
);

// Filter buttons that should be shown, then map them
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {BuddyDetails, ContactServiceQueue, ILogger, TaskUIControls} from '@webex/cc-store';
import {BuddyDetails, ContactServiceQueue, ILogger, ITask, TaskUIControls} from '@webex/cc-store';
import {MUTE_CALL, UNMUTE_CALL} from '../../constants';
import {ButtonConfig} from '../../task.types';
import {shouldShowWxAppTelephonyControls} from '../call-control.utils';

/**
* Interface for list item data
Expand All @@ -22,7 +23,9 @@ export const createConsultButtons = (
consultConference: () => void,
switchToMainCall: () => void,
logger?,
conferenceEnabled = true
conferenceEnabled = true,
enableWxBetterTogether = false,
task: ITask | null = null
): ButtonConfig[] => {
try {
const consultCtrl = controls?.consult;
Expand All @@ -44,7 +47,9 @@ export const createConsultButtons = (
className: `${isMuted ? 'call-control-button-muted' : 'call-control-button'}`,
// Consult mute should only be interactive while consult leg is active.
disabled: !isConsultLegActive || !(consultCtrl?.mute?.isEnabled ?? false),
isVisible: consultCtrl?.mute?.isVisible ?? false,
isVisible: shouldShowWxAppTelephonyControls(enableWxBetterTogether, task)
? false
: (consultCtrl?.mute?.isVisible ?? false),
},
{
key: 'switchToMainCall',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React from 'react';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Update the cc-components spec for the keypad surface

This change adds a new cc-components keypad and changes the exported CallControl prop surface, but only the task and store module specs were updated; packages/contact-center/cc-components/ai-docs/cc-components-spec.md remains unchanged. Update the owning module spec and public-contract documentation in this change so consumers and validators do not retain the old CallControl contract.

AGENTS.md reference: AGENTS.md:L68-L68

Useful? React with 👍 / 👎.

import {Button} from '@momentum-design/components/dist/react';
import {KEY_LIST} from '../OutdialCall/constants';
import type {ILogger} from '@webex/cc-store';

export type CallControlDtmfKeypadProps = {
onDigitPress: (digit: string) => void;
logger?: ILogger;
};

/**
* In-call DTMF keypad for wxApp telephony sessions (Extension login).
* Each key press sends a single tone via SDK task.transmitDtmf().
*/
const CallControlDtmfKeypad: React.FunctionComponent<CallControlDtmfKeypadProps> = ({onDigitPress, logger}) => {
const handleDigitPress = (digit: string) => {
logger?.info(`CC-Widgets: CallControl: DTMF digit pressed`, {
module: 'call-control-dtmf-keypad.tsx',
method: 'handleDigitPress',
});
onDigitPress(digit);
};

return (
<ul className="call-control-dtmf-keys" data-testid="call-control-keypad-keys">
{KEY_LIST.map((key) => (
<li key={key}>
<Button className="call-control-dtmf-key" onClick={() => handleDigitPress(key)} aria-label={`DTMF ${key}`}>
{key}
</Button>
</li>
))}
</ul>
);
};

export default CallControlDtmfKeypad;
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,22 @@
margin-top: 1rem;
}

.call-control-dtmf-keys {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem;
list-style: none;
padding: 0.5rem;
margin: 0;
min-width: 12rem;
}

.call-control-dtmf-key {
width: 100%;
min-height: 2.5rem;
}


.wrapup-group {
display: flex;
flex-direction: column;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import './call-control.styles.scss';
import {PopoverNext, TooltipNext, Text, ButtonCircle} from '@momentum-ui/react-collaboration';
import {Icon, Button, Select, Option} from '@momentum-design/components/dist/react';
import ConsultTransferPopoverComponent from './CallControlCustom/consult-transfer-popover';
import CallControlDtmfKeypad from './call-control-dtmf-keypad';
import AutoWrapupTimer from '../AutoWrapupTimer/AutoWrapupTimer';
import type {MEDIA_CHANNEL as MediaChannelType} from '../task.types';
import {DestinationType} from '@webex/cc-store';
Expand All @@ -24,6 +25,7 @@ import {
filterButtonsForConsultation,
getConsultFilterPhase,
updateCallStateFromTask,
applyWxAppTelephonyControlVisibility,
} from './call-control.utils';
import {withMetrics} from '@webex/cc-ui-logging';

Expand All @@ -40,6 +42,7 @@ function CallControlComponent(props: CallControlComponentProps) {
toggleHold,
toggleRecording,
toggleMute,
sendDtmf,
isMuted,
endCall,
wrapupCall,
Expand Down Expand Up @@ -68,6 +71,8 @@ function CallControlComponent(props: CallControlComponentProps) {
getQueuesFetcher,
consultTransferOptions,
conferenceEnabled = true,
enableWxBetterTogether = false,
agentDeviceType,
} = props;

useEffect(() => {
Expand Down Expand Up @@ -145,8 +150,17 @@ function CallControlComponent(props: CallControlComponentProps) {
conferenceEnabled
);

const wxAppGatedButtons = applyWxAppTelephonyControlVisibility(
buttons,
currentTask,
controls,
isTelephony,
enableWxBetterTogether,
agentDeviceType
);

const consultFilterPhase = getConsultFilterPhase(currentTask, controls);
const filteredButtons = filterButtonsForConsultation(buttons, consultFilterPhase, isTelephony, logger);
const filteredButtons = filterButtonsForConsultation(wxAppGatedButtons, consultFilterPhase, isTelephony, logger);

if (!currentTask) return null;

Expand All @@ -168,13 +182,15 @@ function CallControlComponent(props: CallControlComponentProps) {
<PopoverNext
key={index}
onShow={() => {
logger.info(`CC-Widgets: CallControl: showing consult-transfer popover`, {
logger.info(`CC-Widgets: CallControl: showing ${button.menuType} popover`, {
module: 'call-control.tsx',
method: 'onShowPopover',
});
setShowAgentMenu(true);
setAgentMenuType(button.menuType as CallControlMenuType);
loadBuddyAgents();
if (button.menuType !== 'Keypad') {
loadBuddyAgents();
}
}}
onHide={() => {
setShowAgentMenu(false);
Expand Down Expand Up @@ -219,7 +235,9 @@ function CallControlComponent(props: CallControlComponentProps) {
</TooltipNext>
}
>
{showAgentMenu && agentMenuType === button.menuType ? (
{showAgentMenu && agentMenuType === button.menuType && button.menuType === 'Keypad' ? (
<CallControlDtmfKeypad onDigitPress={sendDtmf} logger={logger} />
) : showAgentMenu && agentMenuType === button.menuType ? (
<ConsultTransferPopoverComponent
heading={button.menuType}
buttonIcon={button.icon}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {CallControlButton, MEDIA_CHANNEL as MediaChannelType, MediaTypeInfo
import type {TaskUIControls} from '@webex/cc-store';
import {getMediaTypeInfo} from '../../../utils';
import {DestinationType, ILogger, ITask} from '@webex/cc-store';

import {
RESUME_CALL,
HOLD_CALL,
Expand All @@ -15,6 +16,11 @@ import {
UNMUTE_CALL,
} from '../constants';

/** SDK P0 keypad control — may exist on main leg before InteractionUIControls ships keypad. */
type TaskMainControlsWithKeypad = TaskUIControls['main'] & {
keypad?: {isVisible: boolean; isEnabled: boolean};
};

/**
* Handles toggle hold functionality
*/
Expand Down Expand Up @@ -166,6 +172,67 @@ export const getMediaType = (mediaType: MediaChannelType, mediaChannel: MediaCha
}
};

/** Duck-type wxApp engaged call — visibility only; avoid upstream import from task package. */
type WxAppTelephonyTaskForVisibility = ITask & {
getWebexCallingCallId?: () => string | null | undefined;
};

export const isWxAppEngagedCall = (task: ITask | null | undefined): boolean => {
const wxTask = task as WxAppTelephonyTaskForVisibility | null | undefined;
return typeof wxTask?.getWebexCallingCallId === 'function' && !!wxTask.getWebexCallingCallId();
};

/** Thick-client main-bar Mute/Keypad visibility gate — visibility only; not mute API routing. */
export const shouldShowWxAppTelephonyControls = (
enableWxBetterTogether: boolean,
task: ITask | null | undefined
): boolean => enableWxBetterTogether === true && isWxAppEngagedCall(task);

/**
* Thin defense layer for thick-client flag ON: suppresses visible+disabled ghost controls.
* SDK owns isVisible/isEnabled for wxApp engaged calls; widgets pass through when SDK is correct.
*/
export const applyWxAppTelephonyControlVisibility = (
buttons: CallControlButton[],
task: ITask | null | undefined,
controls: TaskUIControls | undefined,
isTelephony: boolean,
enableWxBetterTogether?: boolean,
agentDeviceType?: string
): CallControlButton[] => {
if (enableWxBetterTogether !== true) {
return buttons;
}

const mainCtrl = controls?.main as TaskMainControlsWithKeypad | undefined;
const wxAppEngaged = isTelephony && isWxAppEngagedCall(task);
const isExtensionAgent = agentDeviceType != null && agentDeviceType !== 'BROWSER';

return buttons.map((button) => {
if (button.id !== 'mute' && button.id !== 'keypad') {
return button;
}

const ctrl = button.id === 'mute' ? mainCtrl?.mute : mainCtrl?.keypad;

if (!ctrl?.isVisible || ctrl.isEnabled) {
return button;
}

// Extension ghost controls when flag ON but wxApp telephony not active.
if (isExtensionAgent && !wxAppEngaged) {
return {...button, isVisible: false};
}

// wxApp engaged: hide stale visible+disabled if SDK payload briefly desyncs.
if (wxAppEngaged) {
return {...button, isVisible: false};
}

return button;
});
};

/**
* Checks if the media type is telephony
*/
Expand Down Expand Up @@ -204,7 +271,7 @@ export const buildCallControlButtons = (
conferenceEnabled = true
): CallControlButton[] => {
try {
const mainCtrl = controls?.main;
const mainCtrl = controls?.main as TaskMainControlsWithKeypad | undefined;
const isTransferConferenceVisible = mainCtrl?.transferConference?.isVisible ?? false;
const isTransferConferenceEnabled = mainCtrl?.transferConference?.isEnabled ?? false;
const isTransferVisible = mainCtrl?.transfer?.isVisible ?? false;
Expand All @@ -229,6 +296,16 @@ export const buildCallControlButtons = (
isVisible: mainCtrl?.mute?.isVisible ?? false,
dataTestId: 'call-control:mute-toggle',
},
{
id: 'keypad',
icon: 'dialpad-bold',
tooltip: 'Keypad',
className: 'call-control-button',
disabled: !(mainCtrl?.keypad?.isEnabled ?? false),
isVisible: mainCtrl?.keypad?.isVisible ?? false,
menuType: 'Keypad',
dataTestId: 'call-control:keypad',
},
{
id: 'switchToConsult',
icon: 'call-swap-bold',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ const CallControlCADComponent: React.FC<CallControlComponentProps> = (props) =>
conferenceParticipants,
conferenceEnabled = true,
isCampaignCall = false,
enableWxBetterTogether = false,
} = props;

const formatTime = (time: number): string => {
Expand Down Expand Up @@ -322,6 +323,8 @@ const CallControlCADComponent: React.FC<CallControlComponentProps> = (props) =>
controls={controls}
toggleConsultMute={toggleMute}
conferenceEnabled={conferenceEnabled}
enableWxBetterTogether={enableWxBetterTogether}
currentTask={currentTask}
/>
</div>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,17 @@ import {withMetrics} from '@webex/cc-ui-logging';
import {extractIncomingTaskData} from './incoming-task.utils';

const IncomingTaskComponent: React.FunctionComponent<IncomingTaskComponentProps> = (props) => {
const {incomingTask, accept, reject, logger, acceptControl, declineControl, isDeclineButtonEnabled, isBrowser} =
props;
const {
incomingTask,
accept,
reject,
logger,
acceptControl,
declineControl,
isDeclineButtonEnabled,
isBrowser,
offerActionError,
} = props;
if (!incomingTask) {
return <></>; // hidden component
}
Expand Down Expand Up @@ -46,6 +55,7 @@ const IncomingTaskComponent: React.FunctionComponent<IncomingTaskComponentProps>
styles="task-list-hover"
mediaType={taskData.mediaType as MEDIA_CHANNEL}
mediaChannel={taskData.mediaChannel as MEDIA_CHANNEL}
actionError={offerActionError}
/>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,14 @@ export const extractIncomingTaskData = (
// Desktop/WebRTC outdial: accept visible but disabled → show "Accept" (auto-answer handles it)
// Desktop/WebRTC inbound: accept visible and enabled → show "Accept"
const showRinging = isTelephony && !accept.isEnabled && !(isBrowser && isOutdial);
const acceptText = accept.isVisible ? (showRinging ? 'Ringing...' : 'Accept') : undefined;
const showCalling = isTelephony && isOutdial && accept.isVisible && !accept.isEnabled && decline.isVisible;
const acceptText = accept.isVisible
? showCalling
? 'Calling...'
: showRinging
? 'Ringing...'
: 'Accept'
: undefined;

const declineText = decline.isVisible ? 'Decline' : undefined;

Expand Down
Loading
Loading