diff --git a/app/client/lib/helpScout.ts b/app/client/lib/helpScout.ts index fc083c56..110318ed 100644 --- a/app/client/lib/helpScout.ts +++ b/app/client/lib/helpScout.ts @@ -29,6 +29,8 @@ export type BeaconCmd = 'init' | 'destroy' | 'open' | 'close' | 'toggle' | 'sear 'article' | 'navigate' | 'identify' | 'prefill' | 'reset' | 'logout' | 'config' | 'on' | 'off' | 'once' | 'event' | 'session-data'; +export type BeaconRoute = '/ask/message/' | '/answers/'; + export interface IUserObj { name?: string; email?: string; @@ -70,6 +72,8 @@ export function Beacon(method: BeaconCmd, options?: unknown, data?: unknown) { (window as any).Beacon(method, options, data); } +// This is essentially what's done by the code snippet that HelpScout suggests to install in every +// page. In Grist app pages, we only load HelpScout code when the beacon is opened. function _beacon(method: BeaconCmd, options?: unknown, data?: unknown) { _beacon.readyQueue.push({method, options, data}); } @@ -103,7 +107,7 @@ let lastOpenType: 'error' | 'message' = 'message'; * If errors is given, prepares a form for submitting an error report, and includes stack traces * into the session-data. */ -function _beaconOpen(userObj: IUserObj|null, options: {onOpen?: () => void, errors?: IAppError[]}) { +function _beaconOpen(userObj: IUserObj|null, options: IBeaconOpenOptions) { const {onOpen, errors} = options; // The beacon remembers its content, so reset it when switching between reporting errors and @@ -119,6 +123,16 @@ function _beaconOpen(userObj: IUserObj|null, options: {onOpen?: () => void, erro 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: ''})); + }); Beacon('once', 'close', () => { const iframe = document.querySelector('#beacon-container iframe') as HTMLIFrameElement; if (iframe) { iframe.blur(); } @@ -128,6 +142,7 @@ function _beaconOpen(userObj: IUserObj|null, options: {onOpen?: () => void, erro } 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. @@ -147,8 +162,10 @@ function _beaconOpen(userObj: IUserObj|null, options: {onOpen?: () => void, erro 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', { @@ -156,7 +173,7 @@ function _beaconOpen(userObj: IUserObj|null, options: {onOpen?: () => void, erro ...attrs, }); Beacon('open'); - Beacon('navigate', '/ask/message/'); + Beacon('navigate', route); } export interface IBeaconOpenOptions { @@ -164,6 +181,7 @@ export interface IBeaconOpenOptions { includeAppErrors?: boolean; onOpen?: () => void; errors?: IAppError[]; + route?: BeaconRoute; } /** diff --git a/app/client/ui/LeftPanelCommon.ts b/app/client/ui/LeftPanelCommon.ts index cf9c5a15..b4c82ca8 100644 --- a/app/client/ui/LeftPanelCommon.ts +++ b/app/client/ui/LeftPanelCommon.ts @@ -25,23 +25,22 @@ import {dom, DomContents, Observable, styled} from 'grainjs'; * HelpCenter in a new tab. */ export function createHelpTools(appModel: AppModel, spacer = true): DomContents { - const isEfcr = (appModel.topAppModel.productFlavor === 'efcr'); return [ spacer ? cssSpacer() : null, - cssPageEntry( - cssPageLink(cssPageIcon('Feedback'), - cssLinkText('Give Feedback'), - dom.on('click', () => beaconOpenMessage({appModel})), + cssSplitPageEntry( + cssPageEntryMain( + cssPageLink(cssPageIcon('Help'), + cssLinkText('Help Center'), + dom.cls('tour-help-center'), + dom.on('click', (ev) => beaconOpenMessage({appModel})), + testId('left-feedback'), + ), ), - dom.hide(isEfcr), - testId('left-feedback'), - ), - cssPageEntry( - cssPageLink(cssPageIcon('Help'), {href: commonUrls.help, target: '_blank'}, - cssLinkText('Help Center'), - dom.cls('tour-help-center') - ), - dom.hide(isEfcr), + cssPageEntrySmall( + cssPageLink(cssPageIcon('FieldLink'), + {href: commonUrls.help, target: '_blank'}, + ), + ) ), ]; } @@ -156,3 +155,28 @@ export const cssPageIcon = styled(icon, ` export const cssSpacer = styled('div', ` height: 18px; `); + +const cssSplitPageEntry = styled('div', ` + display: flex; + align-items: center; +`); + +const cssPageEntryMain = styled(cssPageEntry, ` + flex: auto; + margin: 0; +`); + +const cssPageEntrySmall = styled(cssPageEntry, ` + flex: none; + border-radius: 3px; + --icon-color: ${colors.lightGreen}; + & > .${cssPageLink.className} { + padding: 0 8px 0 16px; + } + &:hover { + --icon-color: ${colors.darkGreen}; + } + .${cssTools.className}-collapsed & { + display: none; + } +`); diff --git a/app/client/ui/NotifyUI.ts b/app/client/ui/NotifyUI.ts index b2fa9b26..ca3fcbca 100644 --- a/app/client/ui/NotifyUI.ts +++ b/app/client/ui/NotifyUI.ts @@ -145,7 +145,7 @@ function buildNotifyDropdown(ctl: IOpenController, notifier: Notifier, appModel: cssDropdownFeedbackLink( cssDropdownFeedbackIcon('Feedback'), 'Give feedback', - dom.on('click', () => beaconOpenMessage({appModel, onOpen: () => ctl.close()})), + dom.on('click', () => beaconOpenMessage({appModel, onOpen: () => ctl.close(), route: '/ask/message/'})), testId('feedback'), ) ), diff --git a/app/gen-server/ApiServer.ts b/app/gen-server/ApiServer.ts index 4e02f7d4..08adeab8 100644 --- a/app/gen-server/ApiServer.ts +++ b/app/gen-server/ApiServer.ts @@ -39,7 +39,7 @@ export function getOrgFromRequest(req: Request): string|null { * HelpScout the user identity for identifying customer information and conversation history. */ function helpScoutSign(email: string): string|undefined { - const secretKey = process.env.HELP_SCOUT_SECRET_KEY; + const secretKey = process.env.HELP_SCOUT_SECRET_KEY_V2; if (!secretKey) { return undefined; } return crypto.createHmac('sha256', secretKey).update(email).digest('hex'); } diff --git a/app/server/lib/sendAppPage.ts b/app/server/lib/sendAppPage.ts index b52005ba..99a0b204 100644 --- a/app/server/lib/sendAppPage.ts +++ b/app/server/lib/sendAppPage.ts @@ -41,7 +41,7 @@ export function makeGristConfig(homeUrl: string|null, extra: Partial