(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
This commit is contained in:
Dmitry S 2022-03-29 10:40:12 -04:00
parent 499e24b744
commit d55bdbcdf3
2 changed files with 55 additions and 24 deletions

View File

@ -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 <base href>.
//
// Here we set a <base href> 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 <base href>.
//
// Here we set a <base href> 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 {

View File

@ -53,10 +53,14 @@ export function reportMessage(msg: string, options?: Partial<INotifyOptions>) {
}
/**
* 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<INotifyOptions>) {
reportMessage(msg, {level: 'warning', ...options});
options = {level: 'warning', ...options};
log.warn(`${options.level}: `, msg);
_logError(msg);
reportMessage(msg, options);
}
/**