Restructure omni services and add Chatwoot research snapshot
This commit is contained in:
75
research/chatwoot/app/javascript/sdk/DOMHelpers.js
Normal file
75
research/chatwoot/app/javascript/sdk/DOMHelpers.js
Normal file
@@ -0,0 +1,75 @@
|
||||
import { SDK_CSS } from './sdk.js';
|
||||
import { IFrameHelper } from './IFrameHelper';
|
||||
|
||||
export const loadCSS = () => {
|
||||
const css = document.createElement('style');
|
||||
css.innerHTML = `${SDK_CSS}`;
|
||||
css.id = 'cw-widget-styles';
|
||||
css.dataset.turboPermanent = true;
|
||||
document.body.appendChild(css);
|
||||
};
|
||||
|
||||
// This is a method specific to Turbo
|
||||
// The body replacing strategy removes Chatwoot styles
|
||||
// as well as the widget, this help us get it back
|
||||
export const restoreElement = (id, newBody) => {
|
||||
const element = document.getElementById(id);
|
||||
const newElement = newBody.querySelector(`#${id}`);
|
||||
|
||||
if (element && !newElement) {
|
||||
newBody.appendChild(element);
|
||||
}
|
||||
};
|
||||
|
||||
export const restoreWidgetInDOM = newBody => {
|
||||
restoreElement('cw-bubble-holder', newBody);
|
||||
restoreElement('cw-widget-holder', newBody);
|
||||
restoreElement('cw-widget-styles', newBody);
|
||||
};
|
||||
|
||||
export const addClasses = (elm, classes) => {
|
||||
elm.classList.add(...classes.split(' '));
|
||||
};
|
||||
|
||||
export const toggleClass = (elm, classes) => {
|
||||
elm.classList.toggle(classes);
|
||||
};
|
||||
|
||||
export const removeClasses = (elm, classes) => {
|
||||
elm.classList.remove(...classes.split(' '));
|
||||
};
|
||||
|
||||
export const onLocationChange = ({ referrerURL, referrerHost }) => {
|
||||
IFrameHelper.events.onLocationChange({
|
||||
referrerURL,
|
||||
referrerHost,
|
||||
});
|
||||
};
|
||||
|
||||
export const onLocationChangeListener = () => {
|
||||
let oldHref = document.location.href;
|
||||
const referrerHost = document.location.host;
|
||||
const config = {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
};
|
||||
onLocationChange({
|
||||
referrerURL: oldHref,
|
||||
referrerHost,
|
||||
});
|
||||
|
||||
const bodyList = document.querySelector('body');
|
||||
const observer = new MutationObserver(mutations => {
|
||||
mutations.forEach(() => {
|
||||
if (oldHref !== document.location.href) {
|
||||
oldHref = document.location.href;
|
||||
onLocationChange({
|
||||
referrerURL: oldHref,
|
||||
referrerHost,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
observer.observe(bodyList, config);
|
||||
};
|
||||
345
research/chatwoot/app/javascript/sdk/IFrameHelper.js
Normal file
345
research/chatwoot/app/javascript/sdk/IFrameHelper.js
Normal file
@@ -0,0 +1,345 @@
|
||||
import Cookies from 'js-cookie';
|
||||
import {
|
||||
addClasses,
|
||||
loadCSS,
|
||||
removeClasses,
|
||||
onLocationChangeListener,
|
||||
} from './DOMHelpers';
|
||||
import {
|
||||
body,
|
||||
widgetHolder,
|
||||
createBubbleHolder,
|
||||
createBubbleIcon,
|
||||
bubbleSVG,
|
||||
chatBubble,
|
||||
closeBubble,
|
||||
bubbleHolder,
|
||||
onClickChatBubble,
|
||||
onBubbleClick,
|
||||
setBubbleText,
|
||||
addUnreadClass,
|
||||
removeUnreadClass,
|
||||
} from './bubbleHelpers';
|
||||
import { isWidgetColorLighter } from 'shared/helpers/colorHelper';
|
||||
import { dispatchWindowEvent } from 'shared/helpers/CustomEventHelper';
|
||||
import {
|
||||
CHATWOOT_ERROR,
|
||||
CHATWOOT_POSTBACK,
|
||||
CHATWOOT_READY,
|
||||
} from '../widget/constants/sdkEvents';
|
||||
import { SET_USER_ERROR } from '../widget/constants/errorTypes';
|
||||
import { getUserCookieName, setCookieWithDomain } from './cookieHelpers';
|
||||
import {
|
||||
getAlertAudio,
|
||||
initOnEvents,
|
||||
} from 'shared/helpers/AudioNotificationHelper';
|
||||
import { isFlatWidgetStyle } from './settingsHelper';
|
||||
import { popoutChatWindow } from '../widget/helpers/popoutHelper';
|
||||
import addHours from 'date-fns/addHours';
|
||||
|
||||
const updateAuthCookie = (cookieContent, baseDomain = '') =>
|
||||
setCookieWithDomain('cw_conversation', cookieContent, {
|
||||
baseDomain,
|
||||
});
|
||||
|
||||
const updateCampaignReadStatus = baseDomain => {
|
||||
const expireBy = addHours(new Date(), 1);
|
||||
setCookieWithDomain('cw_snooze_campaigns_till', Number(expireBy), {
|
||||
expires: expireBy,
|
||||
baseDomain,
|
||||
});
|
||||
};
|
||||
|
||||
export const IFrameHelper = {
|
||||
getUrl({ baseUrl, websiteToken }) {
|
||||
return `${baseUrl}/widget?website_token=${websiteToken}`;
|
||||
},
|
||||
createFrame: ({ baseUrl, websiteToken }) => {
|
||||
if (IFrameHelper.getAppFrame()) {
|
||||
return;
|
||||
}
|
||||
|
||||
loadCSS();
|
||||
const iframe = document.createElement('iframe');
|
||||
const cwCookie = Cookies.get('cw_conversation');
|
||||
let widgetUrl = IFrameHelper.getUrl({ baseUrl, websiteToken });
|
||||
if (cwCookie) {
|
||||
widgetUrl = `${widgetUrl}&cw_conversation=${cwCookie}`;
|
||||
}
|
||||
iframe.src = widgetUrl;
|
||||
iframe.allow =
|
||||
'camera;microphone;fullscreen;display-capture;picture-in-picture;clipboard-write;';
|
||||
iframe.id = 'chatwoot_live_chat_widget';
|
||||
iframe.style.visibility = 'hidden';
|
||||
|
||||
let holderClassName = `woot-widget-holder woot--hide woot-elements--${window.$chatwoot.position}`;
|
||||
if (window.$chatwoot.hideMessageBubble) {
|
||||
holderClassName += ` woot-widget--without-bubble`;
|
||||
}
|
||||
if (isFlatWidgetStyle(window.$chatwoot.widgetStyle)) {
|
||||
holderClassName += ` woot-widget-holder--flat`;
|
||||
}
|
||||
|
||||
addClasses(widgetHolder, holderClassName);
|
||||
widgetHolder.id = 'cw-widget-holder';
|
||||
widgetHolder.dataset.turboPermanent = true;
|
||||
widgetHolder.appendChild(iframe);
|
||||
body.appendChild(widgetHolder);
|
||||
IFrameHelper.initPostMessageCommunication();
|
||||
IFrameHelper.initWindowSizeListener();
|
||||
IFrameHelper.preventDefaultScroll();
|
||||
},
|
||||
getAppFrame: () => document.getElementById('chatwoot_live_chat_widget'),
|
||||
getBubbleHolder: () => document.getElementsByClassName('woot--bubble-holder'),
|
||||
sendMessage: (key, value) => {
|
||||
const element = IFrameHelper.getAppFrame();
|
||||
element.contentWindow.postMessage(
|
||||
`chatwoot-widget:${JSON.stringify({ event: key, ...value })}`,
|
||||
'*'
|
||||
);
|
||||
},
|
||||
initPostMessageCommunication: () => {
|
||||
window.onmessage = e => {
|
||||
if (
|
||||
typeof e.data !== 'string' ||
|
||||
e.data.indexOf('chatwoot-widget:') !== 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const message = JSON.parse(e.data.replace('chatwoot-widget:', ''));
|
||||
if (typeof IFrameHelper.events[message.event] === 'function') {
|
||||
IFrameHelper.events[message.event](message);
|
||||
}
|
||||
};
|
||||
},
|
||||
initWindowSizeListener: () => {
|
||||
window.addEventListener('resize', () => IFrameHelper.toggleCloseButton());
|
||||
},
|
||||
preventDefaultScroll: () => {
|
||||
widgetHolder.addEventListener('wheel', event => {
|
||||
const deltaY = event.deltaY;
|
||||
const contentHeight = widgetHolder.scrollHeight;
|
||||
const visibleHeight = widgetHolder.offsetHeight;
|
||||
const scrollTop = widgetHolder.scrollTop;
|
||||
|
||||
if (
|
||||
(scrollTop === 0 && deltaY < 0) ||
|
||||
(visibleHeight + scrollTop === contentHeight && deltaY > 0)
|
||||
) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
setFrameHeightToFitContent: (extraHeight, isFixedHeight) => {
|
||||
const iframe = IFrameHelper.getAppFrame();
|
||||
const updatedIframeHeight = isFixedHeight ? `${extraHeight}px` : '100%';
|
||||
|
||||
if (iframe)
|
||||
iframe.setAttribute('style', `height: ${updatedIframeHeight} !important`);
|
||||
},
|
||||
|
||||
setupAudioListeners: () => {
|
||||
const { baseUrl = '' } = window.$chatwoot;
|
||||
getAlertAudio(baseUrl, { type: 'widget', alertTone: 'ding' }).then(() =>
|
||||
initOnEvents.forEach(event => {
|
||||
document.removeEventListener(
|
||||
event,
|
||||
IFrameHelper.setupAudioListeners,
|
||||
false
|
||||
);
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
events: {
|
||||
loaded: message => {
|
||||
updateAuthCookie(message.config.authToken, window.$chatwoot.baseDomain);
|
||||
window.$chatwoot.hasLoaded = true;
|
||||
const campaignsSnoozedTill = Cookies.get('cw_snooze_campaigns_till');
|
||||
IFrameHelper.sendMessage('config-set', {
|
||||
locale: window.$chatwoot.locale,
|
||||
position: window.$chatwoot.position,
|
||||
hideMessageBubble: window.$chatwoot.hideMessageBubble,
|
||||
showPopoutButton: window.$chatwoot.showPopoutButton,
|
||||
widgetStyle: window.$chatwoot.widgetStyle,
|
||||
darkMode: window.$chatwoot.darkMode,
|
||||
showUnreadMessagesDialog: window.$chatwoot.showUnreadMessagesDialog,
|
||||
campaignsSnoozedTill,
|
||||
welcomeTitle: window.$chatwoot.welcomeTitle,
|
||||
welcomeDescription: window.$chatwoot.welcomeDescription,
|
||||
availableMessage: window.$chatwoot.availableMessage,
|
||||
unavailableMessage: window.$chatwoot.unavailableMessage,
|
||||
enableFileUpload: window.$chatwoot.enableFileUpload,
|
||||
enableEmojiPicker: window.$chatwoot.enableEmojiPicker,
|
||||
enableEndConversation: window.$chatwoot.enableEndConversation,
|
||||
});
|
||||
IFrameHelper.onLoad({
|
||||
widgetColor: message.config.channelConfig.widgetColor,
|
||||
});
|
||||
IFrameHelper.toggleCloseButton();
|
||||
|
||||
if (window.$chatwoot.user) {
|
||||
IFrameHelper.sendMessage('set-user', window.$chatwoot.user);
|
||||
}
|
||||
|
||||
window.playAudioAlert = () => {};
|
||||
|
||||
initOnEvents.forEach(e => {
|
||||
document.addEventListener(e, IFrameHelper.setupAudioListeners, false);
|
||||
});
|
||||
|
||||
if (!window.$chatwoot.resetTriggered) {
|
||||
dispatchWindowEvent({ eventName: CHATWOOT_READY });
|
||||
}
|
||||
},
|
||||
error: ({ errorType, data }) => {
|
||||
dispatchWindowEvent({ eventName: CHATWOOT_ERROR, data: data });
|
||||
|
||||
if (errorType === SET_USER_ERROR) {
|
||||
Cookies.remove(getUserCookieName());
|
||||
}
|
||||
},
|
||||
onEvent({ eventIdentifier: eventName, data }) {
|
||||
dispatchWindowEvent({ eventName, data });
|
||||
},
|
||||
setBubbleLabel(message) {
|
||||
setBubbleText(window.$chatwoot.launcherTitle || message.label);
|
||||
},
|
||||
|
||||
setAuthCookie({ data: { widgetAuthToken } }) {
|
||||
updateAuthCookie(widgetAuthToken, window.$chatwoot.baseDomain);
|
||||
},
|
||||
|
||||
setCampaignReadOn() {
|
||||
updateCampaignReadStatus(window.$chatwoot.baseDomain);
|
||||
},
|
||||
|
||||
postback(data) {
|
||||
dispatchWindowEvent({
|
||||
eventName: CHATWOOT_POSTBACK,
|
||||
data,
|
||||
});
|
||||
},
|
||||
|
||||
toggleBubble: state => {
|
||||
let bubbleState = {};
|
||||
if (state === 'open') {
|
||||
bubbleState.toggleValue = true;
|
||||
} else if (state === 'close') {
|
||||
bubbleState.toggleValue = false;
|
||||
}
|
||||
|
||||
onBubbleClick(bubbleState);
|
||||
},
|
||||
|
||||
popoutChatWindow: ({ baseUrl, websiteToken, locale }) => {
|
||||
const cwCookie = Cookies.get('cw_conversation');
|
||||
window.$chatwoot.toggle('close');
|
||||
popoutChatWindow(baseUrl, websiteToken, locale, cwCookie);
|
||||
},
|
||||
|
||||
closeWindow: () => {
|
||||
onBubbleClick({ toggleValue: false });
|
||||
removeUnreadClass();
|
||||
},
|
||||
|
||||
onBubbleToggle: isOpen => {
|
||||
IFrameHelper.sendMessage('toggle-open', { isOpen });
|
||||
if (isOpen) {
|
||||
IFrameHelper.pushEvent('webwidget.triggered');
|
||||
}
|
||||
},
|
||||
onLocationChange: ({ referrerURL, referrerHost }) => {
|
||||
IFrameHelper.sendMessage('change-url', {
|
||||
referrerURL,
|
||||
referrerHost,
|
||||
});
|
||||
},
|
||||
updateIframeHeight: message => {
|
||||
const { extraHeight = 0, isFixedHeight } = message;
|
||||
|
||||
IFrameHelper.setFrameHeightToFitContent(extraHeight, isFixedHeight);
|
||||
},
|
||||
|
||||
setUnreadMode: () => {
|
||||
addUnreadClass();
|
||||
onBubbleClick({ toggleValue: true });
|
||||
},
|
||||
|
||||
resetUnreadMode: () => removeUnreadClass(),
|
||||
handleNotificationDot: event => {
|
||||
if (window.$chatwoot.hideMessageBubble) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bubbleElement = document.querySelector('.woot-widget-bubble');
|
||||
if (
|
||||
event.unreadMessageCount > 0 &&
|
||||
!bubbleElement.classList.contains('unread-notification')
|
||||
) {
|
||||
addClasses(bubbleElement, 'unread-notification');
|
||||
} else if (event.unreadMessageCount === 0) {
|
||||
removeClasses(bubbleElement, 'unread-notification');
|
||||
}
|
||||
},
|
||||
|
||||
closeChat: () => {
|
||||
onBubbleClick({ toggleValue: false });
|
||||
},
|
||||
|
||||
playAudio: () => {
|
||||
window.playAudioAlert();
|
||||
},
|
||||
},
|
||||
pushEvent: eventName => {
|
||||
IFrameHelper.sendMessage('push-event', { eventName });
|
||||
},
|
||||
|
||||
onLoad: ({ widgetColor }) => {
|
||||
const iframe = IFrameHelper.getAppFrame();
|
||||
iframe.style.visibility = '';
|
||||
iframe.setAttribute('id', `chatwoot_live_chat_widget`);
|
||||
|
||||
if (IFrameHelper.getBubbleHolder().length) {
|
||||
return;
|
||||
}
|
||||
createBubbleHolder(window.$chatwoot.hideMessageBubble);
|
||||
onLocationChangeListener();
|
||||
|
||||
let className = 'woot-widget-bubble';
|
||||
let closeBtnClassName = `woot-elements--${window.$chatwoot.position} woot-widget-bubble woot--close woot--hide`;
|
||||
|
||||
if (isFlatWidgetStyle(window.$chatwoot.widgetStyle)) {
|
||||
className += ' woot-widget-bubble--flat';
|
||||
closeBtnClassName += ' woot-widget-bubble--flat';
|
||||
}
|
||||
|
||||
if (isWidgetColorLighter(widgetColor)) {
|
||||
className += ' woot-widget-bubble-color--lighter';
|
||||
closeBtnClassName += ' woot-widget-bubble-color--lighter';
|
||||
}
|
||||
|
||||
const chatIcon = createBubbleIcon({
|
||||
className,
|
||||
path: bubbleSVG,
|
||||
target: chatBubble,
|
||||
});
|
||||
|
||||
addClasses(closeBubble, closeBtnClassName);
|
||||
|
||||
chatIcon.style.background = widgetColor;
|
||||
closeBubble.style.background = widgetColor;
|
||||
|
||||
bubbleHolder.appendChild(chatIcon);
|
||||
bubbleHolder.appendChild(closeBubble);
|
||||
onClickChatBubble();
|
||||
},
|
||||
toggleCloseButton: () => {
|
||||
let isMobile = false;
|
||||
if (window.matchMedia('(max-width: 668px)').matches) {
|
||||
isMobile = true;
|
||||
}
|
||||
IFrameHelper.sendMessage('toggle-close-button', { isMobile });
|
||||
},
|
||||
};
|
||||
112
research/chatwoot/app/javascript/sdk/bubbleHelpers.js
Normal file
112
research/chatwoot/app/javascript/sdk/bubbleHelpers.js
Normal file
@@ -0,0 +1,112 @@
|
||||
import { addClasses, removeClasses, toggleClass } from './DOMHelpers';
|
||||
import { IFrameHelper } from './IFrameHelper';
|
||||
import { isExpandedView } from './settingsHelper';
|
||||
import {
|
||||
CHATWOOT_CLOSED,
|
||||
CHATWOOT_OPENED,
|
||||
} from '../widget/constants/sdkEvents';
|
||||
import { dispatchWindowEvent } from 'shared/helpers/CustomEventHelper';
|
||||
|
||||
export const bubbleSVG =
|
||||
'M240.808 240.808H122.123C56.6994 240.808 3.45695 187.562 3.45695 122.122C3.45695 56.7031 56.6994 3.45697 122.124 3.45697C187.566 3.45697 240.808 56.7031 240.808 122.122V240.808Z';
|
||||
|
||||
export const body = document.getElementsByTagName('body')[0];
|
||||
export const widgetHolder = document.createElement('div');
|
||||
|
||||
export const bubbleHolder = document.createElement('div');
|
||||
export const chatBubble = document.createElement('button');
|
||||
export const closeBubble = document.createElement('button');
|
||||
export const notificationBubble = document.createElement('span');
|
||||
|
||||
export const setBubbleText = bubbleText => {
|
||||
if (isExpandedView(window.$chatwoot.type)) {
|
||||
const textNode = document.getElementById('woot-widget--expanded__text');
|
||||
textNode.innerText = bubbleText;
|
||||
}
|
||||
};
|
||||
|
||||
export const createBubbleIcon = ({ className, path, target }) => {
|
||||
let bubbleClassName = `${className} woot-elements--${window.$chatwoot.position}`;
|
||||
const bubbleIcon = document.createElementNS(
|
||||
'http://www.w3.org/2000/svg',
|
||||
'svg'
|
||||
);
|
||||
bubbleIcon.setAttributeNS(null, 'id', 'woot-widget-bubble-icon');
|
||||
bubbleIcon.setAttributeNS(null, 'width', '24');
|
||||
bubbleIcon.setAttributeNS(null, 'height', '24');
|
||||
bubbleIcon.setAttributeNS(null, 'viewBox', '0 0 240 240');
|
||||
bubbleIcon.setAttributeNS(null, 'fill', 'none');
|
||||
bubbleIcon.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
||||
|
||||
const bubblePath = document.createElementNS(
|
||||
'http://www.w3.org/2000/svg',
|
||||
'path'
|
||||
);
|
||||
bubblePath.setAttributeNS(null, 'd', path);
|
||||
bubblePath.setAttributeNS(null, 'fill', '#FFFFFF');
|
||||
|
||||
bubbleIcon.appendChild(bubblePath);
|
||||
target.appendChild(bubbleIcon);
|
||||
|
||||
if (isExpandedView(window.$chatwoot.type)) {
|
||||
const textNode = document.createElement('div');
|
||||
textNode.id = 'woot-widget--expanded__text';
|
||||
textNode.innerText = '';
|
||||
target.appendChild(textNode);
|
||||
bubbleClassName += ' woot-widget--expanded';
|
||||
}
|
||||
|
||||
target.className = bubbleClassName;
|
||||
target.title = 'Open chat window';
|
||||
return target;
|
||||
};
|
||||
|
||||
export const createBubbleHolder = hideMessageBubble => {
|
||||
if (hideMessageBubble) {
|
||||
addClasses(bubbleHolder, 'woot-hidden');
|
||||
}
|
||||
addClasses(bubbleHolder, 'woot--bubble-holder');
|
||||
bubbleHolder.id = 'cw-bubble-holder';
|
||||
bubbleHolder.dataset.turboPermanent = true;
|
||||
body.appendChild(bubbleHolder);
|
||||
};
|
||||
|
||||
const handleBubbleToggle = newIsOpen => {
|
||||
IFrameHelper.events.onBubbleToggle(newIsOpen);
|
||||
|
||||
if (newIsOpen) {
|
||||
dispatchWindowEvent({ eventName: CHATWOOT_OPENED });
|
||||
} else {
|
||||
dispatchWindowEvent({ eventName: CHATWOOT_CLOSED });
|
||||
chatBubble.focus();
|
||||
}
|
||||
};
|
||||
|
||||
export const onBubbleClick = (props = {}) => {
|
||||
const { toggleValue } = props;
|
||||
const { isOpen } = window.$chatwoot;
|
||||
if (isOpen === toggleValue) return;
|
||||
|
||||
const newIsOpen = toggleValue === undefined ? !isOpen : toggleValue;
|
||||
window.$chatwoot.isOpen = newIsOpen;
|
||||
|
||||
toggleClass(chatBubble, 'woot--hide');
|
||||
toggleClass(closeBubble, 'woot--hide');
|
||||
toggleClass(widgetHolder, 'woot--hide');
|
||||
|
||||
handleBubbleToggle(newIsOpen);
|
||||
};
|
||||
|
||||
export const onClickChatBubble = () => {
|
||||
bubbleHolder.addEventListener('click', onBubbleClick);
|
||||
};
|
||||
|
||||
export const addUnreadClass = () => {
|
||||
const holderEl = document.querySelector('.woot-widget-holder');
|
||||
addClasses(holderEl, 'has-unread-view');
|
||||
};
|
||||
|
||||
export const removeUnreadClass = () => {
|
||||
const holderEl = document.querySelector('.woot-widget-holder');
|
||||
removeClasses(holderEl, 'has-unread-view');
|
||||
};
|
||||
3
research/chatwoot/app/javascript/sdk/constants.js
Normal file
3
research/chatwoot/app/javascript/sdk/constants.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export const BUBBLE_DESIGN = ['standard', 'expanded_bubble'];
|
||||
export const WIDGET_DESIGN = ['standard', 'flat'];
|
||||
export const DARK_MODE = ['light', 'auto', 'dark'];
|
||||
45
research/chatwoot/app/javascript/sdk/cookieHelpers.js
Normal file
45
research/chatwoot/app/javascript/sdk/cookieHelpers.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import md5 from 'md5';
|
||||
import Cookies from 'js-cookie';
|
||||
|
||||
const REQUIRED_USER_KEYS = ['avatar_url', 'email', 'name'];
|
||||
const ALLOWED_USER_ATTRIBUTES = [...REQUIRED_USER_KEYS, 'identifier_hash'];
|
||||
|
||||
export const getUserCookieName = () => {
|
||||
const SET_USER_COOKIE_PREFIX = 'cw_user_';
|
||||
const { websiteToken: websiteIdentifier } = window.$chatwoot;
|
||||
return `${SET_USER_COOKIE_PREFIX}${websiteIdentifier}`;
|
||||
};
|
||||
|
||||
export const getUserString = ({ identifier = '', user }) => {
|
||||
const userStringWithSortedKeys = ALLOWED_USER_ATTRIBUTES.reduce(
|
||||
(acc, key) => `${acc}${key}${user[key] || ''}`,
|
||||
''
|
||||
);
|
||||
return `${userStringWithSortedKeys}identifier${identifier}`;
|
||||
};
|
||||
|
||||
export const computeHashForUserData = (...args) => md5(getUserString(...args));
|
||||
|
||||
export const hasUserKeys = user =>
|
||||
REQUIRED_USER_KEYS.reduce((acc, key) => acc || !!user[key], false);
|
||||
|
||||
export const setCookieWithDomain = (
|
||||
name,
|
||||
value,
|
||||
{ expires = 365, baseDomain = undefined } = {}
|
||||
) => {
|
||||
const cookieOptions = {
|
||||
expires,
|
||||
sameSite: 'Lax',
|
||||
domain: baseDomain,
|
||||
};
|
||||
|
||||
// if type of value is object, stringify it
|
||||
// this is because js-cookies 3.0 removed builtin json support
|
||||
// ref: https://github.com/js-cookie/js-cookie/releases/tag/v3.0.0
|
||||
if (typeof value === 'object') {
|
||||
value = JSON.stringify(value);
|
||||
}
|
||||
|
||||
Cookies.set(name, value, cookieOptions);
|
||||
};
|
||||
295
research/chatwoot/app/javascript/sdk/sdk.js
Normal file
295
research/chatwoot/app/javascript/sdk/sdk.js
Normal file
@@ -0,0 +1,295 @@
|
||||
export const SDK_CSS = `
|
||||
:root {
|
||||
--b-100: #F2F3F7;
|
||||
--s-700: #37546D;
|
||||
}
|
||||
|
||||
.woot-widget-holder {
|
||||
box-shadow: 0 5px 40px rgba(0, 0, 0, .16);
|
||||
opacity: 1;
|
||||
will-change: transform, opacity;
|
||||
transform: translateY(0);
|
||||
overflow: hidden !important;
|
||||
position: fixed !important;
|
||||
transition: opacity 0.2s linear, transform 0.25s linear;
|
||||
z-index: 2147483000 !important;
|
||||
}
|
||||
|
||||
.woot-widget-holder.woot-widget-holder--flat {
|
||||
box-shadow: none;
|
||||
border-radius: 0;
|
||||
border: 1px solid var(--b-100);
|
||||
}
|
||||
|
||||
.woot-widget-holder iframe {
|
||||
border: 0;
|
||||
color-scheme: normal;
|
||||
height: 100% !important;
|
||||
width: 100% !important;
|
||||
max-height: 100vh !important;
|
||||
}
|
||||
|
||||
.woot-widget-holder.has-unread-view {
|
||||
border-radius: 0 !important;
|
||||
min-height: 80px !important;
|
||||
height: auto;
|
||||
bottom: 94px;
|
||||
box-shadow: none !important;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.woot-widget-bubble {
|
||||
background: #1f93ff;
|
||||
border-radius: 100px;
|
||||
border-width: 0px;
|
||||
bottom: 20px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, .16) !important;
|
||||
cursor: pointer;
|
||||
height: 64px;
|
||||
padding: 0px;
|
||||
position: fixed;
|
||||
user-select: none;
|
||||
width: 64px;
|
||||
z-index: 2147483000 !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.woot-widget-bubble.woot-widget-bubble--flat {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.woot-widget-holder.woot-widget-holder--flat {
|
||||
bottom: 90px;
|
||||
}
|
||||
|
||||
.woot-widget-bubble.woot-widget-bubble--flat {
|
||||
height: 56px;
|
||||
width: 56px;
|
||||
}
|
||||
|
||||
.woot-widget-bubble.woot-widget-bubble--flat svg {
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
.woot-widget-bubble.woot-widget-bubble--flat.woot--close::before,
|
||||
.woot-widget-bubble.woot-widget-bubble--flat.woot--close::after {
|
||||
left: 28px;
|
||||
top: 16px;
|
||||
}
|
||||
|
||||
.woot-widget-bubble.unread-notification::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: #ff4040;
|
||||
border-radius: 100%;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
border: 2px solid #ffffff;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.woot-widget-bubble.woot-widget--expanded {
|
||||
bottom: 24px;
|
||||
display: flex;
|
||||
height: 48px !important;
|
||||
width: auto !important;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.woot-widget-bubble.woot-widget--expanded div {
|
||||
align-items: center;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen-Sans, Ubuntu, Cantarell, Helvetica Neue, Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
justify-content: center;
|
||||
padding-right: 20px;
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
.woot-widget-bubble.woot-widget--expanded.woot-widget-bubble-color--lighter div{
|
||||
color: var(--s-700);
|
||||
}
|
||||
|
||||
.woot-widget-bubble.woot-widget--expanded svg {
|
||||
height: 20px;
|
||||
margin: 14px 8px 14px 16px;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.woot-widget-bubble.woot-elements--left {
|
||||
left: 20px;
|
||||
}
|
||||
|
||||
.woot-widget-bubble.woot-elements--right {
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
.woot-widget-bubble:hover {
|
||||
background: #1f93ff;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, .4) !important;
|
||||
}
|
||||
|
||||
.woot-widget-bubble svg {
|
||||
all: revert;
|
||||
height: 24px;
|
||||
margin: 20px;
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
.woot-widget-bubble.woot-widget-bubble-color--lighter path{
|
||||
fill: var(--s-700);
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 667px) {
|
||||
.woot-widget-holder.woot-elements--left {
|
||||
left: 20px;
|
||||
}
|
||||
.woot-widget-holder.woot-elements--right {
|
||||
right: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.woot--close:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.woot--close::before, .woot--close::after {
|
||||
background-color: #fff;
|
||||
content: ' ';
|
||||
display: inline;
|
||||
height: 24px;
|
||||
left: 32px;
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
width: 2px;
|
||||
}
|
||||
|
||||
.woot-widget-bubble-color--lighter.woot--close::before, .woot-widget-bubble-color--lighter.woot--close::after {
|
||||
background-color: var(--s-700);
|
||||
}
|
||||
|
||||
.woot--close::before {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.woot--close::after {
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
.woot--hide {
|
||||
bottom: -100vh !important;
|
||||
top: unset !important;
|
||||
opacity: 0;
|
||||
visibility: hidden !important;
|
||||
z-index: -1 !important;
|
||||
}
|
||||
|
||||
.woot-widget--without-bubble {
|
||||
bottom: 20px !important;
|
||||
}
|
||||
.woot-widget-holder.woot--hide{
|
||||
transform: translateY(40px);
|
||||
}
|
||||
.woot-widget-bubble.woot--close {
|
||||
transform: translateX(0px) scale(1) rotate(0deg);
|
||||
transition: transform 300ms ease, opacity 100ms ease, visibility 0ms linear 0ms, bottom 0ms linear 0ms;
|
||||
}
|
||||
.woot-widget-bubble.woot--close.woot--hide {
|
||||
transform: translateX(8px) scale(.75) rotate(45deg);
|
||||
transition: transform 300ms ease, opacity 200ms ease, visibility 0ms linear 500ms, bottom 0ms ease 200ms;
|
||||
}
|
||||
|
||||
.woot-widget-bubble {
|
||||
transform-origin: center;
|
||||
will-change: transform, opacity;
|
||||
transform: translateX(0) scale(1) rotate(0deg);
|
||||
transition: transform 300ms ease, opacity 100ms ease, visibility 0ms linear 0ms, bottom 0ms linear 0ms;
|
||||
}
|
||||
.woot-widget-bubble.woot--hide {
|
||||
transform: translateX(8px) scale(.75) rotate(-30deg);
|
||||
transition: transform 300ms ease, opacity 200ms ease, visibility 0ms linear 500ms, bottom 0ms ease 200ms;
|
||||
}
|
||||
|
||||
.woot-widget-bubble.woot-widget--expanded {
|
||||
transform: translateX(0px);
|
||||
transition: transform 300ms ease, opacity 100ms ease, visibility 0ms linear 0ms, bottom 0ms linear 0ms;
|
||||
}
|
||||
.woot-widget-bubble.woot-widget--expanded.woot--hide {
|
||||
transform: translateX(8px);
|
||||
transition: transform 300ms ease, opacity 200ms ease, visibility 0ms linear 500ms, bottom 0ms ease 200ms;
|
||||
}
|
||||
.woot-widget-bubble.woot-widget-bubble--flat.woot--close {
|
||||
transform: translateX(0px);
|
||||
transition: transform 300ms ease, opacity 10ms ease, visibility 0ms linear 0ms, bottom 0ms linear 0ms;
|
||||
}
|
||||
.woot-widget-bubble.woot-widget-bubble--flat.woot--close.woot--hide {
|
||||
transform: translateX(8px);
|
||||
transition: transform 300ms ease, opacity 200ms ease, visibility 0ms linear 500ms, bottom 0ms ease 200ms;
|
||||
}
|
||||
.woot-widget-bubble.woot-widget--expanded.woot-widget-bubble--flat {
|
||||
transform: translateX(0px);
|
||||
transition: transform 300ms ease, opacity 200ms ease, visibility 0ms linear 0ms, bottom 0ms linear 0ms;
|
||||
}
|
||||
.woot-widget-bubble.woot-widget--expanded.woot-widget-bubble--flat.woot--hide {
|
||||
transform: translateX(8px);
|
||||
transition: transform 300ms ease, opacity 200ms ease, visibility 0ms linear 500ms, bottom 0ms ease 200ms;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 667px) {
|
||||
.woot-widget-holder {
|
||||
height: 100%;
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.woot-widget-holder iframe {
|
||||
min-height: 100% !important;
|
||||
}
|
||||
|
||||
|
||||
.woot-widget-holder.has-unread-view {
|
||||
height: auto;
|
||||
right: 0;
|
||||
width: auto;
|
||||
bottom: 0;
|
||||
top: auto;
|
||||
max-height: 100vh;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.woot-widget-holder.has-unread-view iframe {
|
||||
min-height: unset !important;
|
||||
}
|
||||
|
||||
.woot-widget-holder.has-unread-view.woot-elements--left {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.woot-widget-bubble.woot--close {
|
||||
bottom: 60px;
|
||||
opacity: 0;
|
||||
visibility: hidden !important;
|
||||
z-index: -1 !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 667px) {
|
||||
.woot-widget-holder {
|
||||
border-radius: 16px;
|
||||
bottom: 104px;
|
||||
height: calc(90% - 64px - 20px);
|
||||
max-height: 640px !important;
|
||||
min-height: 250px !important;
|
||||
width: 400px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.woot-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
`;
|
||||
14
research/chatwoot/app/javascript/sdk/settingsHelper.js
Normal file
14
research/chatwoot/app/javascript/sdk/settingsHelper.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { BUBBLE_DESIGN, DARK_MODE, WIDGET_DESIGN } from './constants';
|
||||
|
||||
export const getBubbleView = type =>
|
||||
BUBBLE_DESIGN.includes(type) ? type : BUBBLE_DESIGN[0];
|
||||
|
||||
export const isExpandedView = type => getBubbleView(type) === BUBBLE_DESIGN[1];
|
||||
|
||||
export const getWidgetStyle = style =>
|
||||
WIDGET_DESIGN.includes(style) ? style : WIDGET_DESIGN[0];
|
||||
|
||||
export const isFlatWidgetStyle = style => style === 'flat';
|
||||
|
||||
export const getDarkMode = darkMode =>
|
||||
DARK_MODE.includes(darkMode) ? darkMode : DARK_MODE[0];
|
||||
130
research/chatwoot/app/javascript/sdk/specs/cookieHelpers.spec.js
Normal file
130
research/chatwoot/app/javascript/sdk/specs/cookieHelpers.spec.js
Normal file
@@ -0,0 +1,130 @@
|
||||
import Cookies from 'js-cookie';
|
||||
import {
|
||||
getUserCookieName,
|
||||
getUserString,
|
||||
hasUserKeys,
|
||||
setCookieWithDomain,
|
||||
} from '../cookieHelpers';
|
||||
|
||||
describe('#getUserCookieName', () => {
|
||||
it('returns correct cookie name', () => {
|
||||
global.$chatwoot = { websiteToken: '123456' };
|
||||
expect(getUserCookieName()).toBe('cw_user_123456');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getUserString', () => {
|
||||
it('returns correct user string', () => {
|
||||
expect(
|
||||
getUserString({
|
||||
user: {
|
||||
name: 'Pranav',
|
||||
email: 'pranav@example.com',
|
||||
avatar_url: 'https://images.chatwoot.com/placeholder',
|
||||
identifier_hash: '12345',
|
||||
},
|
||||
identifier: '12345',
|
||||
})
|
||||
).toBe(
|
||||
'avatar_urlhttps://images.chatwoot.com/placeholderemailpranav@example.comnamePranavidentifier_hash12345identifier12345'
|
||||
);
|
||||
|
||||
expect(
|
||||
getUserString({
|
||||
user: {
|
||||
email: 'pranav@example.com',
|
||||
avatar_url: 'https://images.chatwoot.com/placeholder',
|
||||
},
|
||||
})
|
||||
).toBe(
|
||||
'avatar_urlhttps://images.chatwoot.com/placeholderemailpranav@example.comnameidentifier_hashidentifier'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#hasUserKeys', () => {
|
||||
it('checks whether the allowed list of keys are present', () => {
|
||||
expect(hasUserKeys({})).toBe(false);
|
||||
expect(hasUserKeys({ randomKey: 'randomValue' })).toBe(false);
|
||||
expect(hasUserKeys({ avatar_url: 'randomValue' })).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// Mock the 'set' method of the 'Cookies' object
|
||||
|
||||
describe('setCookieWithDomain', () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(Cookies, 'set');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should set a cookie with default parameters', () => {
|
||||
setCookieWithDomain('myCookie', 'cookieValue');
|
||||
|
||||
expect(Cookies.set).toHaveBeenCalledWith('myCookie', 'cookieValue', {
|
||||
expires: 365,
|
||||
sameSite: 'Lax',
|
||||
domain: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should set a cookie with custom expiration and sameSite attribute', () => {
|
||||
setCookieWithDomain('myCookie', 'cookieValue', {
|
||||
expires: 30,
|
||||
});
|
||||
|
||||
expect(Cookies.set).toHaveBeenCalledWith('myCookie', 'cookieValue', {
|
||||
expires: 30,
|
||||
sameSite: 'Lax',
|
||||
domain: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should set a cookie with a specific base domain', () => {
|
||||
setCookieWithDomain('myCookie', 'cookieValue', {
|
||||
baseDomain: 'example.com',
|
||||
});
|
||||
|
||||
expect(Cookies.set).toHaveBeenCalledWith('myCookie', 'cookieValue', {
|
||||
expires: 365,
|
||||
sameSite: 'Lax',
|
||||
domain: 'example.com',
|
||||
});
|
||||
});
|
||||
|
||||
it('should stringify the cookie value when setting the value', () => {
|
||||
setCookieWithDomain(
|
||||
'myCookie',
|
||||
{ value: 'cookieValue' },
|
||||
{
|
||||
baseDomain: 'example.com',
|
||||
}
|
||||
);
|
||||
|
||||
expect(Cookies.set).toHaveBeenCalledWith(
|
||||
'myCookie',
|
||||
JSON.stringify({ value: 'cookieValue' }),
|
||||
{
|
||||
expires: 365,
|
||||
sameSite: 'Lax',
|
||||
domain: 'example.com',
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should set a cookie with custom expiration, sameSite attribute, and specific base domain', () => {
|
||||
setCookieWithDomain('myCookie', 'cookieValue', {
|
||||
expires: 7,
|
||||
baseDomain: 'example.com',
|
||||
});
|
||||
|
||||
expect(Cookies.set).toHaveBeenCalledWith('myCookie', 'cookieValue', {
|
||||
expires: 7,
|
||||
sameSite: 'Lax',
|
||||
domain: 'example.com',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
import {
|
||||
getBubbleView,
|
||||
getWidgetStyle,
|
||||
isExpandedView,
|
||||
isFlatWidgetStyle,
|
||||
} from '../settingsHelper';
|
||||
|
||||
describe('#getBubbleView', () => {
|
||||
it('returns correct view', () => {
|
||||
expect(getBubbleView('')).toEqual('standard');
|
||||
expect(getBubbleView('standard')).toEqual('standard');
|
||||
expect(getBubbleView('expanded_bubble')).toEqual('expanded_bubble');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#isExpandedView', () => {
|
||||
it('returns true if it is expanded view', () => {
|
||||
expect(isExpandedView('')).toEqual(false);
|
||||
expect(isExpandedView('standard')).toEqual(false);
|
||||
expect(isExpandedView('expanded_bubble')).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getWidgetStyle', () => {
|
||||
it('returns correct view', () => {
|
||||
expect(getWidgetStyle('')).toEqual('standard');
|
||||
expect(getWidgetStyle('standard')).toEqual('standard');
|
||||
expect(getWidgetStyle('flat')).toEqual('flat');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#isFlatWidgetStyle', () => {
|
||||
it('returns true if it is expanded view', () => {
|
||||
expect(isFlatWidgetStyle('')).toEqual(false);
|
||||
expect(isFlatWidgetStyle('standard')).toEqual(false);
|
||||
expect(isFlatWidgetStyle('flat')).toEqual(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user