Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] HelpScout enhancements #830

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
97 changes: 51 additions & 46 deletions src/components/HelpScoutBeacon/BeaconHeadScripts.js
Original file line number Diff line number Diff line change
@@ -1,67 +1,72 @@
import React, { useEffect } from 'react'
import React, { useCallback, useEffect, useState } from 'react'
import PropTypes from 'prop-types'
import { Helmet } from 'react-helmet'
import { noop, GU } from '../../utils'
import { GU } from '../../utils'
import { createGlobalStyle } from 'styled-components'

const BEACON_EMBED =
'!function(e,t,n){function a(){var e=t.getElementsByTagName("script")[0],n=t.createElement("script");n.type="text/javascript",n.async=!0,n.src="https://beacon-v2.helpscout.net",e.parentNode.insertBefore(n,e)}if(e.Beacon=n=function(t,n,a){e.Beacon.readyQueue.push({method:t,options:n,data:a})},n.readyQueue=[],"complete"===t.readyState)return a();e.attachEvent?e.attachEvent("onload",a):e.addEventListener("load",a,!1)}(window,document,window.Beacon||function(){});'
const HELPSCOUT_ID = "'163e0284-762b-4e2d-b3b3-70a73a7e6c9f'"
const BEACON_INIT = "window.Beacon('init'," + HELPSCOUT_ID + ')'
const HELPSCOUT_ID = '163e0284-762b-4e2d-b3b3-70a73a7e6c9f'

const BeaconHeadScripts = React.memo(({ optedIn, onReady }) => {
function useHelpScoutBeacon(optedIn) {
const [beaconInit, setBeaconInit] = useState(false)

// Load the script if it doesn’t exist yet and optedIn
// is true, then set the value of Beacon.
useEffect(() => {
let timeout = null
if (optedIn) {
;(function isBeaconReady() {
if (window.Beacon) {
onReady()
return
}
timeout = setTimeout(isBeaconReady, 100)
})()
let script

return () => clearTimeout(timeout)
if (optedIn && !beaconInit) {
if (!window.Beacon) {
script = document.createElement('script')
script.innerHTML = BEACON_EMBED
document.body.appendChild(script)
}
window.Beacon('init', HELPSCOUT_ID)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Super how this works!
I think this 2 actions (init and setBeaconInit) should be triggered in the onload method from script no?
Maybe attach the script to the head instead to the body?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On another note, by removing the beaconReady prop the loader is no longer shown while the script is loading, you can test this by throttling the network speed in chrome, let's figure a way to show the LoadingRing to avoid the modal from disappearing and reappearing.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this 2 actions (init and setBeaconInit) should be triggered in the onload method from script no?

Beacon.init() gets queued and it’s probably a good idea to keep it first, as the script will probably refuse to execute the other methods if they are called before.

beaconInit is used to know that the script is being initialized, so that we don’t inject the script twice.

setBeaconInit(true)
}
}, [optedIn, onReady])

if (!optedIn) {
return null
}
return () => script.remove()
}, [optedIn, beaconInit])

return (
<Helmet>
<script type="text/javascript">{BEACON_EMBED}</script>
<script type="text/javascript">{BEACON_INIT}</script>
<style>
{`
.BeaconFabButtonFrame,
#beacon-container .Beacon div:first-of-type {
display: none !important;
}
@media (min-width: 768px) {
#beacon-container .BeaconContainer {
height: 600px !important;
width: 350px !important;
top: unset !important;
left: unset !important;
bottom: 80px !important;
right: ${3 * GU}px !important;
}
}
`}
</style>
</Helmet>
const beacon = useCallback(
(...params) => {
if (window.Beacon && optedIn && beaconInit) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens when this function is called but window.Beacon does not exist is the calls gets lost, should we be queueing them? or at least sending them to the future via a setTimeout?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to check again, but IIRC this window.Beacon in the condition is only here as a safety measure because we don’t control what the HelpScout script does with the object.

The window.Beacon object should exist as soon as optedIn changes to be true, and BEACON_EMBED is doing exactly this:

  1. Creating the window.Beacon function.
  2. Queuing calls to window.Beacon.
  3. Injecting the script (that uses the queue).

As a note, the reason why it there is a wrapping function here (instead of exposing window.Beacon directly) is because HelpScout replaces it in a totally unpredictable way.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The window.Beacon object should exist as soon as optedIn

This is not in synch right? So, maybe there is some delay before the window.Beacon object exists.

window.Beacon(...params)
}
},
[optedIn, beaconInit]
)
})

return beacon
}

const BeaconHeadScripts = ({ optedIn }) => {
return optedIn ? <HelpscoutStyle /> : null
}

BeaconHeadScripts.propTypes = {
optedIn: PropTypes.bool,
onReady: PropTypes.func,
}

BeaconHeadScripts.defaultProps = {
optedIn: false,
onReady: noop,
}

const HelpscoutStyle = createGlobalStyle`
.BeaconFabButtonFrame,
#beacon-container .Beacon div:first-of-type {
display: none !important;
}
@media (min-width: 768px) {
#beacon-container .BeaconContainer {
height: 600px !important;
width: 350px !important;
top: unset !important;
left: unset !important;
bottom: 80px !important;
right: ${3 * GU}px !important;
}
}
`
export { useHelpScoutBeacon }
export default BeaconHeadScripts
58 changes: 22 additions & 36 deletions src/components/HelpScoutBeacon/HelpScoutBeacon.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
useViewport,
} from '@aragon/ui'
import useBeaconSuggestions from './useBeaconSuggestions'
import BeaconHeadScripts from './BeaconHeadScripts'
import BeaconHeadScripts, { useHelpScoutBeacon } from './BeaconHeadScripts'
import IconQuestion from './IconQuestion'
import headerImg from './header.png'
import { useClickOutside } from '../../hooks'
Expand All @@ -29,30 +29,26 @@ const CLOSING = Symbol('closing')
const ROUND_BUTTON_HEIGHT = 40

const Beacon = React.memo(function Beacon({ locator, apps }) {
const [beaconReady, setBeaconReady] = useState(false)
const [openOnReady, setOpenOnReady] = useState(false)
const [optedIn, setOptedIn] = useState(
localStorage.getItem(HELPSCOUT_BEACON_KEY) === '1'
)

const beacon = useHelpScoutBeacon(optedIn)
const handleOptIn = () => {
localStorage.setItem(HELPSCOUT_BEACON_KEY, '1')
setOptedIn(true)
setOpenOnReady(true)
}
const handleBeaconReady = useCallback(() => {
if (openOnReady && window.Beacon) {
window.Beacon('open')
window.Beacon('once', 'open', () => {
setBeaconReady(true)
useEffect(() => {
if (openOnReady) {
beacon('open')
beacon('once', 'open', () => {
setOpenOnReady(false)
})
} else {
setBeaconReady(true)
}
}, [openOnReady])
}, [openOnReady, beacon])

useBeaconSuggestions({ apps, locator, optedIn, beaconReady })
useBeaconSuggestions({ apps, locator, optedIn, beacon })

return (
<div
Expand All @@ -72,12 +68,8 @@ const Beacon = React.memo(function Beacon({ locator, apps }) {
)}
`}
>
<BeaconHeadScripts optedIn={optedIn} onReady={handleBeaconReady} />
<HelpOptIn
beaconReady={beaconReady}
onOptIn={handleOptIn}
optedIn={optedIn}
/>
<BeaconHeadScripts optedIn={optedIn} />
<HelpOptIn beacon={beacon} onOptIn={handleOptIn} optedIn={optedIn} />
</div>
)
})
Expand All @@ -87,24 +79,18 @@ Beacon.propTypes = {
locator: PropTypes.object,
}

const HelpOptIn = React.memo(function HelpOptIn({
beaconReady,
onOptIn,
optedIn,
}) {
const HelpOptIn = React.memo(function HelpOptIn({ beacon, onOptIn, optedIn }) {
const { above } = useViewport()
const expandedMode = above('medium')
const [mode, setMode] = useState(CLOSED)

const handleClose = React.useCallback(() => setMode(CLOSED), [])
const handleClose = useCallback(() => setMode(CLOSED), [])
const handleToggle = useCallback(() => {
if (mode !== OPENING && mode !== CLOSING) {
setMode(mode === CLOSED ? OPENING : CLOSING)
}
if (beaconReady && window.Beacon) {
window.Beacon('toggle')
}
}, [beaconReady, mode])
beacon('toggle')
}, [beacon, mode])
const handleToggleEnd = useCallback(() => {
setMode(mode === OPENING ? OPENED : CLOSED)
}, [mode])
Expand All @@ -116,15 +102,16 @@ const HelpOptIn = React.memo(function HelpOptIn({
const { ref } = useClickOutside(handleClickOutside)

useEffect(() => {
if (beaconReady && window.Beacon) {
window.Beacon('on', 'open', () => setMode(OPENED))
window.Beacon('on', 'close', () => setMode(CLOSED))
}
}, [beaconReady])
beacon('on', 'open', () => setMode(OPENED))
beacon('on', 'close', () => {
console.log('BEACON:close')
setMode(CLOSED)
})
}, [beacon])

return (
<div ref={ref}>
{(!optedIn || !beaconReady) && (
{!optedIn && (
<Transition
native
items={mode === OPENING || mode === OPENED}
Expand Down Expand Up @@ -168,7 +155,7 @@ const HelpOptIn = React.memo(function HelpOptIn({
})

HelpOptIn.propTypes = {
beaconReady: PropTypes.bool.isRequired,
beacon: PropTypes.func,
onOptIn: PropTypes.func.isRequired,
optedIn: PropTypes.bool.isRequired,
}
Expand Down Expand Up @@ -393,7 +380,6 @@ const Wrapper = styled.aside`
width: 336px;
height: 482px;
position: unset;
border: 1px solid rgba(209, 209, 209, 0.5);
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.15);
border-radius: 3px;
`
Expand Down
27 changes: 8 additions & 19 deletions src/components/HelpScoutBeacon/IconQuestion.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,12 @@
import React from 'react'

const IconQuestion = props => {
return (
<svg
width="15"
height="26"
viewBox="0 0 15 26"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M9.00619 25.2001H5.8689V22.1497H9.00619V25.2001ZM9.55511 15.516L9.28063 15.7448C9.12349 15.8596 9.00615 16.1264 9.00615 16.3553V19.1381H5.86885V16.3553C5.86885 15.173 6.41781 14.0675 7.32016 13.3429L7.59464 13.1141C10.3394 10.9791 11.672 9.87356 11.672 7.96741C11.6766 6.87409 11.2319 5.82427 10.4368 5.05118C9.6417 4.27808 8.56196 3.84573 7.4375 3.85019C5.0063 3.85019 3.20297 5.60356 3.20297 7.96741H0.0656738C0.0656738 3.96428 3.32031 0.79981 7.4375 0.79981C9.39335 0.797505 11.2698 1.55192 12.6528 2.8966C14.0358 4.24129 14.8117 6.06573 14.8093 7.96741C14.8093 11.4368 12.3781 13.3429 9.55511 15.516Z"
fill="white"
/>
</svg>
)
}
const IconQuestion = props => (
<svg width={15} height={26} fill="none" viewBox="0 0 15 26" {...props}>
<path
d="M9.006 25.2H5.87v-3.05h3.137v3.05zm.55-9.684l-.275.229c-.158.115-.275.381-.275.61v2.783H5.87v-2.783c0-1.182.549-2.287 1.451-3.012l.275-.229c2.744-2.135 4.077-3.24 4.077-5.147a4.043 4.043 0 0 0-1.235-2.916 4.28 4.28 0 0 0-3-1.2c-2.43 0-4.234 1.753-4.234 4.116H.066C.066 3.964 3.32.8 7.438.8a7.471 7.471 0 0 1 5.215 2.097 7.06 7.06 0 0 1 2.156 5.07c0 3.47-2.43 5.376-5.254 7.549z"
fill="#fff"
/>
</svg>
)

export default IconQuestion
12 changes: 6 additions & 6 deletions src/components/HelpScoutBeacon/useBeaconSuggestions.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const sectionToSuggestions = new Map(suggestions)

function useBeaconSuggestions({
apps,
beaconReady,
beacon,
locator: { instanceId, path },
optedIn,
}) {
Expand All @@ -30,15 +30,15 @@ function useBeaconSuggestions({
if (!shouldSuggest) {
return
}
window.Beacon('suggest', sectionToSuggestions.get(getSection()))
}, [getSection, shouldSuggest])
beacon('suggest', sectionToSuggestions.get(getSection()))
}, [getSection, shouldSuggest, beacon])

useEffect(() => {
if (!optedIn || !beaconReady) {
if (!optedIn) {
return
}
// this only happens when user opts in
// when opting in beaconReady is set after the open event has been triggered
// when opting in, beaconReady is set after the open event has been triggered
// give it a minute before suggesting or a weird reace condition happens
let timeout
if (!originalOptedIn) {
Expand All @@ -47,7 +47,7 @@ function useBeaconSuggestions({
}
setShouldSuggest(true)
return () => clearTimeout(timeout)
}, [optedIn, beaconReady, originalOptedIn])
}, [optedIn, originalOptedIn])
}

export default useBeaconSuggestions