diff --git a/packages/livechat/src/components/App/App.tsx b/packages/livechat/src/components/App/App.tsx index ba3e941c27558..e0398b3a08510 100644 --- a/packages/livechat/src/components/App/App.tsx +++ b/packages/livechat/src/components/App/App.tsx @@ -140,11 +140,14 @@ export class App extends Component { iframe: { visible }, config: { theme }, dispatch, + undocked, } = this.props; - parentCall(minimized ? 'minimizeWindow' : 'restoreWindow'); - parentCall(visible ? 'showWidget' : 'hideWidget'); - parentCall('setWidgetPosition', theme.position || 'right'); + if (!undocked) { + parentCall(minimized ? 'minimizeWindow' : 'restoreWindow'); + parentCall(visible ? 'showWidget' : 'hideWidget'); + parentCall('setWidgetPosition', theme.position || 'right'); + } visibility.addListener(this.handleVisibilityChange); diff --git a/packages/livechat/src/components/Screen/ScreenProvider.tsx b/packages/livechat/src/components/Screen/ScreenProvider.tsx index 2bccafd60c6f2..49d3fc4dec918 100644 --- a/packages/livechat/src/components/Screen/ScreenProvider.tsx +++ b/packages/livechat/src/components/Screen/ScreenProvider.tsx @@ -1,10 +1,9 @@ import type { FunctionalComponent } from 'preact'; import { createContext } from 'preact'; -import { useCallback, useContext, useEffect, useState } from 'preact/hooks'; +import { useContext, useEffect, useState } from 'preact/hooks'; import { parse } from 'query-string'; import { isActiveSession } from '../../helpers/isActiveSession'; -import { createOrUpdateGuest, evaluateChangesAndLoadConfigByFields } from '../../lib/hooks'; import { loadConfig } from '../../lib/main'; import { parentCall } from '../../lib/parentCall'; import { loadMessages } from '../../lib/room'; @@ -63,21 +62,11 @@ export const ScreenContext = createContext({ } as ScreenContextValue); export const ScreenProvider: FunctionalComponent = ({ children }) => { - const { - dispatch, - config, - sound, - minimized = true, - undocked, - expanded = false, - alerts, - modal, - iframe, - ...store - } = useContext(StoreContext); + const store = useContext(StoreContext); + const { token, dispatch, config, sound, minimized = true, undocked, expanded = false, alerts, modal, iframe, customFieldsQueue } = store; const { department, name, email } = iframe.guest || {}; const { color, position: configPosition, background } = config.theme || {}; - const { livechatLogo, hideWatermark = false, registrationForm } = config.settings || {}; + const { livechatLogo, hideWatermark = false } = config.settings || {}; const { color: customColor, @@ -128,7 +117,7 @@ export const ScreenProvider: FunctionalComponent = ({ children }) => { }; const handleOpenWindow = () => { - parentCall('openPopout', store.token); + parentCall('openPopout', { token, iframe, customFieldsQueue }); dispatch({ undocked: true, minimized: false }); }; @@ -138,30 +127,14 @@ export const ScreenProvider: FunctionalComponent = ({ children }) => { const dismissNotification = () => !isActiveSession(); - const checkPoppedOutWindow = useCallback(async () => { + useEffect(() => { // Checking if the window is poppedOut and setting parent minimized if yes for the restore purpose const poppedOut = parse(window.location.search).mode === 'popout'; - const { token = '' } = parse(window.location.search); setPopedOut(poppedOut); - if (poppedOut) { dispatch({ minimized: false, undocked: true }); } - - if (token && typeof token === 'string') { - if (registrationForm && !name && !email) { - dispatch({ token }); - return; - } - await evaluateChangesAndLoadConfigByFields(async () => { - await createOrUpdateGuest({ token }); - }); - } - }, [dispatch, email, name, registrationForm]); - - useEffect(() => { - checkPoppedOutWindow(); - }, [checkPoppedOutWindow]); + }, [dispatch]); const screenProps = { theme: { diff --git a/packages/livechat/src/lib/customFields.js b/packages/livechat/src/lib/customFields.js index b5d11a2d6b63c..2cea28256a778 100644 --- a/packages/livechat/src/lib/customFields.js +++ b/packages/livechat/src/lib/customFields.js @@ -28,7 +28,6 @@ class CustomFields { reset() { this._initiated = false; this._started = false; - this._queue = {}; store.off('change', this.handleStoreChange); } @@ -48,18 +47,36 @@ class CustomFields { CustomFields.instance.processCustomFields(); } + addToQueue(key, value, overwrite) { + const { customFieldsQueue } = store.state; + store.setState({ + customFieldsQueue: { + ...customFieldsQueue, + [key]: { value, overwrite }, + }, + }); + } + + getQueue() { + return store.state.customFieldsQueue; + } + + clearQueue() { + store.setState({ customFieldsQueue: {} }); + } + processCustomFields() { - Object.keys(this._queue).forEach((key) => { - const { value, overwrite } = this._queue[key]; + const queue = this.getQueue(); + Object.entries(queue).forEach(([key, { value, overwrite }]) => { this.setCustomField(key, value, overwrite); }); - this._queue = {}; + this.clearQueue(); } setCustomField(key, value, overwrite = true) { if (!this._started) { - this._queue[key] = { value, overwrite }; + this.addToQueue(key, value, overwrite); return; } diff --git a/packages/livechat/src/lib/hooks.ts b/packages/livechat/src/lib/hooks.ts index 1ae9f353bc10f..063a86845fc18 100644 --- a/packages/livechat/src/lib/hooks.ts +++ b/packages/livechat/src/lib/hooks.ts @@ -109,6 +109,22 @@ const updateIframeData = (data: Partial) => { }; const api = { + syncState(data: Partial) { + if (!data || typeof data !== 'object') { + return; + } + + void evaluateChangesAndLoadConfigByFields(async () => { + const { user } = store.state; + + if (user && data.token && user.token !== data.token) { + await createOrUpdateGuest({ token: data.token }); + } + + store.setState(data); + }); + }, + pageVisited(info: { change: string; title: string; location: { href: string } }) { const { token, room } = store.state; const { _id: rid } = room || {}; diff --git a/packages/livechat/src/lib/parentCall.ts b/packages/livechat/src/lib/parentCall.ts index 4b03e22927a51..bffc2c0c0c480 100644 --- a/packages/livechat/src/lib/parentCall.ts +++ b/packages/livechat/src/lib/parentCall.ts @@ -1,5 +1,13 @@ import { VALID_CALLBACKS } from '../widget'; +const getParentWindowTarget = () => { + if (window.opener && !window.opener.closed) { + return window.opener; + } + + return window.parent; +}; + export const parentCall = (method: string, ...args: any[]) => { const data = { src: 'rocketchat', @@ -7,8 +15,9 @@ export const parentCall = (method: string, ...args: any[]) => { args, }; + const target = getParentWindowTarget(); // TODO: This lgtm ignoring deserves more attention urgently! - window.parent.postMessage(data, '*'); // lgtm [js/cross-window-information-leak] + target.postMessage(data, '*'); // lgtm [js/cross-window-information-leak] }; export const runCallbackEventEmitter = (callbackName: string, data: unknown) => diff --git a/packages/livechat/src/store/index.tsx b/packages/livechat/src/store/index.tsx index 8f5294e42cbae..3e8d0663d2fa6 100644 --- a/packages/livechat/src/store/index.tsx +++ b/packages/livechat/src/store/index.tsx @@ -3,13 +3,13 @@ import type { ComponentChildren } from 'preact'; import { Component, createContext } from 'preact'; import { useContext } from 'preact/hooks'; +import Store from './Store'; import type { CustomField } from '../components/Form/CustomFields'; import type { Agent } from '../definitions/agents'; import type { Department } from '../definitions/departments'; import type { TriggerMessage } from '../definitions/triggerMessage'; import { parentCall } from '../lib/parentCall'; import { createToken } from '../lib/random'; -import Store from './Store'; export type LivechatHiddenSytemMessageType = | 'uj' // User joined @@ -123,6 +123,7 @@ export type StoreState = { connecting?: boolean; messageListPosition?: 'top' | 'bottom' | 'free'; renderedTriggers: TriggerMessage[]; + customFieldsQueue: Record; }; export const initialState = (): StoreState => ({ @@ -164,6 +165,7 @@ export const initialState = (): StoreState => ({ ongoingCall: null, // TODO: store call info like url, startTime, timeout, etc here businessUnit: null, renderedTriggers: [], + customFieldsQueue: {}, }); const dontPersist = [ @@ -191,6 +193,10 @@ window.addEventListener('load', () => { }); window.addEventListener('visibilitychange', () => { + if (store.state.undocked) { + return; + } + !store.state.minimized && !store.state.triggered && parentCall('openWidget'); store.state.iframe.visible ? parentCall('showWidget') : parentCall('hideWidget'); }); diff --git a/packages/livechat/src/widget.ts b/packages/livechat/src/widget.ts index ca46ab944913c..589e02e554791 100644 --- a/packages/livechat/src/widget.ts +++ b/packages/livechat/src/widget.ts @@ -7,11 +7,10 @@ import type { HooksWidgetAPI } from './lib/hooks'; import type { StoreState } from './store'; type InternalWidgetAPI = { - popup: Window | null; ready: () => void; minimizeWindow: () => void; restoreWindow: () => void; - openPopout: (token?: string) => void; + openPopout: (state: StoreState) => void; openWidget: () => void; resizeWidget: (height: number) => void; removeWidget: () => void; @@ -23,10 +22,10 @@ type InternalWidgetAPI = { setWidgetPosition: (position: 'left' | 'right') => void; }; -export type LivechatMessageEventData> = { +export type LivechatMessageEventData, Fn extends keyof ApiType = keyof ApiType> = { src?: string; - fn: keyof ApiType; - args: Parameters; + fn: Fn; + args: Parameters; }; type InitializeParams = { @@ -61,6 +60,7 @@ let ready = false; let smallScreen = false; let scrollPosition: number; let widgetHeight: number; +let popoutWindow: Window | null = null; export const VALID_CALLBACKS = [ 'chat-maximized', @@ -103,6 +103,12 @@ function clearAllCallbacks() { }); } +const formatMessage = (action: keyof HooksWidgetAPI, ...params: Parameters) => ({ + src: 'rocketchat', + fn: action, + args: params, +}); + // hooks function callHook(action: keyof HooksWidgetAPI, ...params: Parameters) { if (!ready) { @@ -113,11 +119,7 @@ function callHook(action: keyof HooksWidgetAPI, ...params: Parameters) { } const api: InternalWidgetAPI = { - popup: null, - openWidget, resizeWidget, @@ -475,27 +475,29 @@ const api: InternalWidgetAPI = { minimizeWindow() { closeWidget(); }, - restoreWindow() { - if (api.popup && api.popup.closed !== true) { - api.popup.close(); - api.popup = null; + if (popoutWindow && popoutWindow.closed !== true) { + popoutWindow.close(); + popoutWindow = null; } openWidget(); }, - openPopout(token = '') { + openPopout(state: Partial) { closeWidget(); + if (!config.url) { throw new Error('Config.url is not set!'); } - const urlToken = token && `&token=${token}`; - api.popup = window.open( - `${config.url}${config.url.lastIndexOf('?') > -1 ? '&' : '?'}mode=popout${urlToken}`, - 'livechat-popout', - `width=${WIDGET_OPEN_WIDTH}, height=${widgetHeight}, toolbars=no`, - ); + const url = new URL(config.url); + url.searchParams.append('mode', 'popout'); + + listenForMessageOnce('ready', () => { + popoutWindow?.postMessage(formatMessage('syncState', state), '*'); + }); + + popoutWindow = window.open(url, 'livechat-popout', `width=${WIDGET_OPEN_WIDTH}, height=${widgetHeight}, toolbars=no`); }, removeWidget() { @@ -628,16 +630,24 @@ const currentPage: { href: string | null; title: string | null } = { title: null, }; -function onNewMessage(event: MessageEvent>>) { +function isValidMessage(event: MessageEvent>) { if (event.source === event.target) { - return; + return false; } if (!event.data || typeof event.data !== 'object') { - return; + return false; } if (!event.data.src || event.data.src !== 'rocketchat') { + return false; + } + + return true; +} + +function onNewMessage(event: MessageEvent>) { + if (!isValidMessage(event)) { return; } @@ -652,6 +662,22 @@ function onNewMessage(event: MessageEvent( + key: K, + callback: (data: LivechatMessageEventData) => void, +): void { + const listener = (event: MessageEvent>) => { + if (!isValidMessage(event) || event.data.fn !== key) { + return; + } + + callback(event.data); + window.removeEventListener('message', listener); + }; + + window.addEventListener('message', listener); +} + const attachMessageListener = () => { window.addEventListener('message', onNewMessage, false); };