feat(contact-center): wxcc-6026 wxapp answer, mute sync, dtmf keypad - #734
feat(contact-center): wxcc-6026 wxapp answer, mute sync, dtmf keypad#734akulakum wants to merge 18 commits into
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5158542d05
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (taskId && !this.wxAppMuteStateListeners[taskId]) { | ||
| const wxAppMuteListener = (payload: {muted: boolean}) => this.handleWxAppMuteStateUpdated(payload, task); | ||
| this.wxAppMuteStateListeners[taskId] = wxAppMuteListener; | ||
| task.on(TASK_EVENTS.TASK_WXAPP_MUTE_STATE_UPDATED, wxAppMuteListener); |
There was a problem hiding this comment.
Rebind the mute listener when the SDK replaces a task
When TASK_HYDRATE or TASK_MERGED supplies a replacement task object with the same interaction ID, registerTaskEventListeners is called again, but this ID-only guard skips attaching the mute listener to the replacement. Subsequent TASK_WXAPP_MUTE_STATE_UPDATED events emitted by the new task therefore never update store.isMuted, and removal also tries to detach the callback from the wrong task object. Track both the task and listener and detach/rebind when the task identity changes, as the adjacent real-time-assist listener does.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in latest commit — wxAppMuteStateListeners now stores {task, listener} and rebinds on hydrate/merge (same pattern as realTimeAssistListeners). Unit tests added in storeEventsWrapper.ts.
|
|
||
| handleTaskEnd = () => { | ||
| this.setIsDeclineButtonEnabled(false); | ||
| this.setIsMuted(false); |
There was a problem hiding this comment.
Reset mute only when the current task ends
In a multi-task session, every task is registered with the same handleTaskEnd callback, so ending a background or digital task while the current wxApp call is muted unconditionally resets the global mute state. The UI then shows the active call as unmuted, and the next toggle derives muted: true instead of unmuting it. Pass the ending task identity into the handler or defer the reset to the existing current-task-aware removal path.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed — handleTaskEnd(endedTask) only calls setIsMuted(false) when the ended task matches currentTask, so background task end no longer clears mute on an active wxApp call.
| | 'toggleHold' | ||
| | 'toggleRecording' | ||
| | 'toggleMute' | ||
| | 'sendDtmf' |
There was a problem hiding this comment.
Keep the new DTMF callback backward-compatible
Any external consumer that directly renders the published CallControlComponent or CallControlCADComponent now fails type checking unless it supplies sendDtmf, even for calls where the keypad is never visible, because CallControlComponentProps is exported and the newly picked field is required. Make this callback optional with a safe default, or treat and document the change as a breaking major-version contract update.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed — sendDtmf is optional on ControlProps / CallControlComponentProps; call-control.tsx defaults to a no-op when omitted.
| if (isWxAppCallingOffer(task) && typeof wxTask.acceptOnWebex === 'function') { | ||
| return wxTask.acceptOnWebex(); |
There was a problem hiding this comment.
Route the new wxApp operations through the store
For wxApp accept, reject, mute, and DTMF paths, this widget-package utility directly invokes newly duck-typed SDK task methods, moving SDK compatibility and dispatch outside the repository's sole SDK boundary. Expose these operations and their types from @webex/cc-store instead so SDK upgrades remain isolated to the store rather than coupling @webex/cc-task to the SDK.
AGENTS.md reference: AGENTS.md:L61-L62
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Not taking this change — widgets call unified ITask methods (accept, decline, toggleMute, transmitDtmf); telephony REST stays in the SDK. wxapp-task.utils.ts is visibility-only, not telephony routing.
| @@ -0,0 +1,37 @@ | |||
| import React from 'react'; | |||
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
Fixed — added CC-COMPONENTS-R-019 in cc-components-spec.md documenting the DTMF keypad, optional sendDtmf, and wxApp visibility behavior.
|
This pull request is automatically being deployed by Amplify Hosting (learn more). |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5fe744901a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (isWxAppCallingOffer(task) && typeof wxTask.acceptOnWebex === 'function') { | ||
| return wxTask.acceptOnWebex(); | ||
| } | ||
| return task.accept(); |
There was a problem hiding this comment.
Bump the SDK before routing wxApp calls
When this commit is installed as-is, packages/contact-center/store/package.json remains pinned to @webex/contact-center 3.12.0-next.96 even though these methods and the new mute event come from the required companion SDK change. Because this helper duck-types the missing surface, wxApp offers silently fall through to the legacy accept()/decline() paths, mute falls through to toggleMute(), and DTMF becomes a no-op rather than exposing the incompatible dependency. Update the pinned SDK and lockfile to a release containing the new APIs before enabling these routes.
AGENTS.md reference: AGENTS.md:L56-L57
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Already addressed — store pins @webex/contact-center@3.12.0-next.109 after merge with next (includes wxApp telephony APIs).
| if (!getKeypadControl(controls)?.isVisible) { | ||
| logger.warn('Keypad control not available', {module: 'useCallControl', method: 'sendDtmf'}); | ||
| return; | ||
| } |
There was a problem hiding this comment.
Honor the keypad enabled state before sending tones
If the SDK updates keypad controls to {isVisible: true, isEnabled: false} while its popover is already open, the trigger becomes disabled but the rendered digit buttons remain clickable. This guard checks only isVisible, so those clicks still call transmitDtmfForTask() despite the SDK explicitly disabling DTMF; check isEnabled here and/or disable the individual keys.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed — sendDtmf checks controls.main.keypad.isEnabled (wxApp path requires enabled even when force-visible); keypad passes disabled to digit buttons when the control is disabled.
| | `ai-docs/_archive/.../task/ai-docs/widgets/TaskList/AGENTS.md` + `ARCHITECTURE.md` | architecture / overview / API | reconciled | Task selection / accept / decline flow → Sequence Diagram(s). | | ||
| | `packages/contact-center/ai-docs/migration/*.md` (7 files) | architecture (planned refactor) | reference-only | Describes a planned SDK `task.uiControls` migration that is NOT in current code. Used only to mark conflicts; current behavior documented as-is. | | ||
| | `packages/contact-center/task/src/` | source of truth | migrated | All requirements, flows, state, and error tables derive from real code here. | | ||
| | `packages/contact-center/ai-docs/features/thick-client-answer/intake.md` | feature intake (WXCC-6026) | reference-only (implemented) | wxApp Answer/Decline/Mute + Mercury mute sync — see § Feature: Accept on Webex thick client | |
There was a problem hiding this comment.
Commit the canonical intake before linking it
The newly declared canonical intake resolves to packages/contact-center/ai-docs/features/thick-client-answer/intake.md, but a repository-wide filename and WXCC-6026 search finds no such file or generated equivalent. Consequently the task and store specs both direct maintainers and validators to nonexistent feature evidence; add the intake in this change or retain the existing source-of-truth entry instead.
AGENTS.md reference: AGENTS.md:L68-L68
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Intentional — intake.md is kept as a local handoff doc (not in the PR). Task/store/cc-components specs are the in-repo source of truth; links to the intake file were removed.
…terTogether rename
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7c5a3de68e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| return; | ||
| } | ||
|
|
||
| logger.info(`sendDtmf(${digit}) called`, {module: 'useCallControl', method: 'sendDtmf'}); |
There was a problem hiding this comment.
When an agent enters an IVR PIN, payment-card number, or other secret through the keypad, this message records every DTMF digit in application logs, allowing the complete sequence to be reconstructed. Log only that a tone was sent, without including digit.
AGENTS.md reference: AGENTS.md:L67-L67
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed — removed the digit from sendDtmf logs; only a generic sendDtmf called message is logged now.
| } else { | ||
| this.enableWxBetterTogether = false; |
There was a problem hiding this comment.
Preserve the wxApp flag for preinitialized Webex clients
When a supported host initializes the store with store.init({webex}) after constructing that Webex instance with enableWxBetterTogether: true, this branch nevertheless forces the store flag to false. The CallControl wrappers then disable their wxApp-engaged fallback and CAD consult-mute/disabled-ghost suppression even though the SDK feature is enabled; carry the flag through the WithWebex initialization path or derive it from the supplied instance.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed — { webex } init now reads enableWxBetterTogether from webex.config.cc.enableWxBetterTogether. Unit test added in store/tests/store.ts.
There was a problem hiding this comment.
Verified — { webex } init reads enableWxBetterTogether from webex.config.cc.enableWxBetterTogether. Added unit test sets enableWxBetterTogether from webex.config.cc when init receives a preinitialized webex instance in store/tests/store.ts.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7e55c0fd42
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (!ctrl?.isVisible || ctrl.isEnabled) { | ||
| return button; | ||
| } |
There was a problem hiding this comment.
Force-show enabled wxApp controls on engaged calls
When an engaged wxApp task reports a control as {isVisible: false, isEnabled: true}, this early return preserves the hidden button, so the agent cannot access mute or DTMF despite the SDK enabling the operation. This also contradicts the force-visible behavior documented in task-spec.md; set isVisible for enabled mute/keypad controls when wxAppEngaged is true.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed — when wxApp is engaged and SDK reports isEnabled: true, mute/keypad are force-shown even if isVisible is false. Test added in call-control.utils.tsx.
| const [telephonyToast, setTelephonyToast] = useState<{ | ||
| error: WxAppTelephonyErrorDisplay; | ||
| action: TelephonyToastAction; | ||
| } | null>(null); |
There was a problem hiding this comment.
Clear telephony errors when switching tasks
If mute or DTMF fails on task A and currentTask subsequently changes to task B, CallControlInternal remains mounted and this state is retained, so task A's error toast is rendered over task B until manually dismissed. Reset the toast when the current interaction ID changes, as useIncomingTask already does for offer-action errors.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed — telephonyToast clears when currentTask.data.interactionId changes (same pattern as IncomingTask offer errors).
| const showTelephonyToast = useCallback( | ||
| (error: unknown, action: TelephonyToastAction) => { | ||
| const parsed = reportWxAppTelephonyFailure(error, {widget: 'CallControl', action}, logger, store.onErrorCallback); | ||
| setTelephonyToast({error: getTelephonyToastDisplay(parsed, action), action}); |
There was a problem hiding this comment.
Report CallControlCAD failures under the correct widget name
When the shared hook is used by CallControlCAD/index.tsx, every mute or DTMF failure still invokes the host error callback with widgetName === 'CallControl'. Hosts that distinguish the two exported widgets therefore misattribute CAD failures or apply the wrong recovery behavior; pass the invoking widget identity into the hook instead of hardcoding it here.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed — useCallControl accepts widgetName; CallControl/index.tsx passes 'CallControl' and CallControlCAD/index.tsx passes 'CallControlCAD'.
Integrate origin/next (webex#736 consult transfer SDK policy) with wxApp Keypad/mute changes. Combined Keypad skip + loadBuddyAgents(action). Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9c90e2ceea
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| task.accept().catch((error) => { | ||
| setTaskActionError(task.data.interactionId, error, 'acceptTask'); |
There was a problem hiding this comment.
Clear the prior task error when retrying
After an accept or decline attempt rejects, this entry remains in taskActionErrors indefinitely: neither a subsequent attempt nor its successful resolution calls clearTaskActionError, and TaskListComponent does not consume the returned clearing callback. Consequently, when the agent retries the same task, the previous “Unable to…” alert remains visible while the new request is in progress and can remain after success until the SDK removes the task; departed task IDs also accumulate in the hook state. Clear the entry when beginning a new action and/or when the action succeeds or leaves taskList.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed — TaskList clears taskActionErrors at the start of accept/decline retry and prunes stale entries when tasks leave taskList.
| try { | ||
| await onDigitPress(digit); | ||
| } finally { | ||
| setShowSpinner(false); | ||
| } |
There was a problem hiding this comment.
Serialize overlapping DTMF transmissions
When an agent presses multiple digits before the previous onDigitPress promise settles, each call runs concurrently. DTMF order is significant for IVR inputs, but separate asynchronous requests can be processed out of order; additionally, the first request to finish sets showSpinner to false even while later tones are still pending. Queue the transmissions in press order and track all pending work rather than using a single boolean per invocation.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed — keypad serializes onDigitPress through a promise queue so tones send in press order; spinner tracks pending count instead of a single boolean.
Keep both Webex Better Together (734) and Participant Drop (735): combined handleTaskEnd with setIsMuted(false) and scheduleTaskListRefresh, merged ControlProps/store types, and SDK pin 3.12.0-next.109. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6e96f30157
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| logger?.info('CC-Widgets: IncomingTask uiControls snapshot', { | ||
| module: 'useIncomingTask', | ||
| method: 'render', | ||
| interactionId: incomingTask?.data?.interactionId, |
There was a problem hiding this comment.
Remove interaction IDs from render-time logs
Every IncomingTask render now writes the interaction ID to the SDK logger, and the replacement-task log in IncomingTask/index.tsx records the same identifier. These task identifiers can be persisted or exported by production logging, so keep the control-state diagnostic but omit the interaction ID.
AGENTS.md reference: AGENTS.md:L67-L67
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed — removed interactionId from IncomingTask render-time logs; control-state diagnostics (accept/decline enabled) are retained without PII.
| const result = useIncomingTask({incomingTask, onAccepted, onRejected, logger}); | ||
| const {logger, isDeclineButtonEnabled, deviceType, taskList} = store; | ||
| const interactionId = incomingTask?.data?.interactionId; | ||
| const liveIncomingTask = interactionId && taskList[interactionId] ? taskList[interactionId] : incomingTask; |
There was a problem hiding this comment.
Detach callbacks from the replaced task object
When hydration or merge replaces taskList[interactionId], this changes the task passed to useIncomingTask; its effect cleanup then calls removeTaskCallback, which looks up the already-replaced object in store.taskList rather than detaching from the prior object where the callbacks were registered. The old task retains its TASK_ASSIGNED, TASK_END, and related listeners, so later emissions can duplicate onAccepted/onRejected and repeated replacements accumulate callbacks. Remove listeners from the task object captured by the effect before switching to the replacement.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed — removeTaskCallback accepts an optional task instance; useIncomingTask and useCallControl pass registeredTask on cleanup so listeners detach from the object that registered them.
The WXCC-6026 intake draft was staged locally and accidentally included in the merge commit; drop it from the branch and ignore future local copies. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1e997f96ec
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| }, []); | ||
|
|
||
| useEffect(() => { | ||
| const handlePageHide = () => { |
There was a problem hiding this comment.
Skip logout when pagehide enters the back-forward cache
When a logged-in user navigates away and the browser stores this page in the back-forward cache, pagehide fires with persisted === true, so this handler can successfully log out the station even though the page is only being suspended. Returning with Back then restores the cached React/store state as logged in while the server-side station session has ended, leaving the widgets stale and subsequent operations failing. Accept the PageTransitionEvent and avoid logout when event.persisted is true.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed — pagehide handler now accepts PageTransitionEvent and skips stationLogout when event.persisted === true, so bfcache Back navigation keeps the station session alive.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 205d17c188
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const intendedMuteState = !isMuted; | ||
|
|
||
| try { | ||
| await currentTask.toggleMute(); | ||
| await currentTask.toggleMute({muted: intendedMuteState}); |
There was a problem hiding this comment.
Serialize mute requests until the active request settles
When an SDK mute request remains pending beyond the UI's fixed 500 ms button timeout, the button becomes clickable again while isMuted still has its previous value. Both invocations therefore compute the same intendedMuteState and send the same explicit {muted: ...} value, so an agent clicking again to reverse the first action instead repeats it. Keep mute disabled until the promise settles or serialize requests using the latest intended state.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed — toggleMute serializes SDK requests through a promise chain with latest-intended-state coalescing; handleMuteToggle now awaits the hook promise instead of re-enabling after a fixed 500ms timeout. Unit tests added.
| if (!incomingTask?.data.interactionId) return; | ||
| incomingTask.accept().catch((error) => { |
There was a problem hiding this comment.
Clear the standalone offer error before retrying
When an accept or decline attempt in the standalone IncomingTask fails, retrying the same interaction never clears offerActionError; clearOfferActionError is returned but is not wired into the component, and these handlers only replace the error after another failure. The previous failure alert therefore remains visible throughout a retry and until the SDK changes the interaction, even when the retry succeeds. Clear it when either action begins, as the TaskList path now does.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed — useIncomingTask accept() and reject() now call clearOfferActionError() at the start of each attempt (same pattern as TaskList). Unit test added.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0ed84d8d9e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| @@ -0,0 +1,108 @@ | |||
| import {ITask} from '@webex/contact-center'; | |||
There was a problem hiding this comment.
Import ITask through the store boundary
Fresh evidence beyond the earlier task-routing discussion is this new literal import from @webex/contact-center inside the widget package. It couples @webex/cc-task directly to the SDK even though ITask is already re-exported by @webex/cc-store; import it from the store alongside the singleton so SDK type changes remain behind the designated boundary.
AGENTS.md reference: AGENTS.md:L61-L62
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed — ITask is now imported from @webex/cc-store in mute-coordinator.ts.
| </div> | ||
| </ListItemBaseSection> | ||
| </ListItemBase> | ||
| {actionError ? <WxAppOfferActionError error={actionError} /> : null} |
There was a problem hiding this comment.
Keep offer errors inside a list item
When an accept or decline fails in TaskList, Task is rendered directly under the <ul> but now returns a <li> followed by this alert <div>. The alert therefore becomes an invalid direct child of the list, which triggers DOM-nesting warnings and breaks the list structure exposed to assistive technology; place the alert within the task's list item or wrap the complete row in a valid <li> structure.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed — WxAppOfferActionError is now rendered inside the task list item fill section instead of as a sibling after </ListItemBase>.
| toggleConsultMute={toggleMute} | ||
| conferenceEnabled={conferenceEnabled} | ||
| enableWxBetterTogether={enableWxBetterTogether} | ||
| currentTask={currentTask} |
There was a problem hiding this comment.
Yes, needed — CAD consult mute visibility depends on wxApp engaged state on the task object via shouldShowWxAppTelephonyControls(enableWxBetterTogether, task) in call-control-custom.utils.ts, same as main-bar gating.
| @@ -0,0 +1,32 @@ | |||
| import React from 'react'; | |||
| import {Text, Toast} from '@momentum-design/components/dist/react'; | |||
| import errorLegacyBoldIcon from '@momentum-design/icons/dist/svg/error-legacy-bold.svg'; | |||
There was a problem hiding this comment.
Good catch — switched to Toast variant="error", which uses error-legacy-bold from the Momentum icon set internally. Removed the direct SVG import and CSS mask workaround.
| E911Modal, | ||
| AIAssistantComponent, | ||
| TelephonyActionToast, | ||
| WxAppOfferActionError, |
There was a problem hiding this comment.
are we using these outside of the components?
There was a problem hiding this comment.
Used by @webex/cc-task widget wrappers (CallControl, CallControlCAD) and inline in Task — not intended for third-party host apps. Documented as widget-internal surfaces in cc-components-spec.md Public Surface.
| import {OutdialAniEntriesResponse} from '@webex/contact-center/dist/types/services/config/types'; | ||
| import {enqueueMuteToggle, resetMuteCoordinator, resetMuteCoordinatorForTests} from './mute-coordinator'; | ||
|
|
||
| export {resetMuteCoordinatorForTests}; |
There was a problem hiding this comment.
is it ok to add a test specific login inside this file?
There was a problem hiding this comment.
Agreed — removed the resetMuteCoordinatorForTests re-export from helper.ts. Tests import the reset directly from mute-coordinator.ts.
| }; | ||
|
|
||
| /** Test-only reset for module-level mute serialization state. */ | ||
| export const resetMuteCoordinatorForTests = resetMuteCoordinator; |
There was a problem hiding this comment.
test specific methods here also
There was a problem hiding this comment.
Kept on the coordinator module only (not re-exported from helper). Required for cross-widget mute tests that share module-level serialization state. Added JSDoc marking it test-only.
| const [collapsedTasks, setCollapsedTasks] = React.useState([]); | ||
| const [showLoader, setShowLoader] = useState(false); | ||
| const [toast, setToast] = useState<{type: 'success' | 'error'} | null>(null); | ||
| const [telephonyError, setTelephonyError] = useState<{ |
There was a problem hiding this comment.
Kept the wxApp toggle and bfcache pagehide guard — both are required for the sample demo. Removed the host-level telephonyError toast; widgets now render inline/toast errors and the host only logs wxApp failures for debugging.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 175e2870d8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (isTelephony) { | ||
| this.setIsMuted(false); |
There was a problem hiding this comment.
Prevent background offers from resetting mute
When a host has registered the incoming-task callback and an extension/wxApp agent is muted on the current task, receiving a second telephony offer invokes this method before that offer becomes current and unconditionally changes the singleton isMuted state. The active call is then displayed as unmuted and its next toggle derives the wrong target; as with the guarded handleTaskEnd path, reset mute only when the affected task is current or when the new task is actually selected.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed — handleTaskMuteState only resets isMuted when there is no current task or the incoming offer matches currentTask; background offers no longer clobber mute on the engaged call. Unit tests in store/tests/storeEventsWrapper.ts. Documented in STORE-R-029.
| await currentTask.transmitDtmf({dtmf: digit}); | ||
| } catch (error) { | ||
| logger.error(`sendDtmf failed: ${error}`, {module: 'useCallControl', method: 'sendDtmf'}); | ||
| showTelephonyToast(error, 'dtmf'); |
There was a problem hiding this comment.
Discard DTMF failures after the task changes
When transmitDtmf for task A remains pending while currentTask changes to task B, the task-change effect clears the existing toast, but task A's later rejection still calls showTelephonyToast and displays its failure over task B. Fresh evidence beyond the earlier task-switch toast reset is this unconditional post-await failure publication; capture the interaction identity and verify it is still current before reporting the error.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed — sendDtmf captures interactionId before await transmitDtmf and skips showTelephonyToast if store.currentTask changed. Unit test in task/tests/helper.ts.
| } finally { | ||
| muteChainInFlight = false; | ||
| activeMuteTarget = null; |
There was a problem hiding this comment.
Isolate mute coordinator cleanup by task generation
When task A has a pending mute request, switching tasks resets the shared globals and allows a request for task B to begin, but if A settles while B is still pending, A's old chain executes this finally and marks B's coordinator idle while clearing its active target. A subsequent click on B is then derived from stale store.isMuted and can repeat the pending mute instead of reversing it. Fresh evidence beyond the task-identity completion guard is this unconditional cleanup of state belonging to a newer generation; scope cleanup to the chain/task generation that created it.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed — muteGeneration token scopes finally cleanup; stale chains from a prior task cannot clear in-flight state for a newer generation. Unit test in task/tests/helper.ts.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6c1e3c9c03
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (task && !isSameTask) { | ||
| this.seedWxAppMuteFromTask(task); |
There was a problem hiding this comment.
Reset mute state while seeding a newly selected task
When switching from a muted task A to an unmuted task B, this starts the asynchronous mute sync without replacing A's existing store.isMuted value. CallControl therefore renders B as muted and an immediate toggle derives the wrong target; if syncWxAppMuteFromCallDetails() rejects or is unavailable, the stale value remains indefinitely. Clear or track mute per interaction before promoting B, then apply the synchronized value only while B is still current.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed — setCurrentTask now calls setIsMuted(false) synchronously when promoting a different task, before seedWxAppMuteFromTask backfills the authoritative SDK value. Unit tests: task-switch stale mute reset + sync-reject variant in store/tests/storeEventsWrapper.ts. STORE-R-027 updated.
| const storeInteractionId = store.currentTask?.data?.interactionId; | ||
| if (storeInteractionId !== undefined && storeInteractionId !== interactionId) { | ||
| return false; |
There was a problem hiding this comment.
Reject mute completions after the current task is cleared
When a task ends while toggleMute() is pending, store.currentTask becomes null and storeInteractionId is therefore undefined; this condition skips the mismatch rejection and treats the old completion as valid, allowing it to restore store.isMuted and invoke onToggleMute after task-end cleanup reset the state. Fresh evidence beyond the earlier task-switch fix is the explicit undefined exemption here; require the current interaction ID to equal the captured ID, including rejecting a missing current task.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed — isMuteCompletionStillValid now requires strict equality: store.currentTask?.data?.interactionId === interactionId (rejects null/cleared current task). Unit test: should not apply mute state after current task is cleared while SDK mute is pending in task/tests/helper.ts.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b5b851c249
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| isMuted: store.isMuted, | ||
| task: activeTask, | ||
| }); | ||
| } | ||
| break; | ||
| } |
There was a problem hiding this comment.
Isolate host callback failures from the mute queue
When a host-provided onToggleMute callback throws after the SDK successfully changes mute state, the surrounding try treats it as a telephony failure and invokes the same callback again here. If it throws again, the module-level muteChain becomes rejected; subsequent calls append work with muteChain.then(...) and therefore never execute another SDK mute request until a task change resets the coordinator. Catch callback failures separately and ensure the serialization chain always recovers.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed — onToggleMute is invoked via invokeOnToggleMute() with its own try/catch so host callback throws are logged separately and never enter the SDK failure path or reject muteChain. Added .catch() on the chain as defense in depth. Unit test: should keep mute queue working when onToggleMute callback throws after SDK success in task/tests/helper.ts.
| .then(() => { | ||
| store.clearOfferActionError(interactionId); | ||
| }) |
There was a problem hiding this comment.
Ignore successes from superseded offer attempts
When two offer actions overlap for the same interaction, a newer attempt can fail and publish its inline error before an older attempt resolves. This unconditional success handler then clears the newer failure even though the attempt counter already marks this promise as superseded; the same pattern exists in the decline and standalone IncomingTask handlers. Check isLatestOfferActionAttempt before clearing on success, just as the rejection path does.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed — all four offer-action success handlers (TaskList accept/decline, IncomingTask accept/reject) now guard with isLatestOfferActionAttempt before calling clearOfferActionError, matching the rejection path. Unit tests: useIncomingTask does not clear newer accept failure when an older accept attempt succeeds and useTaskList does not clear newer accept failure when an older accept attempt succeeds in task/tests/helper.ts.
| {showAgentMenu && agentMenuType === button.menuType && button.menuType === 'Keypad' ? ( | ||
| <CallControlDtmfKeypad onDigitPress={sendDtmf} logger={logger} disabled={button.disabled} /> |
There was a problem hiding this comment.
Reset keypad state when the interaction changes
If the keypad popover remains open while currentTask switches from call A to call B, React reuses this CallControlDtmfKeypad instance because the surrounding component and mapped key remain unchanged. Its local dialNumber therefore continues displaying the DTMF sequence entered for A on B's keypad, potentially exposing a prior PIN or making the sequence appear associated with the wrong call. Key or reset the keypad using the current interaction ID.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed — CallControlDtmfKeypad is keyed by currentTask.data.interactionId and the Keypad popover closes when the interaction ID changes, so dial state cannot leak across task switches. Unit test: resets dialNumber when remounted with a different key in cc-components/tests/.../call-control-dtmf-keypad.tsx. Documented in CC-COMPONENTS-R-019.
…nd keypad task switch
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 96fff02249
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| setPendingCount((count) => count + 1); | ||
| transmissionQueueRef.current = transmissionQueueRef.current | ||
| .then(() => onDigitPress(digit)) | ||
| .catch(() => undefined) |
There was a problem hiding this comment.
Cancel queued tones when the interaction changes
When several digits are queued for task A and its first onDigitPress remains pending, switching to task B unmounts this keypad but does not cancel the promise chain. After the first request settles, the retained closures still invoke onDigitPress for every remaining digit against task A, so an IVR/PIN sequence can continue on a call the agent is no longer controlling. Associate the queue with the interaction and discard pending entries when the keypad is unmounted or the task changes.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed — CallControlDtmfKeypad now bumps a generation token on unmount; queued onDigitPress closures skip transmission when stale, and finally avoids setState on unmounted instances. Defense-in-depth: sendDtmf in helper.ts returns before transmitDtmf when store.currentTask no longer matches the captured interaction ID. Tests: discards queued digits when unmounted/remounted before transmission completes in call-control-dtmf-keypad.tsx; sendDtmf skips transmitDtmf when store current task no longer matches hook task in task/tests/helper.ts. Updated CC-COMPONENTS-R-019.
| // 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; |
There was a problem hiding this comment.
Preserve the browser outdial Accept label
For a BROWSER outdial whose accept control is visible-but-disabled and whose decline control is visible, this new condition selects Calling... even though the browser-specific branch immediately above intentionally suppresses the extension-style pending label and previously rendered Accept. Restrict showCalling to non-browser/wxApp sessions; the same unconditional condition in TaskList/task-list.utils.ts causes the corresponding list regression.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed — added !isBrowser to showCalling in both incoming-task.utils.tsx and task-list.utils.ts so browser/WebRTC outdial keeps Accept (auto-answer path) while wxApp/extension outdial still shows Calling.... Tests: should show Accept for browser outdial while answer is pending in IncomingTask and TaskList utils; existing wxApp Calling... test retained.
| participantDropAgentIdRef.current = agentId; | ||
|
|
||
| useEffect(() => { | ||
| resetMuteCoordinator(); |
There was a problem hiding this comment.
Reset the mute coordinator only when the task changes
When CallControl has a mute request pending and a second CallControl/CallControlCAD instance mounts for the same interaction, this mount effect unconditionally resets the module-level coordinator. The successful older request then fails its generation/task validity checks and never updates store.isMuted or invokes its callback; on WebRTC calls without the wxApp mute event, the UI remains permanently out of sync with the actual media state. Track the coordinator's interaction ID and reset only when that identity changes, not on every hook instance's initial effect.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed — introduced resetMuteCoordinatorForInteraction(interactionId) in mute-coordinator.ts; the useCallControl effect now resets the coordinator only when the interaction ID actually changes, not on every hook mount. Mounting CallControlCAD while CallControl has a pending mute on the same task no longer bumps muteGeneration. Test: should not reset mute coordinator when a second hook mounts during pending mute on same interaction in task/tests/helper.ts.
| logger.error(`CC-Widgets: ${context.action} failed: ${parsed.message}`, { | ||
| module: 'wxapp-error.utils', | ||
| method: context.action, | ||
| trackingId: parsed.trackingId, |
There was a problem hiding this comment.
Isolate failures from the host error callback
If the host-provided onErrorCallback (or its metrics wrapper) throws while accept, decline, mute, or DTMF is being reported, this invocation prevents reportWxAppTelephonyFailure from returning. Offer handlers consequently never store the inline error and leave an unhandled rejected promise, while mute/DTMF handlers fail to set their toast. Catch callback failures separately so host error reporting cannot suppress the user-facing recovery UI.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed — onErrorCallback is now invoked via try/catch in reportWxAppTelephonyFailure; callback throws are logged and parsed is always returned so offer/DTMF/mute error handling continues. Test: returns parsed display when onErrorCallback throws in task/tests/wxapp-error.utils.test.ts.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c0408c39b3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
|
||
| | Surface | Change | | ||
| |---|---| | ||
| | **Host init** | `webexConfig.cc.enableAnswerOnWebex: boolean` (default `false`) — set **before** `store.init()`; persisted on store for **UI visibility gating only** | |
There was a problem hiding this comment.
Name the implemented host-init flag
When an integrator follows this new host-init table, enableAnswerOnWebex is silently ignored because Store.init() only reads webexConfig.cc.enableWxBetterTogether (store/src/store.ts:142-146). The store flag therefore remains false and the wxApp mute/keypad visibility path stays disabled; document the implemented enableWxBetterTogether key here, matching the sample app and store spec.
AGENTS.md reference: AGENTS.md:L68-L68
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e02e4df4fc
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (muteChainInFlight) { | ||
| const base = pendingMuteTarget ?? activeMuteTarget ?? !store.isMuted; | ||
| pendingMuteTarget = !base; | ||
| } else { | ||
| pendingMuteTarget = !store.isMuted; |
There was a problem hiding this comment.
Mark mute work in flight before queuing
When two toggleMute() calls occur synchronously—such as from concurrently mounted CallControl and CallControlCAD instances—neither call sees muteChainInFlight because it is set only inside the later promise microtask. Both therefore assign the same !store.isMuted target, so two toggles from an unmuted state produce one mute request instead of a final unmuted intent. Mark the chain as scheduled synchronously or derive subsequent intent from pendingMuteTarget even before the SDK request starts.
Useful? React with 👍 / 👎.
| .catch((error) => { | ||
| if (!isLatestOfferActionAttempt(interactionId, attemptId)) { | ||
| return; | ||
| } | ||
| setTaskActionError(interactionId, error, 'acceptTask'); | ||
| logError(`CC-Widgets: Error accepting task: ${error}`, 'acceptTask'); |
There was a problem hiding this comment.
Ignore offer failures after the task leaves
When an accept or decline request remains pending while its task leaves taskList, the pruning effect runs first, but this later rejection unconditionally recreates the removed interaction's error entry; no further task-list change is required to prune it again. Fresh evidence beyond the earlier pruning fix is this post-prune asynchronous publication without an active-task check, which lets departed interaction IDs accumulate and can restore stale UI if the task is rendered again. Verify the interaction is still active before publishing the failure; the corresponding decline and standalone IncomingTask handlers need the same guard.
Useful? React with 👍 / 👎.
COMPLETES WXCC-6026
This pull request addresses
Third-party CRM embeds need Answer on Webex: agents working in Contact Center widgets should accept, decline, mute, and send DTMF from the embed while Webex App (thick client) handles telephony on the same machine and user.
This widgets PR wires the UI and store layer to the new SDK wxApp telephony APIs. Widgets do not call usersub or Mercury directly — that is SDK-owned.
by making the following changes
@webex/cc-taskacceptOnWebex()/rejectOnWebex()instead of standard task accept/decline.wxapp-task.utils.ts— shared helpers (isWxAppEngagedCall,toggleMuteForTask, accept/decline routing).toggleMuteOnWebex({ muted })with explicit UI intent (fixes Mercury desync); DTMF viatransmitDtmfOnWebex().store.taskListso Accept stays enabled when uiControls update.task-spec.md(spec-currency).@webex/cc-storeTASK_WXAPP_MUTE_STATE_UPDATEDper task;handleWxAppMuteStateUpdated→setIsMuted()when task iscurrentTask.handleTaskRemove(prevent duplicate/stale listeners).store-spec.md(spec-currency).@webex/cc-componentsSample app (
widgets-samples/cc/samples-cc-react-app)store.isAgentLoggedIn.store.cc.setManageWebexCallingInWxcc()(usersub publish in SDK).Change Type
The following scenarios were tested
Automated (unit):
@webex/cc-task— wxApp accept/decline routing,toggleMuteForTask, CallControl mute/DTMF, IncomingTask live task resolution@webex/cc-store—TASK_WXAPP_MUTE_STATE_UPDATEDlistener attach/detach,handleWxAppMuteStateUpdated@webex/cc-components— CallControl snapshots and utilsManual (end-to-end, with SDK #5167):
false(toast suppression cleared)The GAI Coding Policy And Copyright Annotation Best Practices
Checklist before merging
Merge order: Merge SDK PR #5167 first (or bump
@webex/contact-centerin store before release). Widgets depend on SDK wxApp methods,TASK_WXAPP_MUTE_STATE_UPDATED, andsetManageWebexCallingInWxcc().Make sure to have followed the contributing guidelines before submitting.