-
Notifications
You must be signed in to change notification settings - Fork 65
feat(cc-digital-channel): add digital channel package #586
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
43946ca
eb2e774
1bca291
7a694f1
efb887c
5ab4742
f8f12a7
f0432e0
598aa3f
6273e60
d3e037d
cd2bbd7
199a7e0
10fca07
97e4ac7
085cb53
eeb2ad1
402aea9
08cdf82
c1ea8d0
9b47030
94ca387
01e6c63
f052ab8
c9ac3ff
f2a9020
f501c60
2d8709f
4cfe337
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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, | ||
| ]; |
| 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; |
| 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-store": "workspace:*", | ||
| "cc-digital-interactions": "3.0.6-beta.1", | ||
| "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,43 @@ | ||
| import React, {useMemo} from 'react'; | ||
| import Engage from 'cc-digital-interactions'; | ||
|
|
||
| import '@momentum-ui/web-components'; | ||
| import {DigitalChannelsComponentProps} from './digital-channels.types'; | ||
|
|
||
| /** | ||
| * Presentation component for Digital Channels. | ||
| * Renders the Engage widget with proper theming. | ||
| */ | ||
| const DigitalChannelsComponent: React.FunctionComponent<DigitalChannelsComponentProps> = ({ | ||
| conversationId, | ||
| jwtToken, | ||
| dataCenter, | ||
| currentTheme = 'LIGHT', | ||
| }) => { | ||
| // 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]); | ||
|
|
||
| const isDarkTheme = currentTheme === 'DARK'; | ||
|
|
||
| return ( | ||
| <div> | ||
| <md-theme id="app-theme" theme="momentumV2" {...(isDarkTheme ? {darktheme: true} : {lighttheme: true})}> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did we confirm if this is required? To use widgets we have asked dev to wrap the app in Theme and Icon Provider, so if a developer does that and here we are again wrapping the component that mean the Engage widget is wrapped twice. Just wanna ensure this doesnt break anything. Maybe its worthwhile to check with momentum team about this, We are wrapping a web-component in a react component.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Confirmed with Digital Channels team. They strictly require this. Engage Widget should be wrapped inside the md-theme. |
||
| <Engage | ||
| key={componentKey} | ||
| conversationId={conversationId} | ||
| jwtToken={jwtToken} | ||
| dataCenter={dataCenter} | ||
| interactionId="" | ||
| readonly={false} | ||
| theme={isDarkTheme ? 'dark' : 'light'} | ||
| isVisualRebrand={true} | ||
| /> | ||
| </md-theme> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export {DigitalChannelsComponent}; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| import {ITask} from '@webex/cc-store'; | ||
|
|
||
| export interface DigitalChannelsInitHookProps { | ||
| currentTask: ITask; | ||
| jwtToken: string; | ||
| dataCenter: string; | ||
| 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; | ||
| skipInit?: boolean; | ||
| } | ||
|
|
||
| export interface DigitalChannelsDataHookProps { | ||
| getAccessToken: () => Promise<string>; | ||
| currentTask: ITask | null; | ||
| logger?: { | ||
| log: (message: string, meta?: Record<string, unknown>) => void; | ||
| error: (message: string, meta?: Record<string, unknown>) => void; | ||
| }; | ||
| } | ||
|
|
||
| export interface DigitalChannelsProps { | ||
| currentTheme?: string; | ||
| } | ||
|
|
||
| export interface DigitalChannelsComponentProps { | ||
| conversationId: string; | ||
| jwtToken: string; | ||
| dataCenter: string; | ||
| currentTheme?: string; | ||
| } |
|
Shreyas281299 marked this conversation as resolved.
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| import React from 'react'; | ||
| import {observer} from 'mobx-react-lite'; | ||
| import {ErrorBoundary} from 'react-error-boundary'; | ||
|
|
||
| import store from '@webex/cc-store'; | ||
| import {useDigitalChannelsInit, useDigitalChannelsData} from '../helper'; | ||
| import {DigitalChannelsComponent} from './DigitalChannelsComponent'; | ||
| import {DigitalChannelsProps} from './digital-channels.types'; | ||
|
|
||
| const DigitalChannelsInternal: React.FunctionComponent<DigitalChannelsProps> = observer(({currentTheme}) => { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it possible to move this to another file just for organisation perspective perhaps. |
||
| const {logger, currentTask, isDigitalChannelsInitialized, setDigitalChannelsInitialized, getAccessToken, dataCenter} = | ||
| store; | ||
|
|
||
| // Fetch JWT token and conversation ID | ||
| const {jwtToken, conversationId, hasError} = useDigitalChannelsData({ | ||
| getAccessToken, | ||
| currentTask, | ||
| logger, | ||
| }); | ||
|
|
||
| // Initialize Digital Channels app once we have all required data | ||
| const {initialized} = useDigitalChannelsInit({ | ||
| currentTask: currentTask || ({} as typeof currentTask), | ||
| jwtToken: jwtToken || '', | ||
| dataCenter: dataCenter || '', | ||
| logger, | ||
| isDigitalChannelsInitialized, | ||
| setDigitalChannelsInitialized, | ||
| // Skip initialization if we don't have required data | ||
| skipInit: !currentTask || !jwtToken || !dataCenter, | ||
| }); | ||
|
|
||
| // Early return after all hooks are called | ||
| if (!currentTask || !jwtToken || !dataCenter || hasError || !initialized || !conversationId) { | ||
| return null; | ||
| } | ||
|
|
||
| return ( | ||
| <DigitalChannelsComponent | ||
| conversationId={conversationId} | ||
| jwtToken={jwtToken} | ||
| dataCenter={dataCenter} | ||
| currentTheme={currentTheme} | ||
| /> | ||
| ); | ||
| }); | ||
|
|
||
| const DigitalChannels: React.FunctionComponent<DigitalChannelsProps> = (props) => { | ||
| return ( | ||
| <ErrorBoundary | ||
| fallbackRender={() => <></>} | ||
| onError={(error: Error) => { | ||
| if (store.onErrorCallback) store.onErrorCallback('DigitalChannels', error); | ||
| }} | ||
| > | ||
| <DigitalChannelsInternal {...props} /> | ||
| </ErrorBoundary> | ||
| ); | ||
| }; | ||
|
|
||
| export {DigitalChannels}; | ||
|
Shreyas281299 marked this conversation as resolved.
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,116 @@ | ||
| import {useEffect, useState, useMemo} from 'react'; | ||
| import {initializeApp} from 'cc-digital-interactions'; | ||
|
|
||
| import {DigitalChannelsInitHookProps, DigitalChannelsDataHookProps} 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: DigitalChannelsInitHookProps) => { | ||
| const { | ||
| currentTask, | ||
| jwtToken, | ||
| dataCenter, | ||
| logger, | ||
| isDigitalChannelsInitialized, | ||
| setDigitalChannelsInitialized, | ||
| skipInit = false, | ||
| } = props; | ||
|
|
||
| const [initialized, setInitialized] = useState(isDigitalChannelsInitialized); | ||
|
|
||
| useEffect(() => { | ||
| // Skip initialization if required data is not available | ||
| if (skipInit) { | ||
| return; | ||
| } | ||
|
|
||
| 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, | ||
| }); | ||
| } | ||
| } else { | ||
| logger.log('[DIGITAL_CHANNELS_INIT] ✅ App already initialized. Skipping re-initialization.', { | ||
| module: 'cc-digital-channels', | ||
| method: 'useDigitalChannelsInit', | ||
| }); | ||
| setInitialized(true); | ||
| } | ||
| }; | ||
|
|
||
| initialize(); | ||
| }, [currentTask, skipInit, jwtToken]); | ||
|
|
||
| return {initialized}; | ||
| }; | ||
|
|
||
| /** | ||
| * Hook to handle fetching Digital Channels data (token and conversationId). | ||
| * Centralizes token fetching logic to keep the component clean. | ||
| */ | ||
| export const useDigitalChannelsData = (props: DigitalChannelsDataHookProps) => { | ||
| const {getAccessToken, currentTask, logger} = props; | ||
|
|
||
| const [jwtToken, setJwtToken] = useState<string>(''); | ||
| const [tokenError, setTokenError] = useState<boolean>(false); | ||
|
|
||
| // Fetch access token from the store | ||
| useEffect(() => { | ||
| const fetchToken = async () => { | ||
| try { | ||
| const token = await getAccessToken(); | ||
| setJwtToken(token); | ||
| } catch (error) { | ||
| logger?.error('[DIGITAL_CHANNELS] ❌ Failed to get access token', { | ||
| module: 'cc-digital-channels', | ||
| method: 'useDigitalChannelsData.fetchToken', | ||
| error, | ||
| }); | ||
| setTokenError(true); | ||
| } | ||
| }; | ||
| fetchToken(); | ||
| }, [getAccessToken, logger]); | ||
|
|
||
| // Extract conversationId from currentTask (always call this, return empty string if no task) | ||
| const conversationId = useMemo(() => { | ||
| if (!currentTask) return ''; | ||
| return ( | ||
| (currentTask.data.interaction as {callAssociatedDetails?: {mediaResourceId?: string}}).callAssociatedDetails | ||
| ?.mediaResourceId || '' | ||
| ); | ||
| }, [currentTask]); | ||
|
|
||
| const hasError = tokenError; | ||
|
|
||
| return { | ||
| jwtToken, | ||
| conversationId, | ||
| tokenError, | ||
| hasError, | ||
| }; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| import {DigitalChannels} from './digital-channels'; | ||
|
|
||
| export {DigitalChannels}; | ||
| export default DigitalChannels; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| declare global { | ||
| namespace JSX { | ||
| interface IntrinsicElements { | ||
| 'md-theme': React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> & { | ||
| theme?: string; | ||
| class?: string; | ||
| darktheme?: boolean; | ||
| lighttheme?: boolean; | ||
| }; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| export {}; |
Uh oh!
There was an error while loading. Please reload this page.