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
2 changes: 1 addition & 1 deletion .yarnrc.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
yarnPath: .yarn/releases/yarn-4.5.1.cjs
checksumBehavior: update
nodeLinker: node-modules
npmRegistryServer: "https://registry.npmjs.org"
npmRegistryServer: "https://registry.npmjs.org"
3 changes: 3 additions & 0 deletions jest.setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
// This is a workaround for the fact that JSDOM does not support canvas methods like getContext.
import 'jest-canvas-mock';

// Set up the global that @webex/cc-digital-interactions expects
global.AGENTX_SERVICE = {};

// Web components used in @momentum-design imports rely on browser-only APIs like attachInternals.
// Jest (via JSDOM) doesn't support these, causing runtime errors in tests.
// We mock these methods on HTMLElement to prevent test failures.
Expand Down
20 changes: 20 additions & 0 deletions packages/contact-center/cc-digital-channels/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import globals from 'globals';
import pluginJs from '@eslint/js';
import tseslint from 'typescript-eslint';
import pluginReact from 'eslint-plugin-react';
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
import eslintConfigPrettier from 'eslint-config-prettier';

export default [
{files: ['**/src/**/*.{js,mjs,cjs,ts,jsx,tsx}']},
{ignores: ['.babelrc.js', '*config.{js,ts}', 'dist', 'node_modules', 'coverage']},
{languageOptions: {globals: globals.browser}},
pluginJs.configs.recommended,
...tseslint.configs.recommended,
{
...pluginReact.configs.flat.recommended,
settings: {react: {version: 'detect'}},
},
eslintPluginPrettierRecommended,
eslintConfigPrettier,
];
9 changes: 9 additions & 0 deletions packages/contact-center/cc-digital-channels/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const jestConfig = require('../../../jest.config.js');

jestConfig.rootDir = '../../../';
jestConfig.testMatch = ['**/cc-digital-channels/tests/**/*.ts', '**/cc-digital-channels/tests/**/*.tsx'];
jestConfig.transformIgnorePatterns = [
'/node_modules/(?!(@momentum-design/components|@momentum-ui/web-components|@momentum-ui/react-collaboration|@lit|lit|cheerio|@popperjs|@webex-engage|@interactjs|react-error-boundary))',
];

module.exports = jestConfig;
72 changes: 72 additions & 0 deletions packages/contact-center/cc-digital-channels/package.json
Copy link
Contributor

Choose a reason for hiding this comment

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

I would suggest refering cc-statino-login or user-state's package.json rather than creating a new one.

Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
{
"name": "@webex/cc-digital-channels",
"description": "Webex Contact Center Widgets: Digital Channels",
"license": "Cisco's General Terms (https://www.cisco.com/site/us/en/about/legal/contract-experience/index.html)",
"version": "1.28.0-ccwidgets.126",
"main": "dist/index.js",
"types": "dist/types/index.d.ts",
"publishConfig": {
"access": "public"
},
"files": [
"dist/",
"package.json"
],
"scripts": {
"clean": "rm -rf dist && rm -rf node_modules",
"clean:dist": "rm -rf dist",
"build": "yarn run -T tsc",
"build:src": "yarn run clean:dist && yarn run build && webpack",
"build:watch": "webpack --watch",
"test:unit": "jest --coverage",
"test:styles": "eslint",
"deploy:npm": "yarn npm publish"
},
"dependencies": {
"@webex/cc-digital-interactions": "^3.0.4",
"@webex/cc-store": "workspace:*",
"mobx-react-lite": "^4.1.0"
},
"devDependencies": {
"@babel/core": "7.25.2",
"@babel/preset-env": "7.25.4",
"@babel/preset-react": "7.24.7",
"@babel/preset-typescript": "7.25.9",
"@eslint/js": "^9.20.0",
"@testing-library/dom": "10.4.0",
"@testing-library/jest-dom": "6.6.2",
"@testing-library/react": "16.0.1",
"@types/jest": "29.5.14",
"@webex/test-fixtures": "workspace:*",
"babel-jest": "29.7.0",
"babel-loader": "9.2.1",
"css-loader": "7.1.2",
"eslint": "^9.20.1",
"eslint-config-prettier": "^10.0.1",
"eslint-config-standard": "^17.1.0",
"eslint-plugin-import": "^2.25.2",
"eslint-plugin-n": "^15.0.0 || ^16.0.0 ",
"eslint-plugin-prettier": "^5.2.3",
"eslint-plugin-promise": "^6.0.0",
"eslint-plugin-react": "^7.37.4",
"globals": "^16.0.0",
"jest": "29.7.0",
"jest-environment-jsdom": "29.7.0",
"prettier": "^3.5.1",
"sass": "1.79.5",
"sass-loader": "16.0.2",
"style-loader": "4.0.0",
"ts-jest": "^29.1.1",
"ts-loader": "9.5.1",
"typescript": "5.6.3",
"typescript-eslint": "^8.24.1",
"webpack": "5.94.0",
"webpack-cli": "5.1.4",
"webpack-merge": "6.0.1"
},
"peerDependencies": {
"@momentum-ui/web-components": "^2.23.35",
"react": ">=18.3.1",
"react-dom": ">=18.3.1"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React, {useMemo} from 'react';
import Engage from '@webex/cc-digital-interactions';

import '@momentum-ui/web-components';

export interface DigitalChannelsComponentProps {
conversationId: string;
jwtToken: string;
dataCenter: string;
handleError: (error: unknown) => boolean;
}

/**
* Presentation component for Digital Channels.
* Renders the Engage widget with proper theming.
*/
const DigitalChannelsComponent: React.FunctionComponent<DigitalChannelsComponentProps> = ({
conversationId,
jwtToken,
dataCenter,
handleError,
}) => {
// Create a stable key based on critical props to force remount when they change
// This prevents issues with the Froala editor trying to cleanup/reinitialize improperly
const componentKey = useMemo(() => {
return `${conversationId}-${jwtToken.slice(-8)}-${dataCenter}`;
}, [conversationId, jwtToken, dataCenter]);

return (
<div>
<md-theme id="app-theme" theme="momentumV2" class="is-visual-rebrand">
<Engage
key={componentKey}
conversationId={conversationId}
jwtToken={jwtToken}
dataCenter={dataCenter}
onError={handleError}
/>
</md-theme>
</div>
);
};

export {DigitalChannelsComponent};
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {ITask} from '@webex/cc-store';

export interface UseDigitalChannelsInitProps {
currentTask: ITask;
jwtToken: string;
dataCenter: string;
onError?: (error: unknown) => boolean;
logger: {
log: (message: string, meta?: Record<string, unknown>) => void;
error: (message: string, error?: unknown, meta?: Record<string, unknown>) => void;
};
isDigitalChannelsInitialized: boolean;
setDigitalChannelsInitialized: (value: boolean) => void;
}

export interface UseDigitalChannelsProps {
currentTask: ITask;
jwtToken: string;
dataCenter: string;
onError?: (error: unknown) => boolean;
logger?: {
log: (message: string, meta?: Record<string, unknown>) => void;
error: (message: string, error?: unknown, meta?: Record<string, unknown>) => void;
};
}

export interface DigitalChannelsProps {
jwtToken: string;
dataCenter: string;
onError?: (error: unknown) => boolean;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

DigitalChannels dont follow the widget's design.

  • index file is only to bring together the store, helper and presentation.

Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React from 'react';
import {observer} from 'mobx-react-lite';
import {ErrorBoundary} from 'react-error-boundary';

import store from '@webex/cc-store';
import {useDigitalChannels, useDigitalChannelsInit} from '../helper';
import {DigitalChannelsComponent} from './DigitalChannelsComponent';
import {DigitalChannelsProps} from './digital-channels.types';

const DigitalChannelsInternal: React.FunctionComponent<DigitalChannelsProps> = observer(
({jwtToken, dataCenter, onError}) => {
const {logger, currentTask, isDigitalChannelsInitialized, setDigitalChannelsInitialized} = store;

if (!currentTask) {
return null;
}

const {initialized} = useDigitalChannelsInit({
currentTask,
jwtToken,
dataCenter,
onError,
logger,
isDigitalChannelsInitialized,
setDigitalChannelsInitialized,
});

const {handleError, conversationId} = useDigitalChannels({
currentTask,
jwtToken,
dataCenter,
onError,
logger,
});

if (!initialized || !conversationId) {
return null;
}

return (
<DigitalChannelsComponent
conversationId={conversationId}
jwtToken={jwtToken}
dataCenter={dataCenter}
handleError={handleError}
/>
);
}
);

const DigitalChannels: React.FunctionComponent<DigitalChannelsProps> = (props) => {
return (
<ErrorBoundary
fallbackRender={() => <></>}
onError={(error: Error) => {
if (store.onErrorCallback) store.onErrorCallback('DigitalChannels', error);
}}
>
<DigitalChannelsInternal {...props} />
</ErrorBoundary>
);
};

export {DigitalChannels};
Copy link
Contributor

Choose a reason for hiding this comment

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

We need to wrap this in react-error-boundary

103 changes: 103 additions & 0 deletions packages/contact-center/cc-digital-channels/src/helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import {useEffect, useState} from 'react';
import {initializeApp} from '@webex/cc-digital-interactions';

import {UseDigitalChannelsProps, UseDigitalChannelsInitProps} from './digital-channels/digital-channels.types';

/**
* Hook to handle Digital Channels initialization.
* Ensures initialization happens only once per session using store flag.
*/
export const useDigitalChannelsInit = (props: UseDigitalChannelsInitProps) => {
const {
currentTask,
jwtToken,
dataCenter,
onError,
logger,
isDigitalChannelsInitialized,
setDigitalChannelsInitialized,
} = props;

const [initialized, setInitialized] = useState(isDigitalChannelsInitialized);

useEffect(() => {
const initialize = async () => {
// Initialize the digital channels app only once per session
if (!isDigitalChannelsInitialized) {
logger.log(
`[DIGITAL_CHANNELS_INIT] 🚀 Starting Digital Channels initialization for the FIRST TIME (dataCenter: ${dataCenter})...`,
{
module: 'cc-digital-channels',
method: 'useDigitalChannelsInit',
}
);

try {
await initializeApp(dataCenter, jwtToken);
setDigitalChannelsInitialized(true);
setInitialized(true);
logger.log('[DIGITAL_CHANNELS_INIT] ✅ Digital Channels app initialized SUCCESSFULLY', {
module: 'cc-digital-channels',
method: 'useDigitalChannelsInit',
});
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
logger.error(`[DIGITAL_CHANNELS_INIT] ❌ Failed to initialize Digital Channels app: ${errorMessage}`, {
module: 'cc-digital-channels',
method: 'useDigitalChannelsInit',
error,
});
if (onError) {
onError(error);
}
}
} else {
logger.log('[DIGITAL_CHANNELS_INIT] ✅ App already initialized. Skipping re-initialization.', {
module: 'cc-digital-channels',
method: 'useDigitalChannelsInit',
});
setInitialized(true);
}
};

initialize();
}, [currentTask]);

return {initialized};
};

/**
* Hook to derive props for Digital Channels component.
* Extracts conversationId and provides error handling.
*/
export const useDigitalChannels = (props: UseDigitalChannelsProps) => {
const {jwtToken, dataCenter, onError, logger, currentTask} = props;

const handleError = (error: unknown): boolean => {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';

logger?.error('Digital channels error', errorMessage, {
module: 'widget-cc-digital-channels#helper.ts',
method: 'handleError',
});

if (onError) {
return onError(error);
}

// Default error handling
console.debug('Webex Engage component error:', errorMessage);
return false; // Prevent default error handling
};

const conversationId = (currentTask.data.interaction as {callAssociatedDetails?: {mediaResourceId?: string}})
.callAssociatedDetails?.mediaResourceId;

return {
name: 'DigitalChannels',
handleError,
conversationId,
jwtToken,
dataCenter,
};
};
4 changes: 4 additions & 0 deletions packages/contact-center/cc-digital-channels/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import {DigitalChannels} from './digital-channels';

export {DigitalChannels};
export default DigitalChannels;
Copy link
Contributor

Choose a reason for hiding this comment

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

d.ts file are create after build process. we only create .ts files. Just a types.ts would be enough, right?

Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
declare module '@webex/cc-digital-interactions' {
export function initializeApp(dataCenter: string, jwtToken: string): Promise<void>;

const Engage: React.ComponentType<{
conversationId: string;
jwtToken: string;
dataCenter: string;
onError?: (error: unknown) => boolean;
key?: string;
}>;
export default Engage;
}
13 changes: 13 additions & 0 deletions packages/contact-center/cc-digital-channels/src/types/global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Declare custom HTML elements used by the Webex Engage components
Copy link
Contributor

Choose a reason for hiding this comment

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

Since we wont require md-theme, we can get rid of this file as well

declare global {
namespace JSX {
interface IntrinsicElements {
'md-theme': React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> & {
theme?: string;
class?: string;
};
}
}
}

export {};
Loading
Loading