From 6c94768bdbe1da26c8009d8ab97c4d4343a1729e Mon Sep 17 00:00:00 2001 From: Pranav Raj S Date: Fri, 25 Feb 2022 16:18:18 +0530 Subject: [PATCH] feat: Add `flat` design for widget (#4065) --- app/controllers/widget_tests_controller.rb | 26 ++++++++-- app/javascript/packs/sdk.js | 27 +++------- app/javascript/sdk/IFrameHelper.js | 24 ++++++--- app/javascript/sdk/bubbleHelpers.js | 6 +-- app/javascript/sdk/constants.js | 1 + app/javascript/sdk/cookieHelpers.js | 18 +++++++ app/javascript/sdk/sdk.js | 51 ++++++++++++++++--- app/javascript/sdk/settingsHelper.js | 11 ++++ .../sdk/specs/bubbleHelpers.spec.js | 17 ------- .../sdk/specs/cookieHelpers.spec.js | 43 +++++++++++++++- .../sdk/specs/settingsHelpers.spec.js | 38 ++++++++++++++ app/javascript/specs/packs/sdk.spec.js | 38 -------------- app/javascript/widget/App.vue | 2 + app/javascript/widget/assets/scss/woot.scss | 27 ++++++++++ .../widget/components/ChatFooter.vue | 4 +- .../components/layouts/ViewWithHeader.vue | 1 - .../widget/store/modules/appConfig.js | 21 +++++--- app/views/widget_tests/index.html.erb | 5 +- 18 files changed, 251 insertions(+), 109 deletions(-) create mode 100644 app/javascript/sdk/settingsHelper.js delete mode 100644 app/javascript/sdk/specs/bubbleHelpers.spec.js create mode 100644 app/javascript/sdk/specs/settingsHelpers.spec.js delete mode 100644 app/javascript/specs/packs/sdk.spec.js diff --git a/app/controllers/widget_tests_controller.rb b/app/controllers/widget_tests_controller.rb index 22a8c0da8..fff47d907 100644 --- a/app/controllers/widget_tests_controller.rb +++ b/app/controllers/widget_tests_controller.rb @@ -1,5 +1,8 @@ class WidgetTestsController < ActionController::Base - before_action :set_web_widget + before_action :ensure_web_widget + before_action :ensure_widget_position + before_action :ensure_widget_type + before_action :ensure_widget_style def index render @@ -7,7 +10,24 @@ class WidgetTestsController < ActionController::Base private - def set_web_widget - @web_widget = Channel::WebWidget.first + def ensure_widget_style + @widget_style = params[:widget_style] || 'standard' + end + + def ensure_widget_position + @widget_position = params[:position] || 'left' + end + + def ensure_widget_type + @widget_type = params[:type] || 'expanded_bubble' + end + + def inbox_id + @inbox_id ||= params[:inbox_id] || Channel::WebWidget.first.inbox.id + end + + def ensure_web_widget + @inbox = Inbox.find(inbox_id) + @web_widget = @inbox.channel end end diff --git a/app/javascript/packs/sdk.js b/app/javascript/packs/sdk.js index 10e21741a..c4bfcdb08 100755 --- a/app/javascript/packs/sdk.js +++ b/app/javascript/packs/sdk.js @@ -1,25 +1,11 @@ import Cookies from 'js-cookie'; import { IFrameHelper } from '../sdk/IFrameHelper'; -import { getBubbleView } from '../sdk/bubbleHelpers'; -import md5 from 'md5'; -import { getUserCookieName } from '../sdk/cookieHelpers'; - -const REQUIRED_USER_KEYS = ['avatar_url', 'email', 'name']; - -const ALLOWED_USER_ATTRIBUTES = [...REQUIRED_USER_KEYS, 'identifier_hash']; - -export const getUserString = ({ identifier = '', user }) => { - const userStringWithSortedKeys = ALLOWED_USER_ATTRIBUTES.reduce( - (acc, key) => `${acc}${key}${user[key] || ''}`, - '' - ); - return `${userStringWithSortedKeys}identifier${identifier}`; -}; - -const computeHashForUserData = (...args) => md5(getUserString(...args)); - -export const hasUserKeys = user => - REQUIRED_USER_KEYS.reduce((acc, key) => acc || !!user[key], false); +import { getBubbleView } from '../sdk/settingsHelper'; +import { + computeHashForUserData, + getUserCookieName, + hasUserKeys, +} from '../sdk/cookieHelpers'; const runSDK = ({ baseUrl, websiteToken }) => { if (window.$chatwoot) { @@ -38,6 +24,7 @@ const runSDK = ({ baseUrl, websiteToken }) => { type: getBubbleView(chatwootSettings.type), launcherTitle: chatwootSettings.launcherTitle || '', showPopoutButton: chatwootSettings.showPopoutButton || false, + widgetStyle: chatwootSettings.widgetStyle || 'standard', toggle(state) { IFrameHelper.events.toggleBubble(state); diff --git a/app/javascript/sdk/IFrameHelper.js b/app/javascript/sdk/IFrameHelper.js index a5a90dee7..2d851ba49 100644 --- a/app/javascript/sdk/IFrameHelper.js +++ b/app/javascript/sdk/IFrameHelper.js @@ -26,6 +26,7 @@ import { dispatchWindowEvent } from 'shared/helpers/CustomEventHelper'; import { CHATWOOT_ERROR, CHATWOOT_READY } from '../widget/constants/sdkEvents'; import { SET_USER_ERROR } from '../widget/constants/errorTypes'; import { getUserCookieName } from './cookieHelpers'; +import { isFlatWidgetStyle } from './settingsHelper'; export const IFrameHelper = { getUrl({ baseUrl, websiteToken }) { @@ -52,6 +53,10 @@ export const IFrameHelper = { if (window.$chatwoot.hideMessageBubble) { holderClassName += ` woot-widget--without-bubble`; } + if (isFlatWidgetStyle(window.$chatwoot.widgetStyle)) { + holderClassName += ` woot-widget-holder--flat`; + } + addClass(widgetHolder, holderClassName); widgetHolder.appendChild(iframe); body.appendChild(widgetHolder); @@ -121,6 +126,7 @@ export const IFrameHelper = { position: window.$chatwoot.position, hideMessageBubble: window.$chatwoot.hideMessageBubble, showPopoutButton: window.$chatwoot.showPopoutButton, + widgetStyle: window.$chatwoot.widgetStyle, }); IFrameHelper.onLoad({ widgetColor: message.config.channelConfig.widgetColor, @@ -222,21 +228,27 @@ export const IFrameHelper = { createBubbleHolder(); onLocationChangeListener(); if (!window.$chatwoot.hideMessageBubble) { + 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'; + } + const chatIcon = createBubbleIcon({ - className: 'woot-widget-bubble', + className, src: bubbleImg, target: chatBubble, }); - const closeIcon = closeBubble; - const closeIconclassName = `woot-elements--${window.$chatwoot.position} woot-widget-bubble woot--close woot--hide`; - addClass(closeIcon, closeIconclassName); + addClass(closeBubble, closeBtnClassName); chatIcon.style.background = widgetColor; - closeIcon.style.background = widgetColor; + closeBubble.style.background = widgetColor; bubbleHolder.appendChild(chatIcon); - bubbleHolder.appendChild(closeIcon); + bubbleHolder.appendChild(closeBubble); bubbleHolder.appendChild(createNotificationBubble()); onClickChatBubble(); } diff --git a/app/javascript/sdk/bubbleHelpers.js b/app/javascript/sdk/bubbleHelpers.js index 740d83733..18f8e2fbc 100644 --- a/app/javascript/sdk/bubbleHelpers.js +++ b/app/javascript/sdk/bubbleHelpers.js @@ -1,6 +1,6 @@ import { addClass, removeClass, toggleClass, wootOn } from './DOMHelpers'; import { IFrameHelper } from './IFrameHelper'; -import { BUBBLE_DESIGN } from './constants'; +import { isExpandedView } from './settingsHelper'; export const bubbleImg = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAAAUVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////8IN+deAAAAGnRSTlMAAwgJEBk0TVheY2R5eo+ut8jb5OXs8fX2+cjRDTIAAADsSURBVHgBldZbkoMgFIThRgQv8SKKgGf/C51UnJqaRI30/9zfe+NQUQ3TvG7bOk9DVeCmshmj/CuOTYnrdBfkUOg0zlOtl9OWVuEk4+QyZ3DIevmSt/ioTvK1VH/s5bY3YdM9SBZ/mUUyWgx+U06ycgp7D8msxSvtc4HXL9BLdj2elSEfhBJAI0QNgJEBI1BEBsQClVBVGDgwYOLAhJkDM1YOrNg4sLFAsLJgZsHEgoEFFQt0JAFGFjQsKAMJ0LFAexKgZYFyJIDxJIBNJEDNAtSJBLCeBDCOBFAPzwFA94ED+zmhwDO9358r8ANtIsMXi7qVAwAAAABJRU5ErkJggg=='; @@ -13,10 +13,6 @@ export const chatBubble = document.createElement('button'); export const closeBubble = document.createElement('button'); export const notificationBubble = document.createElement('span'); -export const getBubbleView = type => - BUBBLE_DESIGN.includes(type) ? type : BUBBLE_DESIGN[0]; -export const isExpandedView = type => getBubbleView(type) === BUBBLE_DESIGN[1]; - export const setBubbleText = bubbleText => { if (isExpandedView(window.$chatwoot.type)) { const textNode = document.getElementById('woot-widget--expanded__text'); diff --git a/app/javascript/sdk/constants.js b/app/javascript/sdk/constants.js index f35378a47..52faf1540 100644 --- a/app/javascript/sdk/constants.js +++ b/app/javascript/sdk/constants.js @@ -1 +1,2 @@ export const BUBBLE_DESIGN = ['standard', 'expanded_bubble']; +export const WIDGET_DESIGN = ['standard', 'flat']; diff --git a/app/javascript/sdk/cookieHelpers.js b/app/javascript/sdk/cookieHelpers.js index d56448569..afdc12cc2 100644 --- a/app/javascript/sdk/cookieHelpers.js +++ b/app/javascript/sdk/cookieHelpers.js @@ -1,5 +1,23 @@ +import md5 from 'md5'; + +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); diff --git a/app/javascript/sdk/sdk.js b/app/javascript/sdk/sdk.js index 2e2aca7aa..bbd920382 100644 --- a/app/javascript/sdk/sdk.js +++ b/app/javascript/sdk/sdk.js @@ -1,5 +1,10 @@ -export const SDK_CSS = `.woot-widget-holder { - box-shadow: 0 5px 40px rgba(0, 0, 0, .16) !important; +export const SDK_CSS = ` +:root { + --b-100: #F2F3F7; +} + +.woot-widget-holder { + box-shadow: 0 5px 40px rgba(0, 0, 0, .16); opacity: 1; will-change: transform, opacity; transform: translateY(0); @@ -9,6 +14,12 @@ export const SDK_CSS = `.woot-widget-holder { 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; height: 100% !important; @@ -22,21 +33,45 @@ export const SDK_CSS = `.woot-widget-holder { height: auto; bottom: 94px; box-shadow: none !important; + border: 0; } .woot-widget-bubble { background: #1f93ff; - border-radius: 100px !important; + border-radius: 100px; border-width: 0px; bottom: 20px; - padding: 0px; box-shadow: 0 8px 24px rgba(0, 0, 0, .16) !important; cursor: pointer; - height: 64px !important; + height: 64px; + padding: 0px; position: fixed; - width: 64px !important; - z-index: 2147483000 !important; user-select: none; + width: 64px; + z-index: 2147483000 !important; +} + +.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 img { + 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 { @@ -184,7 +219,7 @@ export const SDK_CSS = `.woot-widget-holder { @media only screen and (min-width: 667px) { .woot-widget-holder { - border-radius: 16px !important; + border-radius: 16px; bottom: 104px; height: calc(85% - 64px - 20px); max-height: 590px !important; diff --git a/app/javascript/sdk/settingsHelper.js b/app/javascript/sdk/settingsHelper.js new file mode 100644 index 000000000..4e256b53a --- /dev/null +++ b/app/javascript/sdk/settingsHelper.js @@ -0,0 +1,11 @@ +import { BUBBLE_DESIGN, 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'; diff --git a/app/javascript/sdk/specs/bubbleHelpers.spec.js b/app/javascript/sdk/specs/bubbleHelpers.spec.js deleted file mode 100644 index e67cc4c96..000000000 --- a/app/javascript/sdk/specs/bubbleHelpers.spec.js +++ /dev/null @@ -1,17 +0,0 @@ -import { getBubbleView, isExpandedView } from '../bubbleHelpers'; - -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); - }); -}); diff --git a/app/javascript/sdk/specs/cookieHelpers.spec.js b/app/javascript/sdk/specs/cookieHelpers.spec.js index c0edc1348..103224760 100644 --- a/app/javascript/sdk/specs/cookieHelpers.spec.js +++ b/app/javascript/sdk/specs/cookieHelpers.spec.js @@ -1,4 +1,8 @@ -import { getUserCookieName } from '../cookieHelpers'; +import { + getUserCookieName, + getUserString, + hasUserKeys, +} from '../cookieHelpers'; describe('#getUserCookieName', () => { it('returns correct cookie name', () => { @@ -6,3 +10,40 @@ describe('#getUserCookieName', () => { 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); + }); +}); diff --git a/app/javascript/sdk/specs/settingsHelpers.spec.js b/app/javascript/sdk/specs/settingsHelpers.spec.js new file mode 100644 index 000000000..55648f3f2 --- /dev/null +++ b/app/javascript/sdk/specs/settingsHelpers.spec.js @@ -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); + }); +}); diff --git a/app/javascript/specs/packs/sdk.spec.js b/app/javascript/specs/packs/sdk.spec.js deleted file mode 100644 index c9c546cdb..000000000 --- a/app/javascript/specs/packs/sdk.spec.js +++ /dev/null @@ -1,38 +0,0 @@ -import { getUserString, hasUserKeys } from '../../packs/sdk'; - -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); - }); -}); diff --git a/app/javascript/widget/App.vue b/app/javascript/widget/App.vue index 7b047f7cc..8b34c51bf 100755 --- a/app/javascript/widget/App.vue +++ b/app/javascript/widget/App.vue @@ -12,6 +12,7 @@ 'is-mobile': isMobile, 'is-widget-right': isRightAligned, 'is-bubble-hidden': hideMessageBubble, + 'is-flat-design': isWidgetStyleFlat, }" > @@ -61,6 +62,7 @@ export default { isWidgetOpen: 'appConfig/getIsWidgetOpen', messageCount: 'conversation/getMessageCount', unreadMessageCount: 'conversation/getUnreadMessageCount', + isWidgetStyleFlat: 'appConfig/isWidgetStyleFlat', }), isIFrame() { return IFrameHelper.isIFrame(); diff --git a/app/javascript/widget/assets/scss/woot.scss b/app/javascript/widget/assets/scss/woot.scss index 29d5851fa..986dd390b 100755 --- a/app/javascript/widget/assets/scss/woot.scss +++ b/app/javascript/widget/assets/scss/woot.scss @@ -57,3 +57,30 @@ body { padding-left: $space-normal; } } + +.is-flat-design { + .chat-bubble { + border-bottom-left-radius: 0 !important; + border-bottom-right-radius: 0 !important; + border-top-left-radius: 0 !important; + border-top-right-radius: 0 !important; + box-shadow: none; + } + + button { + border-radius: 0 !important; + } + + input { + border-radius: 0; + } + + .chat-message--input { + border-radius: 0 !important; + box-shadow: none !important; + + &.is-focused { + box-shadow: none !important; + } + } +} diff --git a/app/javascript/widget/components/ChatFooter.vue b/app/javascript/widget/components/ChatFooter.vue index f399ff252..423eb2642 100755 --- a/app/javascript/widget/components/ChatFooter.vue +++ b/app/javascript/widget/components/ChatFooter.vue @@ -1,7 +1,8 @@