From d55bdbcdf3a1100594a2c089fd00d099f0a4c572 Mon Sep 17 00:00:00 2001 From: Dmitry S Date: Tue, 29 Mar 2022 10:40:12 -0400 Subject: [PATCH] (core) Reopen HelpScout beacon to the last-opened article Summary: - When opening HelpScout beacon to an article ("answers"), avoid a 'navigate' call to let the beacon show the previously open article. - Work around a bug with reloading a page with a beacon article open: HelpScout renders the last state without triggering usual events. - Report errors to server when beacon fails to load. - reportWarning() method now reports the message to the server. Test Plan: Added a test case Reviewers: paulfitz Reviewed By: paulfitz Differential Revision: https://phab.getgrist.com/D3345 --- app/client/lib/helpScout.ts | 71 +++++++++++++++++++++++++------------ app/client/models/errors.ts | 8 +++-- 2 files changed, 55 insertions(+), 24 deletions(-) diff --git a/app/client/lib/helpScout.ts b/app/client/lib/helpScout.ts index 110318ed..19c522cf 100644 --- a/app/client/lib/helpScout.ts +++ b/app/client/lib/helpScout.ts @@ -15,7 +15,8 @@ // tslint:disable:unified-signatures -import {AppModel, reportError} from 'app/client/models/AppModel'; +import {AppModel} from 'app/client/models/AppModel'; +import {reportWarning} from 'app/client/models/errors'; import {IAppError} from 'app/client/models/NotifyModel'; import {GristLoadConfig} from 'app/common/gristUrls'; import {timeFormat} from 'app/common/timeFormat'; @@ -64,7 +65,8 @@ export function Beacon(method: 'navigate', route: string): void; export function Beacon(method: 'identify', userObj: IUserObj): void; export function Beacon(method: 'prefill', formObj: IFormObj): void; export function Beacon(method: 'config', configObj: object): void; -export function Beacon(method: 'on'|'off'|'once', event: 'open'|'close', callback: () => void): void; +export function Beacon(method: 'on'|'once', event: string, callback: () => void): void; +export function Beacon(method: 'off', event: string, callback?: () => void): void; export function Beacon(method: 'session-data', data: ISessionData): void; export function Beacon(method: BeaconCmd): void; export function Beacon(method: BeaconCmd, options?: unknown, data?: unknown) { @@ -85,21 +87,30 @@ function initBeacon(): void { const beaconId = gristConfig && gristConfig.helpScoutBeaconId; if (beaconId) { (window as any).Beacon = _beacon; - document.head.appendChild(dom('script', { - type: 'text/javascript', - src: 'https://beacon-v2.helpscout.net', - async: true, - })); + document.head.appendChild(dom('script', + { + type: 'text/javascript', + src: 'https://beacon-v2.helpscout.net', + async: true, + }, + // Report when the beacon fails to load so that the user knows something is wrong, and we + // have a log of the error. (Note: might not report all failures due to ad-blockers.) + dom.on('error', (e) => { + reportWarning("Support form failed to load. " + + "Please email support@getgrist.com with questions instead."); + }), + )); _beacon('init', beaconId); _beacon('config', {display: {style: "manual"}}); } else { (window as any).Beacon = () => null; - reportError(new Error("Support form is not configured")); + reportWarning("Support form is not configured"); } } } let lastOpenType: 'error' | 'message' = 'message'; +let lastRoute: BeaconRoute|null = null; /** * Helper to open a beacon, taking care of setting focus appropriately. Calls optional onOpen @@ -118,31 +129,33 @@ function _beaconOpen(userObj: IUserObj|null, options: IBeaconOpenOptions) { lastOpenType = openType; } + const route: BeaconRoute = options.route || (errors?.length ? '/ask/message/' : '/answers/'); + // If beacon was and still is being opened for help articles, avoid the 'navigate' call + // altogether, to keep the beacon at the last article it was on. + const skipNav = (route === lastRoute && route === '/answers/'); + lastRoute = route; + Beacon('once', 'open', () => { const iframe = document.querySelector('#beacon-container iframe') as HTMLIFrameElement; if (iframe) { iframe.focus(); } if (onOpen) { onOpen(); } }); - Beacon('once', 'article-viewed' as any, () => { - // HelpScout creates an iframe with an empty 'src' attribute, then writes to it. In such an - // iframe, different browsers interpret relative links differently: Chrome's are relative to - // the parent page's URL; Firefox's are relative to the parent page's . - // - // Here we set a explicitly in the iframe to get consistent behavior of links - // relative to the top page's URL (HelpScout then seems to handle clicks on them correctly). - const iframe = document.querySelector('#beacon-container iframe') as HTMLIFrameElement; - iframe?.contentDocument?.head.appendChild(dom('base', {href: ''})); - }); + // Fix base-href tag when opening an article. + Beacon('once', 'article-viewed', () => fixBeaconBaseHref()); + // We duplicate this check for 'ready' event, because 'open' and 'article-viewed' events don't + // trigger on page reload when a beacon article is already open (seems to be a HelpScout bug). + Beacon('once', 'ready', () => fixBeaconBaseHref()); + Beacon('once', 'close', () => { const iframe = document.querySelector('#beacon-container iframe') as HTMLIFrameElement; if (iframe) { iframe.blur(); } + Beacon('off', 'article-viewed'); }); if (userObj) { Beacon('identify', userObj); } const attrs: ISessionData = {}; - let route: BeaconRoute; if (errors?.length) { // If sending errors, prefill part of the message (the user sees this and can add to it), and // include more detailed errors with stack traces into session-data. @@ -162,10 +175,8 @@ function _beaconOpen(userObj: IUserObj|null, options: IBeaconOpenOptions) { attrs[`error-${i}-stack`] = JSON.stringify(error.stack.trim().split('\n')); } }); - route = options.route || '/ask/message/'; } else { Beacon('config', {messaging: {contactForm: {showSubject: true}}}); - route = options.route || '/answers/'; } Beacon('session-data', { @@ -173,7 +184,23 @@ function _beaconOpen(userObj: IUserObj|null, options: IBeaconOpenOptions) { ...attrs, }); Beacon('open'); - Beacon('navigate', route); + if (!skipNav) { + Beacon('navigate', route); + } +} + +function fixBeaconBaseHref() { + // HelpScout creates an iframe with an empty 'src' attribute, then writes to it. In such an + // iframe, different browsers interpret relative links differently: Chrome's are relative to + // the parent page's URL; Firefox's are relative to the parent page's . + // + // Here we set a explicitly in the iframe to get consistent behavior of links + // relative to the top page's URL (HelpScout then seems to handle clicks on them correctly). + const iframe = document.querySelector('#beacon-container iframe') as HTMLIFrameElement; + const iframeDoc = iframe?.contentDocument; + if (iframeDoc && !iframeDoc.querySelector('head > base')) { + iframeDoc.head.appendChild(dom('base', {href: ''})); + } } export interface IBeaconOpenOptions { diff --git a/app/client/models/errors.ts b/app/client/models/errors.ts index 72d25cdb..a6e0b207 100644 --- a/app/client/models/errors.ts +++ b/app/client/models/errors.ts @@ -53,10 +53,14 @@ export function reportMessage(msg: string, options?: Partial) { } /** - * Shows warning toast notification (with yellow styling). + * Shows warning toast notification (with yellow styling), and log to server and to console. Pass + * {level: 'error'} for same behavior with adjusted styling. */ export function reportWarning(msg: string, options?: Partial) { - reportMessage(msg, {level: 'warning', ...options}); + options = {level: 'warning', ...options}; + log.warn(`${options.level}: `, msg); + _logError(msg); + reportMessage(msg, options); } /**