diff --git a/.gitignore b/.gitignore index 71545b7..dd11e69 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ output.html packages/*/dist .idea lerna-debug.log +.Ds_Store diff --git a/packages/adapter-ameyo/CHANGELOG.md b/packages/adapter-ameyo/CHANGELOG.md index dc1a9c6..e379c5e 100644 --- a/packages/adapter-ameyo/CHANGELOG.md +++ b/packages/adapter-ameyo/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## 1.1.15 (2025-01-16) + +**Note:** Version bump only for package @universal-call-sdk/adapter-ameyo + + + + + ## 1.0.100 (2025-01-06) **Note:** Version bump only for package @universal-call-sdk/adapter-ameyo diff --git a/packages/adapter-ameyo/lib/assets/js/ajaxClient.ts b/packages/adapter-ameyo/lib/assets/js/ajaxClient.ts index 8c192fe..0e6fbca 100644 --- a/packages/adapter-ameyo/lib/assets/js/ajaxClient.ts +++ b/packages/adapter-ameyo/lib/assets/js/ajaxClient.ts @@ -1,9 +1,8 @@ // eslint-disable-next-line @typescript-eslint/ban-ts-comment //@ts-nocheck - -import messagingType from "../../../types/MessagingType.ts"; -import {parse} from "path"; +import {callStateManager} from "../../callStateManager.ts"; +import {EventData} from "../../types.ts"; export enum MessagingType { ON_AMEYO_CALL_INCOMING = 'onAmeyoCallIncoming', @@ -42,157 +41,6 @@ const monitorPushTime = 60 * 1000; //call the above function to create the XMLHttpRequest object let http = createRequestObject(); -enum pushResponseTypes { - UserCallModelUpdatedPush = 'UserCallModelUpdatedPush', - CustomerCallMemberUpdatedPush = 'CustomerCallMemberUpdatedPush', - UserCallMemberUpdatedPush = 'UserCallMemberUpdatedPush', - CRMCreateNotifyPush = 'CRMCreateNotifyPush', - UserCCRuntimeUpdatedPush = 'UserCCRuntimeUpdatedPush', - UserLoggedOffPush = 'UserLoggedOffPush' -} - -export function parseQuerystring(url: string): Record { - const foo = url?.split('?')?.[1]?.split('&') || []; - const dict = {}; - let elem = []; - for (let i = foo.length - 1; i >= 0; i--) { - elem = foo[i].split('='); - dict[elem[0]] = elem[1]; - } - return dict || {}; -} - -function extractUserCallModelUpdatedPush(rawResponse) { - const parts = rawResponse - .replaceAll(`Content-type: application/json ; charset: utf-8\r\n\r\n`, '') - .split("--DON'T_PANIC!_more_will_follow\r\n") - .map((part) => { - try { - return JSON.parse(part.trim()); - } catch { - return null; // Ignore parsing errors - } - }) - .filter(Boolean); // Remove null values - - // Prioritize CRMCreateNotifyPush and keep the rest - const sortedParts = [ - ...parts.filter(part => part?.pushType === pushResponseTypes.CRMCreateNotifyPush), - ...parts.filter(part => part?.pushType !== pushResponseTypes.CRMCreateNotifyPush), - ]; - - sortedParts.forEach(jsonData => { - if (jsonData?.pushType === pushResponseTypes.CRMCreateNotifyPush) { - const crtObjectId = jsonData?.data?.crtObjectId; - const parsedObject = parseQuerystring(jsonData?.data?.crmURL); - let userCRTObjectId = parsedObject?.userCrtObjectId?.replace('%40', '@'); - userCRTObjectId = userCRTObjectId.replace('%40', '@'); - const phoneNumber = parsedObject?.phone; - const lan = parsedObject?.loanaccountnumber; - const callId = parsedObject?.unique_id || parsedObject?.callid; - localStorage.setItem( - 'revEngCustomerInfo', - JSON.stringify({phoneNumber, lan, crtObjectId, userCRTObjectId, callId}) - ); - } - if (jsonData?.pushType === pushResponseTypes.UserCallModelUpdatedPush) { - sendCallStatusMessage(jsonData); - } - if (jsonData?.pushType === pushResponseTypes.CustomerCallMemberUpdatedPush) { - const payload = jsonData?.data; - // handle for transfer call - if (payload?.isDisposing && - payload?.status === 'hungup') - sendCallStatusMessage(jsonData); - } - if (jsonData?.pushType === pushResponseTypes.UserCallMemberUpdatedPush) { - const payload = jsonData?.data; - const validStatuses = ["ringing", "connected", "hungup"]; - // handle for transfer call - if (payload?.associationType === 'transfer.association' && validStatuses.includes(payload?.status)) { - window.postMessage({ - type: MessagingType.ON_AMEYO_CALL_TRANSFER, - data: payload - }) - } - } - if (jsonData?.pushType === pushResponseTypes.UserCCRuntimeUpdatedPush) { - const payload = jsonData?.data; - //handle ameyo availablility changes - window.postMessage({ - type: messagingType.ON_AMEYO_AVAILABILITY_CHANGE, - data:{ - isOnBreak: payload?.isOnBreak, - reason: payload?.statusDescription - } - }) - } - if (jsonData?.pushType == pushResponseTypes.UserLoggedOffPush) { - const payload = jsonData?.data; - window.postMessage({ - type: MessagingType.ON_AMEYO_FORCED_LOGOUT, - data: {} - }) - } - } ) -} - -const sendCallStatusMessage = (res) => { - try { - const status = res?.data?.status; - - const getCustomerInfo = () => - JSON.parse(localStorage.getItem('revEngCustomerInfo') || '{}'); - - const sendConnectedMessage = (retryCount = 3) => { - /* The callId is received after the call is connected. - * Since callId is essential and cannot be null, - * this retry mechanism ensures we wait until it becomes available. */ - const data = getCustomerInfo(); - if (data?.callId || data?.phoneNumber) { - const message = { - type: MessagingType.ON_AMEYO_CALL_ACCEPTED, - data, - }; - window.postMessage(message); - } else if (retryCount > 0) { - setTimeout(() => sendConnectedMessage(retryCount - 1), 100); - } else { - console.warn('Unable to send ON_AMEYO_CALL_ACCEPTED: callId not found.'); - } - }; - - switch (status) { - case 'ringing': { - const data = getCustomerInfo(); - const message = { - type: MessagingType.ON_AMEYO_CALL_INCOMING, - data, - }; - window.postMessage(message); - break; - } - case 'connected': { - sendConnectedMessage(); - break; - } - case 'hungup': { - const data = getCustomerInfo(); - const message = { - type: MessagingType.ON_AMEYO_CALL_DISCONNECTED, - data, - }; - window.postMessage(message); - localStorage.removeItem('revEngCustomerInfo'); - break; - } - default: - console.warn(`Unhandled call status: ${status}`); - } - } catch (error) { - console.error('Error in sendCallStatusMessage:', error); - } -}; function createRequestObject() { let tmpXmlHttpObject; @@ -262,9 +110,30 @@ function processResponse() { // response } +function extractAndProcessEvents(rawResponse: any) { + const parts = rawResponse + .replaceAll(`Content-type: application/json ; charset: utf-8\r\n\r\n`, '') + .split("--DON'T_PANIC!_more_will_follow\r\n") + .map((part: string) => { + try { + return JSON.parse(part.trim()); + } catch { + return null; + } + }) + .filter(Boolean); + + parts.forEach((event : EventData) => { + try { + callStateManager.handleEvent(event); + } catch (error) { + console.error('Error processing event:', event, error); + } + }); +} + function processPush(pushResponse) { - extractUserCallModelUpdatedPush(pushResponse); - //ajaxRequest.handleIntermediateResponse(pushResponse); + extractAndProcessEvents(pushResponse); } function makePostRequest(url, session) { diff --git a/packages/adapter-ameyo/lib/callStateManager.ts b/packages/adapter-ameyo/lib/callStateManager.ts new file mode 100644 index 0000000..be94035 --- /dev/null +++ b/packages/adapter-ameyo/lib/callStateManager.ts @@ -0,0 +1,189 @@ +import {MessagingType} from "./assets/js/ajaxClient.ts"; +import {CallStatus, PushType, CallState} from "./types.ts"; +import {parseQuerystring} from "@universal-call-sdk/common/lib/utils/parsingUtils.ts"; +import messagingType from "../types/MessagingType.ts"; + +class CallStateManager { + private state: CallState = { + isConnected: false, + lastCallStatus: 'inactive', + customerCallStatus: null, + crtObjectId: null, + connectionAttempts: 0, + customerInfo: null + }; + + private readonly MAX_CONNECTION_ATTEMPTS = 3; + private connectionTimeout: ReturnType | null = null; + + public sendConnectedMessage() { + const customerInfo = this.state.customerInfo; + + if (customerInfo?.callId || customerInfo?.phoneNumber) { + window.postMessage({ + type: MessagingType.ON_AMEYO_CALL_ACCEPTED, + data: customerInfo + }); + this.state.connectionAttempts = 0; + this.clearConnectionTimeout(); + } else if (this.state.connectionAttempts < this.MAX_CONNECTION_ATTEMPTS) { + this.state.connectionAttempts++; + this.setConnectionTimeout(); + } else { + console.warn('Max connection attempts reached without valid customer info'); + this.clearConnectionTimeout(); + } + } + + private setConnectionTimeout() { + this.clearConnectionTimeout(); + this.connectionTimeout = setTimeout(() => { + this.sendConnectedMessage(); + }, 100 * Math.pow(2, this.state.connectionAttempts)); // Exponential backoff + } + + private clearConnectionTimeout() { + if (this.connectionTimeout) { + clearTimeout(this.connectionTimeout); + this.connectionTimeout = null; + } + } + + public handleEvent(jsonData: any) { + const pushType = jsonData?.pushType; + const data = jsonData?.data; + + switch (pushType) { + case PushType.UserCallModelUpdated: + this.handleCallModelUpdate(data); + break; + + case PushType.CustomerCallMemberUpdated: + this.handleCustomerCallUpdate(data); + break; + + case PushType.CustomerCallMemberCreated: + this.handleCustomerCallCreated(data); + break; + + case PushType.CRMCreateNotify: + this.handleCRMCreate(data); + break; + + case PushType.UserCallMemberUpdated: + this.handleUserCallMemberUpdate(data); + break; + + case PushType.UserCCRuntimeUpdated: + this.handleUserCCRuntimeUpdate(data); + break; + + case PushType.UserLoggedOff: + this.handleUserLoggedOff(); + break; + } + } + + private handleCallModelUpdate(data: any) { + const status = data?.status as CallStatus; + this.state.lastCallStatus = status; + + switch (status) { + case 'ringing': + window.postMessage({ + type: MessagingType.ON_AMEYO_CALL_INCOMING, + data: this.state.customerInfo + }); + break; + case 'hungup': + this.handleCallDisconnect(); + break; + } + } + + private handleCustomerCallUpdate(data: any) { + this.state.customerCallStatus = data?.status; + this.state.crtObjectId = data?.crtObjectId; + + // handle for transfer call + if (data?.isDisposing && data?.status === 'hungup') { + this.handleCallDisconnect(); + } + } + + private handleCustomerCallCreated(data: any) { + this.state.customerInfo = { + ...this.state.customerInfo, + phoneNumber: data?.phone, + crtObjectId: data?.crtObjectId, + callId: data?.callId + }; + } + + private handleCRMCreate(data: any) { + const crmURL = data?.crmURL; + if (!crmURL) return; + + const parsedQuery = parseQuerystring(crmURL); + const userCRTObjectId = parsedQuery?.userCrtObjectId?.replaceAll('%40', '@'); + + this.state.customerInfo = { + ...this.state.customerInfo, + phoneNumber: parsedQuery?.phone, + lan: parsedQuery?.loanaccountnumber, + crtObjectId: data?.crtObjectId, + userCRTObjectId, + callId: parsedQuery?.unique_id || parsedQuery?.callid + }; + } + + private handleUserCallMemberUpdate(data: any) { + const validStatuses = ["ringing", "connected", "hungup"]; + + if (data?.associationType === 'transfer.association' && + validStatuses.includes(data?.status)) { + window.postMessage({ + type: MessagingType.ON_AMEYO_CALL_TRANSFER, + data + }); + } + } + + private handleUserCCRuntimeUpdate(data: any) { + const payload = data; + window.postMessage({ + type: messagingType.ON_AMEYO_AVAILABILITY_CHANGE, + data:{ + isOnBreak: payload?.isOnBreak, + reason: payload?.statusDescription + } + }) + } + + private handleUserLoggedOff() { + window.postMessage({ + type: MessagingType.ON_AMEYO_FORCED_LOGOUT, + data: {} + }); + } + + private handleCallDisconnect() { + window.postMessage({ + type: MessagingType.ON_AMEYO_CALL_DISCONNECTED, + data: this.state.customerInfo + }); + + // Reset state + this.state = { + isConnected: false, + lastCallStatus: 'inactive', + customerCallStatus: null, + crtObjectId: null, + connectionAttempts: 0, + customerInfo: null + }; + this.clearConnectionTimeout(); + } +} + +export const callStateManager = new CallStateManager(); diff --git a/packages/adapter-ameyo/lib/htmlTagManager.ts b/packages/adapter-ameyo/lib/htmlTagManager.ts new file mode 100644 index 0000000..e5bf0dd --- /dev/null +++ b/packages/adapter-ameyo/lib/htmlTagManager.ts @@ -0,0 +1,156 @@ +type TagConfig = { + id: string; + type: 'audio' | 'video'; + attributes?: Record; + styles?: Partial; +} + +class TagManager { + private static instance: TagManager; + private initialized: boolean = false; + private readonly scriptUrl = 'https://public-assets.np.navi-gi.in/jarvis/sip5ml.js'; + + private readonly tagConfigs: TagConfig[] = [ + { + id: 'audio_remote', + type: 'audio', + attributes: { + autoplay: true + } + }, + { + id: 'video_local', + type: 'video', + attributes: { + className: 'video', + width: '100%', + height: '100%', + autoplay: true, + muted: true + }, + styles: { + opacity: '0', + display: 'none', + backgroundColor: '#000000', + webkitTransitionProperty: 'opacity', + webkitTransitionDuration: '2s' + } + }, + { + id: 'video_remote', + type: 'video', + attributes: { + className: 'video', + width: '100%', + height: '100%', + autoplay: true + }, + styles: { + opacity: '0', + display: 'none', + backgroundColor: '#000000', + webkitTransitionProperty: 'opacity', + webkitTransitionDuration: '2s' + } + }, + { + id: 'ringtone', + type: 'audio', + attributes: { + loop: true, + src: 'https://public-assets.np.navi-gi.in/jarvis/ringtone.wav' + } + }, + { + id: 'ringbacktone', + type: 'audio', + attributes: { + loop: true, + src: 'https://public-assets.np.navi-gi.in/jarvis/ringbacktone.wav' + } + }, + { + id: 'dtmfTone', + type: 'audio', + attributes: { + src: 'https://public-assets.np.navi-gi.in/jarvis/dtmf.wav' + } + }, + { + id: 'beep', + type: 'audio', + attributes: { + src: 'https://public-assets.np.navi-gi.in/jarvis/beep.wav' + } + } + ]; + + private constructor() {} + + public static getInstance(): TagManager { + if (!TagManager.instance) { + TagManager.instance = new TagManager(); + } + return TagManager.instance; + } + + private createTag(config: TagConfig): HTMLElement { + const element = document.createElement(config.type); + + // Set attributes + if (config.attributes) { + Object.entries(config.attributes).forEach(([key, value]) => { + if (typeof value === 'boolean') { + if (value) element.setAttribute(key, ''); + } else { + element.setAttribute(key, value); + } + }); + } + + // Set styles + if (config.styles) { + Object.assign(element.style, config.styles); + } + + element.id = config.id; + return element; + } + + private areTagsPresent(): boolean { + return this.tagConfigs.every(config => document.getElementById(config.id)); + } + + private appendScript(): Promise { + return new Promise((resolve) => { + const script = document.createElement('script'); + script.src = this.scriptUrl; + script.async = true; + script.onload = () => { + const event = new CustomEvent('onSipSetupReady', { + detail: { message: 'SIP setup loaded' } + }); + window.dispatchEvent(event); + resolve(); + }; + document.head.appendChild(script); + }); + } + + public async initialize(): Promise { + if (this.initialized || this.areTagsPresent()) { + console.log('Tags already initialized, skipping'); + return; + } + + this.tagConfigs.forEach(config => { + const element = this.createTag(config); + document.body.appendChild(element); + }); + + await this.appendScript(); + this.initialized = true; + } +} + +export const tagManager = TagManager.getInstance(); diff --git a/packages/adapter-ameyo/lib/main.ts b/packages/adapter-ameyo/lib/main.ts index 67034c4..4ccf852 100644 --- a/packages/adapter-ameyo/lib/main.ts +++ b/packages/adapter-ameyo/lib/main.ts @@ -1,50 +1,46 @@ -import IAdapter from "@universal-call-sdk/common/lib/Interfaces/IAdapter.ts"; -import GenericObject from "@universal-call-sdk/common/lib/types/GenericObject.ts"; +import IAdapter from "@universal-call-sdk/common/lib/Interfaces/IAdapter"; +import GenericObject from "@universal-call-sdk/common/lib/types/GenericObject"; import { AmeyoInitializationOptions, - CALL_STATES, + CALL_STATES, CallbackFunctions, CallTransferData, RequestKeys, SipAccountInfo, StateType -} from "./types.ts"; -import MessagingType from "../types/MessagingType.ts"; +} from "./types"; import { ameyoHangupUser, attachOmniqueService, autoSelectExtension, getSipAccountInfo, - loginInAmeyo, maintainHeartbeat, + loginInAmeyo, + maintainHeartbeat, selectCampaign, - setAgentActive, setAgentOnBreak, + setAgentActive, + setAgentOnBreak, setAutoStatus, - getCampaignId, ameyoDisposeCall, - getAllAgentsForTransferCall, transferCallToAgent, logoutFromAmeyo -} from "./api.ts"; + getCampaignId, + ameyoDisposeCall, + getAllAgentsForTransferCall, + transferCallToAgent, logoutFromAmeyo +} from "./api"; import { acceptSipCall, loadCallOptions, loadCredentials, sipHangUp, sipMuteCall, - sipRegister, sipUnmuteCall -} from "./assets/js/sip5ml.service.ts"; -import registerEventProcessor from "./eventsProcessor.ts"; -import MetricsProcessor from "@universal-call-sdk/common/lib/utils/metricsProcessor.ts"; -import ClickStreamProcessor from "@universal-call-sdk/common/lib/utils/clickStreamProcessor.ts"; - + sipRegister, + sipUnmuteCall +} from "./assets/js/sip5ml.service"; +import registerEventProcessor from "./eventsProcessor"; +import MetricsProcessor from "@universal-call-sdk/common/lib/utils/metricsProcessor"; +import ClickStreamProcessor from "@universal-call-sdk/common/lib/utils/clickStreamProcessor"; +import {tagManager} from "./htmlTagManager"; +import {callStateManager} from "./callStateManager.ts"; +import MessagingType from "../types/MessagingType.ts"; class AmeyoAdapter implements IAdapter { - private callbacks: { - onCallConnected: (data: StateType) => void; - onCallDisconnected: (data: StateType) => void; - onCallIncoming: (data: StateType) => void, - onAdapterReady: () => void, - onAgentAvailabilityChange: (isAgentAvailable: boolean, reason: string) => void - onForcedLogout: () => void, - onLoginFailed: (err: GenericObject)=>void - onAgentsForCallTransfer: (data: GenericObject) => void - onCallTransferStatus: (data: GenericObject) => void; - }; + private callbacks: CallbackFunctions; private currentCallState: CALL_STATES; private eventListenerUrl: string; private baseUrl: string; @@ -55,19 +51,24 @@ class AmeyoAdapter implements IAdapter { private currentCallMetadata: GenericObject; private sipAccountInfo: GenericObject; private isAgentAvailable: boolean; - private clickStreamProcessor: ClickStreamProcessor ; + private clickStreamProcessor: ClickStreamProcessor; private metricProcessor: MetricsProcessor; + constructor(options: AmeyoInitializationOptions) { - - if(document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', this._appendTags); - } else { - this._appendTags(); - } - + this.validateInitializationOptions(options); this.baseUrl = options.baseUrl; this.eventListenerUrl = options.eventListenerUrl; + this.userName = options.userName; + this.password = options.password; this.currentCallState = CALL_STATES.IDLE; + this.sessionId = ''; + this.campaignId = ''; + this.sipAccountInfo = {}; + this.currentCallMetadata = {}; + this.isAgentAvailable = false; + this.clickStreamProcessor = {} as ClickStreamProcessor; + this.metricProcessor = {} as MetricsProcessor; + this.callbacks = { onCallIncoming: () => { }, @@ -81,384 +82,369 @@ class AmeyoAdapter implements IAdapter { }, onForcedLogout: () => { }, + onLoginFailed: () => { + }, onAgentsForCallTransfer: () => { }, onCallTransferStatus: () => { - }, - onLoginFailed: ()=>{ - } }; - this.sessionId = ''; - this.userName = options.userName; - this.password = options.password; - this.campaignId = ''; - this.sipAccountInfo = {}; - this.currentCallMetadata = {}; - this.isAgentAvailable = false; - this.clickStreamProcessor = {} as ClickStreamProcessor; - this.metricProcessor={} as MetricsProcessor; - window.BASE_AMEYO_URL = this.baseUrl; - window.AMEYO_LOGIN_URL = options.loginUrl + this.setupGlobalVariables(options); + this.initializeTagManager(); } - init(metricProcessor: MetricsProcessor, clickStreamProcessor: ClickStreamProcessor) { - window.addEventListener('message', this._registerMessageListener); - this._initializeAmeyo(); + private validateInitializationOptions(options: AmeyoInitializationOptions): void { + if (!options.baseUrl || !options.eventListenerUrl || !options.userName || !options.password || !options.loginUrl) { + throw new Error('Missing required initialization options'); + } + } + + + + private setupGlobalVariables(options: AmeyoInitializationOptions): void { + window.BASE_AMEYO_URL = this.baseUrl; + window.AMEYO_LOGIN_URL = options.loginUrl; + } + + private initializeTagManager(): void { + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', () => tagManager.initialize()); + } else { + tagManager.initialize(); + } + } + + public init(metricProcessor: MetricsProcessor, clickStreamProcessor: ClickStreamProcessor): void { + window.addEventListener('message', this.handleMessage); + this.initializeAmeyo(); this.metricProcessor = metricProcessor; this.clickStreamProcessor = clickStreamProcessor; - this.metricProcessor?.pushCounterMetric({metricName: 'initSdk', flow: "sdk_init_count"}) + this.metricProcessor?.pushCounterMetric({ + metricName: 'initSdk', + flow: "sdk_init_count" + }); } - - _initializeSipStack = ({accountName, userName, domain = "", password}: SipAccountInfo) => { - const domainOnly = domain?.split?.(':')?.[0]; - const port = domain?.split?.(':')?.[1]; - //initialize sip stack - loadCredentials({accountName, userName, domain, password}); - loadCallOptions(); - sipRegister({domain: domainOnly, port}); - this.metricProcessor?.pushCounterMetric({metricName: 'sipStackInitialised',flow: 'ameyo-sip-stack_init_count'}) - - - } - - _initializeAmeyo = () => { - loginInAmeyo(this.userName?.toLowerCase(), this.password); - - } - - - _getElementFromDomById = (elementId: string): HTMLElement | null => { - return document.getElementById(elementId); + private initializeSipStack = ({accountName, userName, domain = "", password}: SipAccountInfo): void => { + try { + const [domainOnly, port] = domain.split(':'); + loadCredentials({accountName, userName, domain, password}); + loadCallOptions(); + sipRegister({domain: domainOnly, port}); + this.metricProcessor?.pushCounterMetric({ + metricName: 'sipStackInitialised', + flow: 'ameyo-sip-stack_init_count' + }); + } catch (error) { + console.error('Error initializing SIP stack:', error); + this.handleError('SIP_INIT_ERROR', error); + } }; - _onListenForCorsBypassResponse = (payload: GenericObject) => { - if(payload?.data?.requestKey !== RequestKeys.AMEYO_HEARTBEAT && !payload?.data?.err) { + private initializeAmeyo = (): void => { + loginInAmeyo(this.userName.toLowerCase(), this.password); + }; + + private handleMessage = async ({data}: MessageEvent): Promise => { + try { + if (this.shouldTrackMetrics(data.type)) { + this.trackEventMetrics(data.type); + } + + switch (data.type) { + case MessagingType.SET_RESPONSE_WITHOUT_CORS: + await this.handleCorsBypassResponse(data); + break; + case MessagingType.ON_AMEYO_CALL_INCOMING: + this.handleCallIncoming(data); + break; + case MessagingType.ON_AMEYO_CALL_ACCEPTED: + this.handleCallAccepted(data); + break; + case MessagingType.ON_AMEYO_CALL_DISCONNECTED: + await this.handleCallDisconnected(data); + break; + case MessagingType.ON_AMEYO_AVAILABILITY_CHANGE: + this.handleAgentAvailabilityChange(data); + break; + case MessagingType.ON_AMEYO_FORCED_LOGOUT: + this.callbacks.onForcedLogout(); + break; + case MessagingType.ON_AMEYO_CALL_TRANSFER: + this.callbacks.onCallTransferStatus(data.data); + break; + } + } catch (error) { + console.error('Error handling message:', error); + this.handleError('MESSAGE_HANDLING_ERROR', error); + } + }; + + private shouldTrackMetrics(messageType: string): boolean { + return messageType !== MessagingType.SET_RESPONSE_WITHOUT_CORS && + messageType !== MessagingType.GET_RESPONSE_WITHOUT_CORS; + } + + private trackEventMetrics(messageType: string): void { + this.metricProcessor?.pushCounterMetric({ + metricName: `ameyo-events-count`, + flow: 'api-events-count', + subFlow: `universal-call-sdk-${messageType}` + }); + } + + private handleError(type: string, error: any): void { + this.metricProcessor?.pushCounterMetric({ + metricName: `ameyo-error-${type}`, + flow: 'error-count', + subFlow: type + }); + this.clickStreamProcessor?.sendClickStreamEvent({ + type: 'error', + error: error.message || error + }); + } + + private async handleCorsBypassResponse(payload: GenericObject): Promise { + this.trackApiMetrics(payload); + + switch (payload?.data?.requestKey) { + case RequestKeys.AMEYO_LOGIN: + await this.handleLoginResponse(payload); + break; + case RequestKeys.SIP_ACCOUNT_INFO: + await this.handleSipAccountInfo(payload); + break; + case RequestKeys.GET_CAMPAIGN_ID: + await this.handleCampaignId(payload); + break; + case RequestKeys.AMEYO_AVAILABLE: + this.handleAgentAvailable(); + break; + case RequestKeys.OMNIQUEUE_SERVICE: + await this.handleOmniQueueService(); + break; + case RequestKeys.SELECT_CAMPAIGN: + await this.handleCampaignSelection(); + break; + case RequestKeys.AMEYO_ON_BREAK: + this.handleAgentBreak(); + break; + case RequestKeys.GET_AGENTS_FOR_CALL_TRANSFER: + this.handleAgentsForTransfer(payload); + break; + } + } + + private trackApiMetrics(payload: GenericObject): void { + if (payload?.data?.requestKey !== RequestKeys.AMEYO_HEARTBEAT && !payload?.data?.err) { this.metricProcessor?.pushCounterMetric({ - metricName: `ameyo-api-call-count`, + metricName: 'ameyo-api-call-count', flow: 'api-call-count', subFlow: payload?.data?.requestKey - }) + }); this.metricProcessor?.pushHistogramMetric({ metricName: `ameyo-api-latency-${payload?.data?.requestKey}`, flow: 'api-latency', subFlow: payload?.data?.requestKey, - value: payload?.data?.time/1000 || 0 //converting to seconds - }) - } - - if(payload?.data?.err) { - this.metricProcessor?.pushCounterMetric({metricName: `ameyo-api-err-count`, flow: 'api-error-count', subFlow: payload?.data?.requestKey}) - this.clickStreamProcessor?.sendClickStreamEvent({type: 'api-error', err: payload}); - } - - if (payload?.data?.requestKey === RequestKeys.AMEYO_LOGIN) { - if(payload?.data?.err) { - console.log('on login failed', payload?.data?.err); - this.callbacks.onLoginFailed(payload?.err); - } - const sessionId = payload?.data?.response?.userSessionInfo?.sessionId; - this.sessionId = sessionId; - getSipAccountInfo(sessionId, this.userName?.toLowerCase()); - registerEventProcessor(this.eventListenerUrl, sessionId); - maintainHeartbeat(this.sessionId, window?.listenerName || '', 0); - } - if (payload?.data?.requestKey === RequestKeys.AMEYO_HEARTBEAT) { - } - if (payload?.data?.requestKey === RequestKeys.SIP_ACCOUNT_INFO) { - const response = payload?.data?.response; - this._initializeSipStack({ - accountName: response?.accountName, - userName: response?.userName, - domain: response?.domain, - password: response?.secret + value: payload?.data?.time / 1000 || 0 }); - this.sipAccountInfo = payload?.data?.response; - console.log('sip account info', this.sipAccountInfo) - getCampaignId(this.sessionId); - setAutoStatus(this.sessionId); } - if (payload?.data?.requestKey === RequestKeys.GET_CAMPAIGN_ID) { - this.campaignId = payload?.data?.response?.campaignInfos?.[0]?.campaignId; - attachOmniqueService(this.sessionId, this.userName.toLowerCase(), this.campaignId); - } - if (payload?.data?.requestKey === RequestKeys.AMEYO_AVAILABLE) { - setAutoStatus(this.sessionId); - } - if (payload?.data?.requestKey === RequestKeys.OMNIQUEUE_SERVICE) { - selectCampaign(this.sessionId, this.userName.toLowerCase(), this.campaignId); - } - if (payload?.data?.requestKey === RequestKeys.SELECT_CAMPAIGN) { - autoSelectExtension(this.sessionId, this.userName.toLowerCase()); - this.callbacks.onAdapterReady(); - this.currentCallState = CALL_STATES.IDLE; - } - if (payload?.data?.requestKey === RequestKeys.AMEYO_ON_BREAK) { - setAutoStatus(this.sessionId); - window.postMessage({type: 'onAmeyoAvailabiltyChange', data: false},); - } - if (payload?.data?.requestKey === RequestKeys.GET_AGENTS_FOR_CALL_TRANSFER) { - this.callbacks.onAgentsForCallTransfer(payload?.data?.response); - } - } - - _registerMessageListener = async ({data}: GenericObject) => { - if(data?.type !== MessagingType.SET_RESPONSE_WITHOUT_CORS && data?.type !== MessagingType.GET_RESPONSE_WITHOUT_CORS) { + if (payload?.data?.err) { this.metricProcessor?.pushCounterMetric({ - metricName: `ameyo-events-count`, - flow: 'api-events-count', - subFlow:`universal-call-sdk-${data?.type}` - }) + metricName: 'ameyo-api-err-count', + flow: 'api-error-count', + subFlow: payload?.data?.requestKey + }); + this.clickStreamProcessor?.sendClickStreamEvent({ + type: 'api-error', + err: payload + }); + } + } + + private async handleLoginResponse(payload: GenericObject): Promise { + if (payload?.data?.err) { + console.log('Login failed:', payload?.data?.err); + this.callbacks.onLoginFailed(payload?.err); + return; } - if (data?.type === MessagingType.SET_RESPONSE_WITHOUT_CORS) { - this._onListenForCorsBypassResponse(data); - } - if (data?.type === MessagingType.ON_AMEYO_CALL_INCOMING) { - this.callbacks.onCallIncoming(data?.data); - this.currentCallState = CALL_STATES.CALL_INCOMING; - this.currentCallMetadata = {...this.currentCallMetadata, ...data?.data} - } - if (data?.type === MessagingType.ON_AMEYO_CALL_ACCEPTED) { - this.callbacks.onCallConnected(data?.data); - this.currentCallState = CALL_STATES.CALL_CONNECTED; - this.currentCallMetadata = {...this.currentCallMetadata, ...data?.data} - } - if (data?.type === MessagingType.ON_AMEYO_CALL_DISCONNECTED) { - this.callbacks.onCallDisconnected(data?.data); - this.currentCallState = CALL_STATES.CALL_DISCONNECTED; - ameyoHangupUser(this.sessionId, this.currentCallMetadata?.userCRTObjectId); - this.currentCallMetadata = {...this.currentCallMetadata, ...data?.data} - const audioElement =this._getElementFromDomById("beep") as HTMLAudioElement - audioElement?.play(); - - } - if (data?.type === MessagingType.ON_AMEYO_AVAILABILITY_CHANGE) { - this.isAgentAvailable = !data?.data?.isOnBreak; - this.callbacks.onAgentAvailabilityChange(this.isAgentAvailable, data?.data?.reason || ''); - window.postMessage({type: 'onAmeyoAvailabiltyChange', data: this.isAgentAvailable,}); - - } - if (data?.type === MessagingType.ON_AMEYO_FORCED_LOGOUT) { - this.callbacks.onForcedLogout() - } - if(data?.type === MessagingType.ON_AMEYO_CALL_TRANSFER){ - this.callbacks.onCallTransferStatus(data?.data); - } - }; - - registerOnCallIncoming(callback: (callState: StateType) => void) { - console.log('registerOnCallIncoming'); - this.callbacks.onCallIncoming = callback; + const sessionId = payload?.data?.response?.userSessionInfo?.sessionId; + this.sessionId = sessionId; + await getSipAccountInfo(sessionId, this.userName?.toLowerCase()); + registerEventProcessor(this.eventListenerUrl, sessionId); + maintainHeartbeat(this.sessionId, window?.listenerName || '', 0); } - registerOnCallConnected(callback: (callState: StateType) => void) { - console.log('registerOnCallConnected'); - this.callbacks.onCallConnected = callback; + private async handleSipAccountInfo(payload: GenericObject): Promise { + const response = payload?.data?.response; + this.initializeSipStack({ + accountName: response?.accountName, + userName: response?.userName, + domain: response?.domain, + password: response?.secret + }); + this.sipAccountInfo = payload?.data?.response; + console.log('sip account info', this.sipAccountInfo) + await getCampaignId(this.sessionId); + await setAutoStatus(this.sessionId); } - registerOnCallDisconnected(callback: (callState: StateType) => void) { - console.log('registerOnCallDisconnected'); - this.callbacks.onCallDisconnected = callback; + private async handleCampaignId(payload: GenericObject): Promise { + this.campaignId = payload?.data?.response?.campaignInfos?.[0]?.campaignId; + await attachOmniqueService(this.sessionId, this.userName.toLowerCase(), this.campaignId); } - registerOnAdapterReady(callback: () => void) { - console.log('registerOnAdapterReady'); - this.callbacks.onAdapterReady = callback; + private handleAgentAvailable(): void { + setAutoStatus(this.sessionId); } - registerOnAgentAvailabilityChange(callback: (isAgentAvailable: boolean, reason: string) => void) { - console.log('registerOnAgentAvailabilityChange'); - this.callbacks.onAgentAvailabilityChange = callback; + private handleAgentBreak(): void { + setAutoStatus(this.sessionId); } - registerOnForcedLogoutListener(callback: () => void) { - console.log('registerOnAgentAvailabilityChange'); - this.callbacks.onForcedLogout = callback; + private async handleOmniQueueService(): Promise { + await selectCampaign(this.sessionId, this.userName.toLowerCase(), this.campaignId); } - registerOnLoginFailedListener(callback: ()=>void) { - console.log('register on login failed'); - this.callbacks.onLoginFailed = callback + private async handleCampaignSelection(): Promise { + await autoSelectExtension(this.sessionId, this.userName.toLowerCase()); + this.callbacks.onAdapterReady(); + this.currentCallState = CALL_STATES.IDLE; + window.postMessage({type: 'onAmeyoAvailabiltyChange', data: false}); } - registerOnAgentsForCallTransfer(callback: (data: GenericObject) => void) { - console.log('registerOnAgentsForCallTransfer'); - this.callbacks.onAgentsForCallTransfer = callback; + private handleAgentAvailabilityChange(payload :GenericObject): void { + this.isAgentAvailable = !payload?.data?.isOnBreak; + this.callbacks.onAgentAvailabilityChange(this.isAgentAvailable, payload?.data?.reason || ''); + window.postMessage({type: 'onAmeyoAvailabiltyChange', data: this.isAgentAvailable,}); } - registerOnCallTransferStatus(callback: (data: GenericObject) => void) { - this.callbacks.onCallTransferStatus = callback; + private handleAgentsForTransfer(payload: GenericObject): void { + this.callbacks.onAgentsForCallTransfer(payload?.data?.response); } - acceptCall() { + private handleCallIncoming(data: GenericObject): void { + this.callbacks.onCallIncoming(data?.data); + this.currentCallState = CALL_STATES.CALL_INCOMING; + this.currentCallMetadata = {...this.currentCallMetadata, ...data?.data}; + } + + private handleCallAccepted(data: GenericObject): void { + this.callbacks.onCallConnected(data?.data); + this.currentCallState = CALL_STATES.CALL_CONNECTED; + this.currentCallMetadata = {...this.currentCallMetadata, ...data?.data}; + } + + private async handleCallDisconnected(data: GenericObject): Promise { + this.callbacks.onCallDisconnected(data?.data); + this.currentCallState = CALL_STATES.CALL_DISCONNECTED; + await ameyoHangupUser(this.sessionId, this.currentCallMetadata?.userCRTObjectId); + this.currentCallMetadata = {...this.currentCallMetadata, ...data?.data}; + + const audioElement = document.getElementById("beep") as HTMLAudioElement; + await audioElement?.play(); + } + + public logOut() { + logoutFromAmeyo(this.sessionId); + } + + public acceptCall(): void { acceptSipCall(); + callStateManager.sendConnectedMessage(); } - rejectCall() { + public rejectCall(): void { sipHangUp(); - localStorage.removeItem('revEngCustomerInfo'); } - disposeCall() { - ameyoDisposeCall(this.sessionId, this.campaignId, this.currentCallMetadata?.crtObjectId, this.currentCallMetadata?.userCRTObjectId); + public disposeCall(): void { + ameyoDisposeCall( + this.sessionId, + this.campaignId, + this.currentCallMetadata?.crtObjectId, + this.currentCallMetadata?.userCRTObjectId + ); } - setOnBreak() { + public setOnBreak(): void { setAgentOnBreak(this.sessionId); } - setAvailable() { + public setAvailable(): void { setAgentActive(this.sessionId); } - muteCall() { + public muteCall(): void { sipMuteCall(); } - unmuteCall() { + public unmuteCall(): void { sipUnmuteCall(); } - getAgentAvailability() { + public getAgentAvailability(): boolean { return this.isAgentAvailable; } - getLatestCallState() { + public getLatestCallState(): CALL_STATES { return this.currentCallState; } - getAvailableAgentsForCallTransfer() { + public getAvailableAgentsForCallTransfer(): void { getAllAgentsForTransferCall(this.sessionId); } - transferCallToAgent(data: CallTransferData) { - transferCallToAgent(data, + public transferCallToAgent(data: CallTransferData): void { + transferCallToAgent( + data, this.sessionId, this.currentCallMetadata?.crtObjectId, this.currentCallMetadata?.userCRTObjectId, - this.campaignId); + this.campaignId + ); } - logOut() { - logoutFromAmeyo(this.sessionId); + // Event registration methods + public registerOnCallIncoming(callback: (callState: StateType) => void): void { + this.callbacks.onCallIncoming = callback; } - private _appendTags: () => void = () => { - const script = document.createElement('script'); - script.src = 'https://public-assets.np.navi-gi.in/jarvis/sip5ml.js'; // Assuming it's placed in the public folder - script.async = true; - document.head.appendChild(script); - const is_already_appended = document.querySelector('#audio_remote') && document.querySelector('#video_local') && document.querySelector('#video_remote') && document.querySelector('#ringtone') && document.querySelector('#ringbacktone') && document.querySelector('#dtmfTone'); - if (is_already_appended) { - console.log('tags already appended skipping') - return; - } - type ElementAttributes = { - id?: string; - className?: string; - width?: string; - height?: string; - autoplay?: boolean; - muted?: boolean; - loop?: boolean; - src?: string; - style?: Partial; - [key: string]: any; - }; + public registerOnCallConnected(callback: (callState: StateType) => void): void { + this.callbacks.onCallConnected = callback; + } - function createElement( - tag: keyof HTMLElementTagNameMap, - attributes: ElementAttributes = {}, - parent: HTMLElement = document.body - ): HTMLElement { - const element = document.createElement(tag); + public registerOnCallDisconnected(callback: (callState: StateType) => void): void { + this.callbacks.onCallDisconnected = callback; + } - Object.keys(attributes).forEach((attr) => { - if (attr === 'style' && attributes.style) { - Object.assign(element.style, attributes.style); - } else if (attr in element) { - (element as any)[attr] = attributes[attr]; - } else { - element.setAttribute(attr, attributes[attr]); - } - }); + public registerOnAdapterReady(callback: () => void): void { + this.callbacks.onAdapterReady = callback; + } - parent.appendChild(element); - return element; - } + public registerOnAgentAvailabilityChange(callback: (isAgentAvailable: boolean, reason: string) => void): void { + this.callbacks.onAgentAvailabilityChange = callback; + } - createElement('audio', { - id: 'audio_remote', - autoplay: true, - }); + public registerOnForcedLogoutListener(callback: () => void): void { + this.callbacks.onForcedLogout = callback; + } - createElement('video', { - className: 'video', - width: '100%', - height: '100%', - id: 'video_local', - autoplay: true, - muted: true, - style: { - opacity: '0', - display: 'none', - backgroundColor: '#000000', - webkitTransitionProperty: 'opacity', - webkitTransitionDuration: '2s', - }, - }); - - createElement('video', { - className: 'video', - width: '100%', - height: '100%', - id: 'video_remote', - autoplay: true, - style: { - display: 'none', - opacity: '0', - backgroundColor: '#000000', - webkitTransitionProperty: 'opacity', - webkitTransitionDuration: '2s', - }, - }); - - createElement('audio', { - id: 'ringtone', - loop: true, - src: 'https://public-assets.np.navi-gi.in/jarvis/ringtone.wav', - }); - - createElement('audio', { - id: 'ringbacktone', - loop: true, - src: 'https://public-assets.np.navi-gi.in/jarvis/ringbacktone.wav', - }); - - createElement('audio', { - id: 'dtmfTone', - src: 'https://public-assets.np.navi-gi.in/jarvis/dtmf.wav', - }); - createElement("audio", { - id: "beep", - src: "https://public-assets.np.navi-gi.in/jarvis/beep.wav", - }); - const onSipSetupReadyEvent = new CustomEvent('onSipSetupReady', { - detail: {message: 'Custom page loaded event triggered'} - }); - - script.onload = () => { - window.dispatchEvent(onSipSetupReadyEvent) - - } - - }; + public registerOnLoginFailedListener(callback: () => void): void { + this.callbacks.onLoginFailed = callback; + } + public registerOnAgentsForCallTransfer(callback: (data: GenericObject) => void): void { + this.callbacks.onAgentsForCallTransfer = callback; + } + public registerOnCallTransferStatus(callback: (data: GenericObject) => void): void { + this.callbacks.onCallTransferStatus = callback; + } } export default AmeyoAdapter; diff --git a/packages/adapter-ameyo/lib/types.ts b/packages/adapter-ameyo/lib/types.ts index c82dbdd..36817a3 100644 --- a/packages/adapter-ameyo/lib/types.ts +++ b/packages/adapter-ameyo/lib/types.ts @@ -65,6 +65,18 @@ export type SipAccountInfo = { password: string } +export type CallbackFunctions = { + onCallConnected: (data: StateType) => void; + onCallDisconnected: (data: StateType) => void; + onCallIncoming: (data: StateType) => void; + onAdapterReady: () => void; + onAgentAvailabilityChange: (isAgentAvailable: boolean, reason: string) => void; + onForcedLogout: () => void; + onLoginFailed: (err: GenericObject) => void; + onAgentsForCallTransfer: (data: GenericObject) => void; + onCallTransferStatus: (data: GenericObject) => void; +}; + export enum CALL_STATES { CALL_INCOMING = 'CALL_INCOMING', @@ -77,3 +89,35 @@ export type CallTransferData = { campaignId : string, targetCRTObjectId: string } + +export type CallStatus = 'inactive' | 'initialized' | 'ringing' | 'connected' | 'hungup'; + +export enum PushType { + UserCallModelUpdated = 'UserCallModelUpdatedPush', + CustomerCallMemberUpdated = 'CustomerCallMemberUpdatedPush', + CustomerCallMemberCreated = 'CustomerCallMemberCreatedPush', + CRMCreateNotify = 'CRMCreateNotifyPush', + UserCallMemberUpdated = 'UserCallMemberUpdatedPush', + UserCCRuntimeUpdated = 'UserCCRuntimeUpdatedPush', + UserLoggedOff = 'UserLoggedOffPush' +} + +export interface CallState { + isConnected: boolean; + lastCallStatus: CallStatus; + customerCallStatus: string | null; + crtObjectId: string | null; + connectionAttempts: number; + customerInfo: { + phoneNumber?: string; + lan?: string; + crtObjectId?: string; + userCRTObjectId?: string; + callId?: string; + } | null; +} + +export interface EventData { + pushType: PushType; + data: any; +} diff --git a/packages/adapter-ameyo/package.json b/packages/adapter-ameyo/package.json index de1d84b..c90412a 100644 --- a/packages/adapter-ameyo/package.json +++ b/packages/adapter-ameyo/package.json @@ -1,6 +1,6 @@ { "name": "@universal-call-sdk/adapter-ameyo", - "version": "1.1.3", + "version": "1.1.15", "type": "module", "scripts": { "dev": "vite", @@ -9,7 +9,7 @@ "preview": "vite preview" }, "dependencies": { - "@universal-call-sdk/common": "^1.0.54", + "@universal-call-sdk/common": "^1.1.13", "uuid": "^11.0.3" }, "devDependencies": { diff --git a/packages/common/CHANGELOG.md b/packages/common/CHANGELOG.md index e763c35..08b44d0 100644 --- a/packages/common/CHANGELOG.md +++ b/packages/common/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## 1.1.13 (2025-01-16) + +**Note:** Version bump only for package @universal-call-sdk/common + + + + + ## 1.0.51 (2025-01-06) **Note:** Version bump only for package @universal-call-sdk/common diff --git a/packages/common/lib/utils/parsingUtils.ts b/packages/common/lib/utils/parsingUtils.ts new file mode 100644 index 0000000..b38c33c --- /dev/null +++ b/packages/common/lib/utils/parsingUtils.ts @@ -0,0 +1,10 @@ +export function parseQuerystring(url: string): Record { + const foo = url?.split('?')?.[1]?.split('&') || []; + const dict: Record = {}; + let elem = []; + for (let i = foo.length - 1; i >= 0; i--) { + elem = foo[i].split('='); + dict[elem[0]] = elem[1]; + } + return dict || {}; +} diff --git a/packages/common/package.json b/packages/common/package.json index 784b032..9630821 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -1,6 +1,6 @@ { "name": "@universal-call-sdk/common", - "version": "1.0.54", + "version": "1.1.13", "type": "module", "scripts": { "dev": "vite", diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index 9677108..5c7b92d 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## 1.1.12 (2025-01-16) + +**Note:** Version bump only for package @universal-call-sdk/core + + + + + ## 1.0.55 (2025-01-06) **Note:** Version bump only for package @universal-call-sdk/core diff --git a/packages/core/lib/useCallSdk.ts b/packages/core/lib/useCallSdk.ts index cd0db8c..02feeb4 100644 --- a/packages/core/lib/useCallSdk.ts +++ b/packages/core/lib/useCallSdk.ts @@ -91,7 +91,7 @@ function UseCallSdk({AdapterClass, adapterOptions, metricsConfig, clickStreamCon }) { useEffect(() => { adapter = new AdapterClass(adapterOptions); - }, []); + }, [adapterOptions]); // @ts-expect-error sdfsf const [callState] = useReducer(reducer, initialState,()=> initialState); diff --git a/packages/core/package.json b/packages/core/package.json index 69a0189..03226ef 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@universal-call-sdk/core", - "version": "1.0.58", + "version": "1.1.12", "type": "module", "scripts": { "dev": "vite",