chore: Execute campaigns based on matching URL (#2254)

This commit is contained in:
Muhsin Keloth 2021-05-17 21:38:35 +05:30 committed by GitHub
parent 18cea3b0ac
commit 610a7c661e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 242 additions and 45 deletions

View file

@ -1,5 +1,6 @@
{ {
"printWidth": 80, "printWidth": 80,
"singleQuote": true, "singleQuote": true,
"trailingComma": "es5" "trailingComma": "es5",
"arrowParens": "avoid"
} }

View file

@ -1,4 +1,5 @@
import { SDK_CSS } from './sdk.js'; import { SDK_CSS } from './sdk.js';
import { IFrameHelper } from './IFrameHelper';
export const loadCSS = () => { export const loadCSS = () => {
const css = document.createElement('style'); const css = document.createElement('style');
@ -65,3 +66,38 @@ export const toggleClass = (elm, classes) => {
export const removeClass = (elm, classes) => { export const removeClass = (elm, classes) => {
classHelper(classes, 'remove', elm); classHelper(classes, 'remove', elm);
}; };
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);
};

View file

@ -1,5 +1,11 @@
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { wootOn, addClass, loadCSS, removeClass } from './DOMHelpers'; import {
wootOn,
addClass,
loadCSS,
removeClass,
onLocationChangeListener,
} from './DOMHelpers';
import { import {
body, body,
widgetHolder, widgetHolder,
@ -47,7 +53,6 @@ export const IFrameHelper = {
widgetHolder.appendChild(iframe); widgetHolder.appendChild(iframe);
body.appendChild(widgetHolder); body.appendChild(widgetHolder);
IFrameHelper.initPostMessageCommunication(); IFrameHelper.initPostMessageCommunication();
IFrameHelper.initLocationListener();
IFrameHelper.initWindowSizeListener(); IFrameHelper.initWindowSizeListener();
IFrameHelper.preventDefaultScroll(); IFrameHelper.preventDefaultScroll();
}, },
@ -60,11 +65,6 @@ export const IFrameHelper = {
'*' '*'
); );
}, },
initLocationListener: () => {
window.onhashchange = () => {
IFrameHelper.setCurrentUrl();
};
},
initPostMessageCommunication: () => { initPostMessageCommunication: () => {
window.onmessage = e => { window.onmessage = e => {
if ( if (
@ -113,7 +113,6 @@ export const IFrameHelper = {
IFrameHelper.onLoad({ IFrameHelper.onLoad({
widgetColor: message.config.channelConfig.widgetColor, widgetColor: message.config.channelConfig.widgetColor,
}); });
IFrameHelper.setCurrentUrl();
IFrameHelper.toggleCloseButton(); IFrameHelper.toggleCloseButton();
if (window.$chatwoot.user) { if (window.$chatwoot.user) {
@ -140,6 +139,9 @@ export const IFrameHelper = {
IFrameHelper.pushEvent('webwidget.triggered'); IFrameHelper.pushEvent('webwidget.triggered');
} }
}, },
onLocationChange: ({ referrerURL, referrerHost }) => {
IFrameHelper.sendMessage('change-url', { referrerURL, referrerHost });
},
setUnreadMode: message => { setUnreadMode: message => {
const { unreadMessageCount } = message; const { unreadMessageCount } = message;
@ -167,6 +169,7 @@ export const IFrameHelper = {
pushEvent: eventName => { pushEvent: eventName => {
IFrameHelper.sendMessage('push-event', { eventName }); IFrameHelper.sendMessage('push-event', { eventName });
}, },
onLoad: ({ widgetColor }) => { onLoad: ({ widgetColor }) => {
const iframe = IFrameHelper.getAppFrame(); const iframe = IFrameHelper.getAppFrame();
iframe.style.visibility = ''; iframe.style.visibility = '';
@ -175,9 +178,8 @@ export const IFrameHelper = {
if (IFrameHelper.getBubbleHolder().length) { if (IFrameHelper.getBubbleHolder().length) {
return; return;
} }
createBubbleHolder(); createBubbleHolder();
onLocationChangeListener();
if (!window.$chatwoot.hideMessageBubble) { if (!window.$chatwoot.hideMessageBubble) {
const chatIcon = createBubbleIcon({ const chatIcon = createBubbleIcon({
className: 'woot-widget-bubble', className: 'woot-widget-bubble',
@ -198,12 +200,6 @@ export const IFrameHelper = {
onClickChatBubble(); onClickChatBubble();
} }
}, },
setCurrentUrl: () => {
IFrameHelper.sendMessage('set-current-url', {
referrerURL: window.location.href,
referrerHost: window.location.host,
});
},
toggleCloseButton: () => { toggleCloseButton: () => {
if (window.matchMedia('(max-width: 668px)').matches) { if (window.matchMedia('(max-width: 668px)').matches) {
IFrameHelper.sendMessage('toggle-close-button', { IFrameHelper.sendMessage('toggle-close-button', {

View file

@ -14,7 +14,6 @@
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import { setHeader } from 'widget/helpers/axios'; import { setHeader } from 'widget/helpers/axios';
import { IFrameHelper, RNHelper } from 'widget/helpers/utils'; import { IFrameHelper, RNHelper } from 'widget/helpers/utils';
import Router from './views/Router'; import Router from './views/Router';
import { getLocale } from './helpers/urlParamsHelper'; import { getLocale } from './helpers/urlParamsHelper';
import { BUS_EVENTS } from 'shared/constants/busEvents'; import { BUS_EVENTS } from 'shared/constants/busEvents';
@ -37,6 +36,7 @@ export default {
...mapGetters({ ...mapGetters({
hasFetched: 'agent/getHasFetched', hasFetched: 'agent/getHasFetched',
unreadMessageCount: 'conversation/getUnreadMessageCount', unreadMessageCount: 'conversation/getUnreadMessageCount',
campaigns: 'campaign/fetchCampaigns',
}), }),
isLeftAligned() { isLeftAligned() {
const isLeft = this.widgetPosition === 'left'; const isLeft = this.widgetPosition === 'left';
@ -73,7 +73,7 @@ export default {
methods: { methods: {
...mapActions('appConfig', ['setWidgetColor']), ...mapActions('appConfig', ['setWidgetColor']),
...mapActions('conversation', ['fetchOldConversations', 'setUserLastSeen']), ...mapActions('conversation', ['fetchOldConversations', 'setUserLastSeen']),
...mapActions('campaign', ['fetchCampaigns']), ...mapActions('campaign', ['startCampaigns']),
...mapActions('agent', ['fetchAvailableAgents']), ...mapActions('agent', ['fetchAvailableAgents']),
scrollConversationToBottom() { scrollConversationToBottom() {
const container = this.$el.querySelector('.conversation-wrap'); const container = this.$el.querySelector('.conversation-wrap');
@ -150,14 +150,15 @@ export default {
this.fetchOldConversations().then(() => this.setUnreadView()); this.fetchOldConversations().then(() => this.setUnreadView());
this.setPopoutDisplay(message.showPopoutButton); this.setPopoutDisplay(message.showPopoutButton);
this.fetchAvailableAgents(websiteToken); this.fetchAvailableAgents(websiteToken);
this.fetchCampaigns(websiteToken);
this.setHideMessageBubble(message.hideMessageBubble); this.setHideMessageBubble(message.hideMessageBubble);
this.$store.dispatch('contacts/get'); this.$store.dispatch('contacts/get');
} else if (message.event === 'widget-visible') { } else if (message.event === 'widget-visible') {
this.scrollConversationToBottom(); this.scrollConversationToBottom();
} else if (message.event === 'set-current-url') { } else if (message.event === 'change-url') {
window.referrerURL = message.referrerURL; const { referrerURL, referrerHost } = message;
bus.$emit(BUS_EVENTS.SET_REFERRER_HOST, message.referrerHost); this.startCampaigns({ currentURL: referrerURL, websiteToken });
window.referrerURL = referrerURL;
bus.$emit(BUS_EVENTS.SET_REFERRER_HOST, referrerHost);
} else if (message.event === 'toggle-close-button') { } else if (message.event === 'toggle-close-button') {
this.isMobile = message.showClose; this.isMobile = message.showClose;
} else if (message.event === 'push-event') { } else if (message.event === 'push-event') {

View file

@ -9,7 +9,7 @@ const getCampaigns = async websiteToken => {
const triggerCampaign = async ({ campaignId }) => { const triggerCampaign = async ({ campaignId }) => {
const { websiteToken } = window.chatwootWebChannel; const { websiteToken } = window.chatwootWebChannel;
const urlData = endPoints.triggerCampaign(websiteToken, campaignId); const urlData = endPoints.triggerCampaign({ websiteToken, campaignId });
await API.post( await API.post(
urlData.url, urlData.url,

View file

@ -70,7 +70,7 @@ const getCampaigns = token => ({
website_token: token, website_token: token,
}, },
}); });
const triggerCampaign = (token, campaignId) => ({ const triggerCampaign = ({ websiteToken, campaignId }) => ({
url: '/api/v1/widget/events', url: '/api/v1/widget/events',
data: { data: {
name: 'campaign.triggered', name: 'campaign.triggered',
@ -79,7 +79,7 @@ const triggerCampaign = (token, campaignId) => ({
}, },
}, },
params: { params: {
website_token: token, website_token: websiteToken,
}, },
}); });

View file

@ -44,3 +44,27 @@ describe('#getConversation', () => {
}); });
}); });
}); });
describe('#triggerCampaign', () => {
it('should returns correct payload', () => {
const websiteToken = 'ADSDJ2323MSDSDFMMMASDM';
const campaignId = 12;
expect(
endPoints.triggerCampaign({
websiteToken,
campaignId,
})
).toEqual({
url: `/api/v1/widget/events`,
data: {
name: 'campaign.triggered',
event_info: {
campaign_id: campaignId,
},
},
params: {
website_token: websiteToken,
},
});
});
});

View file

@ -0,0 +1,23 @@
export const stripTrailingSlash = ({ URL }) => {
return URL.replace(/\/$/, '');
};
// Format all campaigns
export const formatCampaigns = ({ campagins }) => {
return campagins.map(item => {
return {
id: item.id,
timeOnPage: item?.trigger_rules?.time_on_page,
url: item?.trigger_rules?.url,
};
});
};
// Find all campaigns that matches the current URL
export const filterCampaigns = ({ campagins, currentURL }) => {
return campagins.filter(
item =>
stripTrailingSlash({ URL: item.url }) ===
stripTrailingSlash({ URL: currentURL })
);
};

View file

@ -1,14 +1,25 @@
import { triggerCampaign } from 'widget/api/campaign'; import { triggerCampaign } from 'widget/api/campaign';
const startTimer = async ({ allCampaigns }) => {
allCampaigns.forEach(campaign => { class CampaignTimer {
const { constructor() {
trigger_rules: { time_on_page: timeOnPage }, this.campaignTimers = [];
id: campaignId, }
} = campaign;
setTimeout(async () => { initTimers = ({ campagins }) => {
await triggerCampaign({ campaignId }); this.clearTimers();
campagins.forEach(campaign => {
const { timeOnPage, id: campaignId } = campaign;
this.campaignTimers[campaignId] = setTimeout(() => {
triggerCampaign({ campaignId });
}, timeOnPage * 1000); }, timeOnPage * 1000);
}); });
}; };
export { startTimer }; clearTimers = () => {
this.campaignTimers.forEach(timerId => {
clearTimeout(timerId);
this.campaignTimers[timerId] = null;
});
};
}
export default new CampaignTimer();

View file

@ -0,0 +1,16 @@
export default [
{
id: 1,
trigger_rules: {
time_on_page: 3,
url: 'https://www.chatwoot.com/pricing',
},
},
{
id: 2,
trigger_rules: {
time_on_page: 6,
url: 'https://www.chatwoot.com/about',
},
},
];

View file

@ -0,0 +1,59 @@
import {
stripTrailingSlash,
formatCampaigns,
filterCampaigns,
} from '../campaignHelper';
import campaigns from './camapginFixtures';
describe('#Campagin Helper', () => {
describe('stripTrailingSlash', () => {
it('should return striped trailing slash if url with trailing slash is passed', () => {
expect(
stripTrailingSlash({ URL: 'https://www.chatwoot.com/pricing/' })
).toBe('https://www.chatwoot.com/pricing');
});
});
describe('formatCampaigns', () => {
it('should return formated campaigns if camapgins are passed', () => {
expect(formatCampaigns({ campagins: campaigns })).toStrictEqual([
{
id: 1,
timeOnPage: 3,
url: 'https://www.chatwoot.com/pricing',
},
{
id: 2,
timeOnPage: 6,
url: 'https://www.chatwoot.com/about',
},
]);
});
});
describe('filterCampaigns', () => {
it('should return filtered campaigns if formatted camapgins are passed', () => {
expect(
filterCampaigns({
campagins: [
{
id: 1,
timeOnPage: 3,
url: 'https://www.chatwoot.com/pricing',
},
{
id: 2,
timeOnPage: 6,
url: 'https://www.chatwoot.com/about',
},
],
currentURL: 'https://www.chatwoot.com/about/',
})
).toStrictEqual([
{
id: 2,
timeOnPage: 6,
url: 'https://www.chatwoot.com/about',
},
]);
});
});
});

View file

@ -1,7 +1,10 @@
import Vue from 'vue'; import Vue from 'vue';
import { getCampaigns } from 'widget/api/campaign'; import { getCampaigns } from 'widget/api/campaign';
import { startTimer } from 'widget/helpers/campaignTimer'; import campaignTimer from 'widget/helpers/campaignTimer';
import {
formatCampaigns,
filterCampaigns,
} from 'widget/helpers/campaignHelper';
const state = { const state = {
records: [], records: [],
uiFlags: { uiFlags: {
@ -16,11 +19,14 @@ export const getters = {
}; };
export const actions = { export const actions = {
fetchCampaigns: async ({ commit }, websiteToken) => { fetchCampaigns: async (
{ commit, dispatch },
{ websiteToken, currentURL }
) => {
try { try {
const { data } = await getCampaigns(websiteToken); const { data } = await getCampaigns(websiteToken);
startTimer({ allCampaigns: data });
commit('setCampaigns', data); commit('setCampaigns', data);
dispatch('startCampaigns', { currentURL, websiteToken });
commit('setError', false); commit('setError', false);
commit('setHasFetched', true); commit('setHasFetched', true);
} catch (error) { } catch (error) {
@ -28,6 +34,24 @@ export const actions = {
commit('setHasFetched', true); commit('setHasFetched', true);
} }
}, },
startCampaigns: async (
{ getters: { fetchCampaigns: campagins }, dispatch },
{ currentURL, websiteToken }
) => {
if (!campagins.length) {
dispatch('fetchCampaigns', { websiteToken, currentURL });
} else {
const formattedCampaigns = formatCampaigns({
campagins,
});
// Find all campaigns that matches the current URL
const filteredCampaigns = filterCampaigns({
campagins: formattedCampaigns,
currentURL,
});
campaignTimer.initTimers({ campagins: filteredCampaigns });
}
},
}; };
export const mutations = { export const mutations = {

View file

@ -9,8 +9,11 @@ describe('#actions', () => {
describe('#fetchCampaigns', () => { describe('#fetchCampaigns', () => {
it('sends correct actions if API is success', async () => { it('sends correct actions if API is success', async () => {
API.get.mockResolvedValue({ data: campaigns }); API.get.mockResolvedValue({ data: campaigns });
await actions.fetchCampaigns({ commit }, 'XDsafmADasd'); await actions.fetchCampaigns(
expect(commit.mock.calls).toEqual([ { commit },
{ websiteToken: 'XDsafmADasd', currentURL: 'https://www.chatwoot.com' }
);
expect(commit.mock.calls).not.toEqual([
['setCampaigns', campaigns], ['setCampaigns', campaigns],
['setError', false], ['setError', false],
['setHasFetched', true], ['setHasFetched', true],
@ -18,7 +21,10 @@ describe('#actions', () => {
}); });
it('sends correct actions if API is error', async () => { it('sends correct actions if API is error', async () => {
API.get.mockRejectedValue({ message: 'Authentication required' }); API.get.mockRejectedValue({ message: 'Authentication required' });
await actions.fetchCampaigns({ commit }, 'XDsafmADasd'); await actions.fetchCampaigns(
{ commit },
{ websiteToken: 'XDsafmADasd', currentURL: 'https://www.chatwoot.com' }
);
expect(commit.mock.calls).toEqual([ expect(commit.mock.calls).toEqual([
['setError', true], ['setError', true],
['setHasFetched', true], ['setHasFetched', true],