import { get, map, omit, isEqual, isEmpty } from 'lodash' import { Store } from 'react-chrome-redux' import { accessToken, user, userId, userName, squareAvatarUrl } from './modules/auth/selectors' import { organizationUrl } from './modules/embed/selectors' import * as onboardingActions from './modules/onboarding/actions' import * as orgSelector from './modules/org/selectors' import * as profileSelector from './modules/profile/selectors' import * as settingsActions from './modules/settings/actions' import * as settingsSelectors from './modules/settings/selectors' import * as gatesSelectors from './modules/gates/selectors' import { extractGmailIdFailed, sentEmail } from './modules/analytics/actions' import { getOneShotTokens, bulkGetClickTrackIds, patchClickTrackIds, getOnboardingHyperlinkFromContentOptions, getLatestCalendarAvailability, getMeetingBaseUrlFromContentOptions, createErrorMessage, } from './utils/contentHelpers' import { getApplicableLinksFromDoc, addIdentityTrackingToLink, shouldParseLink, replaceLinksWithWrappedLinksInInbox, isCalendaringLink, encodeRecipients, replaceLinksWithWCalendarLinkInInbox, } from './utils/email' import { IWrapLinksData, IExtractedLink, IClickTrackData, IContentOptions, IContentMainOptions, IShouldParseLinkOpts, } from './utils/contentInterfaces' import Raven from './utils/sentry' declare const chrome: any require('./content-main.styl') // new additions const jq: any = require('jquery') import { v4 as uuid } from 'uuid' import { white_icon_src } from '../img/inlineImages' declare const InboxSDK: any import * as React from 'react' import { renderToString } from 'react-dom/server' import StatusBar from './ExtensionComponents/StatusBar' import OneClickScheduling from './ExtensionComponents/OneClickScheduling' import { CLICK_TRACKING_URL } from './utils/ResourceConstants' import * as composeViewsActions from './modules/composeViews/actions' import * as composeViewsSelectors from './modules/composeViews/selectors' import * as userPropertiesSelectors from './modules/userProperties/selectors' require('../img/drift_on.png') require('../img/drift_off.png') require('../img/cal.png') require('../img/warning.png') require('../img/loader.gif') import { Provider } from 'react-redux' import { render } from 'react-dom' import { IMeeting } from './modules/meetingTypes/interfaces' const store = new Store({ portName: 'DRIFT', // communication port name }) store.ready().then(() => { const state = store.getState() if ( !accessToken(state) || !(gatesSelectors.hasNewCalendaring(state) || gatesSelectors.hasSalesCalendaring(state)) ) { let token: string = null let contentMainReady: boolean = false let contentOptions: IContentOptions = { orgUrl: null, handle: null, globalHandle: null, orgHandle: null, profileVisibility: null, } let contentMainOptions: IContentMainOptions = { orgId: null, userId: null, userName: '', avatarUrl: '', handle: null, bccStatus: false, bccEmailAddress: '', onboardingHyperlink: '', inboxesToRunExtensionOn: {}, meetingBaseUrl: '', } let frameRef: any = null const hasChanged = (value: any, nextValue: any) => !isEqual(value, nextValue) store.subscribe(() => { const state = store.getState() const nextToken = accessToken(state) const nextContentOptions = { orgUrl: organizationUrl(state), handle: profileSelector.handle(state), globalHandle: profileSelector.globalHandle(state), orgHandle: orgSelector.orgHandle(state), profileVisibility: profileSelector.visibility(state), } const nextContentMainOption = { orgId: get(user(state), 'orgId', null), userId: userId(state), userName: userName(state), avatarUrl: squareAvatarUrl(state), handle: profileSelector.handle(state), bccStatus: settingsSelectors.bccStatus(state), bccEmailAddress: settingsSelectors.bccEmailAddress(state), onboardingHyperlink: getOnboardingHyperlinkFromContentOptions(nextContentOptions), inboxesToRunExtensionOn: settingsSelectors.inboxesToRunExtensionOn(state), meetingBaseUrl: getMeetingBaseUrlFromContentOptions(nextContentOptions), } if (hasChanged(token, nextToken)) { token = nextToken updateAuthStatus() } if (hasChanged(contentOptions, nextContentOptions)) { contentOptions = nextContentOptions } if (hasChanged(contentMainOptions, nextContentMainOption)) { contentMainOptions = nextContentMainOption updateContentMainOptions() } }) try { // pass through for messages from chrome to window chrome.runtime.onMessage.addListener(({ type }: { type: string }) => window.postMessage({ type }, '*')) } catch (error) { console.log(error) } const updateAuthStatus = () => { if (contentMainReady) { window.postMessage({ type: '@@driftext/update_auth_status', authStatus: !!token }, '*') } } const updateContentMainOptions = () => { if (contentMainReady) { window.postMessage({ contentMainOptions, type: '@@driftext/update_content_main_options' }, '*') } } const fetchToken = (to: string): Promise => { if (!token) { console.warn('tried to generate Drift token prior to authenticating - please log in first.') return } return getOneShotTokens(token, to).then(({ oneshotToken }) => oneshotToken) } const bulkFetchClickTrackId = (links: IExtractedLink[]) => { if (!token) { console.warn('tried to generate Drift token prior to authenticating - please log in first.') return } return bulkGetClickTrackIds(token, links) } const patchClickLink = (emailUUID: string, gmailId: string, clickId: string) => { if (!token) { console.warn('tried to generate Drift token prior to authenticating - please log in first.') return } return patchClickTrackIds(token, clickId, { emailUUID, gmailId }) } const wrapLinks = ({ id, to, links }: IWrapLinksData) => { fetchToken(to) .then( oneShotToken => links && links.map((link) => { const originalUrlOrIdentifiedUrl = shouldParseLink(link, contentOptions) ? addIdentityTrackingToLink(link.url, oneShotToken) : link.url return { ...link, url: originalUrlOrIdentifiedUrl } }), ) .then(bulkFetchClickTrackId) .then((wrappedLinks) => { window.postMessage({ id, to, wrappedLinks, type: '@@driftext/wrapped_links' }, '*') }) .catch((err: Error) => { Raven.captureException(err) window.postMessage({ id, to, wrappedLinks: [], type: '@@driftext/wrapped_links' }, '*') }) } const patchClickTracksWithEmailId = ({ emailUUID, gmailId, clickIds }: any) => { clickIds && clickIds.forEach((clickData: IClickTrackData) => { patchClickLink(emailUUID, gmailId, clickData.clickId) }) } const latestCalendarAvailability = ({ id }: { id: string }) => { getLatestCalendarAvailability(token, contentMainOptions.userId).then((data: any) => { window.postMessage({ id, data, type: '@@driftext/latest_calendar_availability' }, '*') }) } const messageEventHandler = (event: any) => { if ( (event.source !== window && event.source !== (frameRef && frameRef.contentWindow)) || !event.data.type ) { return } switch (event.data.type) { case '@@driftext/ready': contentMainReady = true const { inboxesToRunExtensionOn } = contentMainOptions if (event.data.email && !get(inboxesToRunExtensionOn, event.data.email)) { store.dispatch( settingsActions.setInboxesToRunExtensionOn({ [event.data.email]: isEmpty(inboxesToRunExtensionOn), }), ) } updateAuthStatus() updateContentMainOptions() break case '@@driftext/wrap_links': wrapLinks(event.data) break case '@@driftext/get_latest_calendar_availability': latestCalendarAvailability(event.data) break case '@@driftext/patch_click_tracks_with_email_id': patchClickTracksWithEmailId(event.data) break case '@@driftext/extract_gmail_id_failed': store.dispatch(extractGmailIdFailed(event.data)) break case '@@driftext/sent_email': store.dispatch(sentEmail(omit(event.data, 'type'))) break case '@@driftext/toggle_onboarding': event.data.visible ? jq('#DRIFT_ONBOARDING').show() : jq('#DRIFT_ONBOARDING').hide() break case '@@driftext/send_test_email': if (contentMainReady) { window.postMessage({ type: '@@driftext/create_test_email' }, '*') } else { setTimeout(() => { window.postMessage({ type: '@@driftext/create_test_email' }, '*') }, 1000) } break case '@@driftext/onboarding_email_sent': store.dispatch(onboardingActions.onboardingEmailSent(omit(event.data, 'type'))) setTimeout(() => jq('#DRIFT_ONBOARDING').show(), 300) break case '@@driftext/capture_error': Raven.captureException(new Error(event.data.error)) break } } window.addEventListener('message', messageEventHandler, false) // inject content-main into page store.ready().then(() => { const s = document.createElement('script') chrome.storage.sync.get('seenGmailOnboarding', (data: any) => { if (!data.seenGmailOnboarding) { chrome.storage.sync.set({ seenGmailOnboarding: true }) const frame = document.createElement('iframe') as any frame.id = 'DRIFT_ONBOARDING' frame.frameBorder = '0' frame.style.border = 'none' frame.style.position = 'absolute' frame.style['z-index'] = 2147483645 frame.style.top = 0 frame.style.bottom = 0 frame.style.left = 0 frame.style.right = 0 frame.style.margin = 'auto auto' frame.style.height = '100%' frame.style.width = '100%' frame.style.background = 'rgba(255,255,255,0.9)' frameRef = frame try { frame.src = chrome.extension.getURL('onboarding.html') } catch (e) { console.warn('couldn\'t load onboarding.html', e) } try { (document.body || document.documentElement).appendChild(frame) store.dispatch(onboardingActions.userHasSeenOnboarding(true)) } catch (e) { console.warn('couldn\'t add to head', e) } } }) try { s.src = chrome.extension.getURL('content-main.js') } catch (e) { console.warn('couldn\'t load content-main.js or content-main.css', e) } try { (document.head || document.documentElement).appendChild(s) } catch (e) { console.warn('couldn\'t add to head', e) } chrome.runtime.connect() chrome.runtime.onMessage.addListener((message: any) => { switch (message.type) { case '@@driftext-content/show_modal': window.postMessage({ ...message, type: '@@driftext/show_modal' }, '*') break } }) }) } else { let frameRef: any = null const waitForEl = (selector: string, callback: () => any) => { if (!jq(selector).length) { setTimeout(() => { window.requestAnimationFrame(() => { waitForEl(selector, callback) }) }, 100) } else { callback() } } const gmailBanner = ` ` const fetchToken = (to: string): Promise => { const token = accessToken(store.getState()) if (!token) { console.warn('tried to generate Drift token prior to authenticating - please log in first.') return } return getOneShotTokens(token, to).then(({ oneshotToken }) => oneshotToken) } const bulkFetchClickTrackId = (links: IExtractedLink[]) => { const token = accessToken(store.getState()) if (!token) { console.warn('tried to generate Drift token prior to authenticating - please log in first.') return } return bulkGetClickTrackIds(token, links) } const patchClickLink = (emailUUID: string, gmailId: string, clickId: string) => { const token = accessToken(store.getState()) if (!token) { console.warn('tried to generate Drift token prior to authenticating - please log in first.') return } return patchClickTrackIds(token, clickId, { emailUUID, gmailId }) } const patchClickTracksWithEmailId = ({ emailUUID, gmailId, clickIds }: any) => { clickIds && clickIds.forEach((clickData: IClickTrackData) => { patchClickLink(emailUUID, gmailId, clickData.clickId) }) } jq(jq.parseHTML(gmailBanner)).insertAfter('.nH.w-asV.aiw') const gmailBannerClose = jq('.drift-outbound-gmail-notification .close') gmailBannerClose.click(() => jq('.drift-outbound-gmail-notification').hide()) let onboardingComposeId: string = null InboxSDK.load(1, 'sdk_drift-email_d533357b1d').then((sdk: any) => { const userEmail = sdk.User.getEmailAddress() const inboxesToRunExtensionOn = settingsSelectors.inboxesToRunExtensionOn(store.getState()) if (userEmail && !get(inboxesToRunExtensionOn, userEmail)) { store.dispatch( settingsActions.setInboxesToRunExtensionOn({ [userEmail]: isEmpty(inboxesToRunExtensionOn), }), ) } // the SDK has been loaded, now do something with it! sdk.Compose.registerComposeViewHandler((composeView: any) => { if (!get(inboxesToRunExtensionOn, userEmail, false) || !accessToken(store.getState())) { return } // update gmail elements waitForEl(`.aA4`, () => { jq(`.aA4`).hide() }) waitForEl('.Ur .aX', () => { jq('.Ur .aX').addClass('move-format-bar-for-drift') }) const composeViewUUID = uuid() let driftParsingDone = false let meetingBaseUrl: string = null let clickData: IClickTrackData[] const emailUUID = uuid() const toRecipientsLength = composeView.getToRecipients().length const ccRecipentsLength = composeView.getCcRecipients().length store.dispatch( composeViewsActions.createComposeViewState({ composeViewUUID, recipientLength: toRecipientsLength + ccRecipentsLength, }), ) const statusBar: any = composeView.addStatusBar({ addAboveNativeStatusBar: true, }) const calendarOnClick = (slug: string, customMeeting: IMeeting) => { store.dispatch( composeViewsActions.driftCalenderInjectionLoading({ composeViewUUID, calendarInjectionLoading: true, }), ) const state = store.getState() getLatestCalendarAvailability(accessToken(state), userId(state), slug).then( ({ slots, slotDuration, agentTimezone }) => { const options = { orgUrl: organizationUrl(state), handle: profileSelector.handle(state), globalHandle: profileSelector.globalHandle(state), orgHandle: orgSelector.orgHandle(state), profileVisibility: profileSelector.visibility(state), } meetingBaseUrl = getMeetingBaseUrlFromContentOptions(options, slug) const oneClickSchedulingProps = { agentTimezone, meetingBaseUrl, agentName: userName(state), agentAvatarUrl: squareAvatarUrl(state), meetingDuration: slotDuration, meetingName: customMeeting.meetingTemplate.name, availabilitySlots: slots, widgetColor: orgSelector.widgetColor(state), } composeView.insertHTMLIntoBodyAtCursor( renderToString(), ) store.dispatch( composeViewsActions.driftCalenderInjectionLoading({ composeViewUUID, calendarInjectionLoading: false, }), ) }, ) } render( , statusBar.el, ) composeView.on('recipientsChanged', (event: any) => { const toRecipientsLength = composeView.getToRecipients().length const ccRecipentsLength = composeView.getCcRecipients().length const recipientLength = toRecipientsLength + ccRecipentsLength store.dispatch(composeViewsActions.driftRecipientLength({ composeViewUUID, recipientLength })) if ( !userPropertiesSelectors.userHasMeetingManagment(state) && !gatesSelectors.hasSalesCalendaring(state) ) { if (recipientLength > 1) { const injection = jq('.drift-email-injection') injection && jq(jq.parseHTML(createErrorMessage(chrome.extension.getURL('warning.png')))).insertBefore( '.drift-email-injection', ) injection && jq(jq.parseHTML(`
`)).insertAfter( '.drift-email-injection>tbody', ) } else { jq('.calendar-error').remove() jq('.calendar-error-message').remove() } } }) composeView.on('presending', (event: any) => { const recipients = composeView.getToRecipients() const ccRecipents = composeView.getCcRecipients() const recipientLength = recipients.length + ccRecipents.length const state = store.getState() const shouldTrackEmail = composeViewsSelectors.shouldTrackEmail(state, { composeViewUUID }) if (driftParsingDone) { return } event.cancel() const bccStatus = settingsSelectors.bccStatus(state) const bccEmailAddress = settingsSelectors.bccEmailAddress(state) const options = { orgUrl: organizationUrl(state), handle: profileSelector.handle(state), globalHandle: profileSelector.globalHandle(state), orgHandle: orgSelector.orgHandle(state), profileVisibility: profileSelector.visibility(state), } as IShouldParseLinkOpts const endUserEmail = recipients[0].emailAddress const emailSubject = composeView.getSubject() const existingBCCEmailAddresses = map(composeView.getBccRecipients(), 'emailAddress') if (bccStatus && bccEmailAddress) { composeView.setBccRecipients( existingBCCEmailAddresses.length ? [...existingBCCEmailAddresses, bccEmailAddress] : [bccEmailAddress], ) } const body = composeView.getBodyElement() const links = getApplicableLinksFromDoc(body) const extractedLinks = links.map((a: HTMLAnchorElement): IExtractedLink => { const href = a.getAttribute('href') const linkText = a.textContent return { linkText, emailSubject, endUserEmail, emailUUID, url: href } }) as IExtractedLink[] if (shouldTrackEmail) { fetchToken(endUserEmail) .then( oneShotToken => extractedLinks && extractedLinks.map(link => { const originalUrlOrIdentifiedUrl = shouldParseLink(link, options) ? addIdentityTrackingToLink(link.url, oneShotToken) : link.url return { ...link, url: originalUrlOrIdentifiedUrl } }), ) .then(bulkFetchClickTrackId) .then((wrappedLinks: IClickTrackData[]) => { clickData = wrappedLinks const replacedBody = replaceLinksWithWrappedLinksInInbox(body, wrappedLinks) composeView.setBodyHTML(replacedBody) composeView.setBodyHTML( composeView.getHTMLContent() + ``, ) driftParsingDone = true composeView.send() }) } else { const calendarParsedLinks = map(extractedLinks, link => { if (isCalendaringLink(link.url, meetingBaseUrl) && recipientLength !== 1) { return { ...link, url: encodeRecipients(link.url, [...recipients, ...ccRecipents]) } } else { return link } }) const replacedBody = replaceLinksWithWCalendarLinkInInbox(body, calendarParsedLinks) composeView.setBodyHTML(replacedBody) driftParsingDone = true composeView.send() } }) composeView.on('sent', (event: any) => { if (!composeViewsSelectors.shouldTrackEmail(store.getState(), { composeViewUUID })) { return } driftParsingDone = false return event.getMessageID().then((messageId: string) => { const endUserEmail = composeView.getToRecipients()[0].emailAddress const emailSubject = composeView.getSubject() if (composeView.getElement().id === onboardingComposeId && endUserEmail === 'hi@drift.com') { store.dispatch( onboardingActions.onboardingEmailSent({ subject: emailSubject, gmailId: messageId }), ) setTimeout(() => jq('#DRIFT_ONBOARDING').show(), 300) } patchClickTracksWithEmailId({ emailUUID, gmailId: messageId, clickIds: clickData }) return store.dispatch( sentEmail({ emailUUID, endUserEmail, subject: emailSubject, gmailId: messageId }), ) }) }) }) const messageEventHandler = (event: any) => { if ( (event.source !== window && event.source !== (frameRef && frameRef.contentWindow)) || !event.data.type ) { return } switch (event.data.type) { case '@@driftext/toggle_onboarding': event.data.visible ? jq('#DRIFT_ONBOARDING').show() : jq('#DRIFT_ONBOARDING').hide() break case '@@driftext/send_test_email': sdk.Compose.openNewComposeView().then((composeView: any) => { onboardingComposeId = composeView.getElement().id const state = store.getState() const options = { orgUrl: organizationUrl(state), handle: profileSelector.handle(state), globalHandle: profileSelector.globalHandle(state), orgHandle: orgSelector.orgHandle(state), profileVisibility: profileSelector.visibility(state), } as IShouldParseLinkOpts composeView.setToRecipients(['hi@drift.com']) composeView.setSubject('Drift Extension') composeView.setBodyHTML(` Hey there!

Welcome to the Drift Extension!

A couple tips to get started:

1) Use hyperlinks to link to pages where Drift is installed if you want to greet recipients with a personalized message.

2) Add one recipient per email. We won't track emails with multiple recipients to ensure we're identifying the right person. And our links will only identify a person once in case your email gets shared :)

Go ahead and give it a try. Just hit “Send with Drift” and you’ll get notified as soon as we open your email.
`) }) break } } window.addEventListener('message', messageEventHandler, false) }) store.ready().then(() => { chrome.storage.sync.get('seenGmailOnboarding', (data: any) => { if (!data.seenGmailOnboarding) { chrome.storage.sync.set({ seenGmailOnboarding: true }) const frame = document.createElement('iframe') as any frame.id = 'DRIFT_ONBOARDING' frame.frameBorder = '0' frame.style.border = 'none' frame.style.position = 'absolute' frame.style['z-index'] = 2147483645 frame.style.top = 0 frame.style.bottom = 0 frame.style.left = 0 frame.style.right = 0 frame.style.margin = 'auto auto' frame.style.height = '100%' frame.style.width = '100%' frame.style.background = 'rgba(255,255,255,0.9)' frameRef = frame try { frame.src = chrome.extension.getURL('onboarding.html') } catch (e) { console.warn('couldn\'t load onboarding.html', e) } try { (document.body || document.documentElement).appendChild(frame) store.dispatch(onboardingActions.userHasSeenOnboarding(true)) } catch (e) { console.warn('couldn\'t add to head', e) } } }) chrome.runtime.connect() chrome.runtime.onMessage.addListener((message: any) => { switch (message.type) { case '@@driftext-content/show_modal': // need to wait for gmail to finish loading waitForEl('.nH.w-asV.aiw', () => { jq(jq.parseHTML(gmailBanner)).insertAfter('.nH.w-asV.aiw') const gmailBannerClose = jq('.drift-outbound-gmail-notification .close') gmailBannerClose.click(() => jq('.drift-outbound-gmail-notification').hide()) jq('#drift-outbound-banner-email').text(message.email || 'the notification') jq('.drift-outbound-gmail-notification').css('display', 'flex') }) break } }) }) } })