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

Add helpscout's embedded script #758

Merged
merged 53 commits into from May 16, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
270e4a0
Add react-helmet dep
AquiGorka May 2, 2019
c660f82
Beacon: add component
AquiGorka May 2, 2019
91d9b6c
App: add opt-in helpscout embed
AquiGorka May 2, 2019
ae7b9e6
IconQuestion: add component
AquiGorka May 2, 2019
00f3251
Beacon: add logo
AquiGorka May 2, 2019
7e7264a
Beacon: add styles to opt in box
AquiGorka May 2, 2019
6f32341
Beacon: add animated show/hide transition
AquiGorka May 2, 2019
0f43404
Beacon: add transition animations to toggle button
AquiGorka May 2, 2019
f5bd9cf
App: update beacon prop
AquiGorka May 3, 2019
697b870
Beacon: refactor functions and add toggling directly from local button
AquiGorka May 3, 2019
ebd708f
Beacon: improve animations and custom integration
AquiGorka May 3, 2019
48ae525
Beacon: set project id as var, fix odd behaviour of opt-in dialogue o…
AquiGorka May 7, 2019
0122445
Beacon: fix typo
sohkai May 8, 2019
0f0565f
Beacon: add Oxford comma to copy
sohkai May 8, 2019
cf17d22
Beacon: update copy
sohkai May 8, 2019
8920ff0
Refactor local storage logic from App to Beacon
AquiGorka May 8, 2019
3e7606b
BeaconHeadScripts: add component
AquiGorka May 8, 2019
d15b6d0
Beacon: move head logic into independent component
AquiGorka May 8, 2019
d6fa58e
Beacon: rename local storage key var
AquiGorka May 8, 2019
4e4ad69
Beacon: refactor css declarations to styled components
AquiGorka May 8, 2019
e1befba
Rename Beacon to HelpScoutBeacon
AquiGorka May 8, 2019
8da68d2
HelpScoutBeacon: use non github diff compare destructive apostrophe
AquiGorka May 10, 2019
39520f8
HelpScoutBeacon: remove non contextual alt from img
AquiGorka May 10, 2019
1c2813f
HelpScoutBeacon: remove useEffect and use default state directly
AquiGorka May 10, 2019
4ba4a03
HelpScoutBeacon: set dependency for useCallback
AquiGorka May 10, 2019
df29ce9
HelpScoutBeacon: rename component
AquiGorka May 10, 2019
ccc4415
HelpScoutBeacon: fix dependency on useCallback
AquiGorka May 10, 2019
883e4fa
HelpScoutBeacon: remove unnecessary rule
AquiGorka May 10, 2019
5983c96
HelpScoutBeacon: add memoization to component
AquiGorka May 10, 2019
a34f8a2
HelpScoutBeacon: fix container z-index
AquiGorka May 10, 2019
29c34a1
HelpScoutBeacon: fix aside positioning on small viewports
AquiGorka May 10, 2019
d7692de
IconQuestion: update to accept props for overriding
AquiGorka May 14, 2019
fcb1fe0
HelpScoutBeacon: update button size and positioning
AquiGorka May 14, 2019
7128538
Remove file
AquiGorka May 14, 2019
958f2b3
Add header illustration
AquiGorka May 14, 2019
95d8608
HelpScoutBeacon: update popover container styles
AquiGorka May 14, 2019
8cc3975
BeaconHeadScripts: update container position
AquiGorka May 14, 2019
815057a
HelpScoutBeacon: add mechanism to show loader while beacon inits
AquiGorka May 14, 2019
85eb567
HelpScoutBeacon: add opt-in fullscreen mobile styles
AquiGorka May 14, 2019
97bbf56
BeaconHeadScripts: update styles for mobile fullscreen
AquiGorka May 14, 2019
0e2ad83
HelpScoutBeacon: add close button to fullscreen popover
AquiGorka May 14, 2019
49bafdf
Hooks: add click outside hook
AquiGorka May 15, 2019
c7b6e62
HelpScoutBeacon: add click outside close functionality
AquiGorka May 15, 2019
ede2846
HelpScoutBeacon: remove unnecesary useCallback dependencies
AquiGorka May 15, 2019
52d8cb5
HelpScoutBeacon: move towards an onLoad handler and lift ready state …
sohkai May 16, 2019
0d29500
HelpScoutBeacon: remove close button for fullscreen
AquiGorka May 16, 2019
4a6c459
BeaconHeadScripts: recover beacon close button for fullscreen
AquiGorka May 16, 2019
b0591f9
Hooks: no need to spread deps array
AquiGorka May 16, 2019
a9d185d
HelpScoutBeacon: update constants to symbols
AquiGorka May 16, 2019
01c340e
Hooks: remove deps from useClickOutside
AquiGorka May 16, 2019
6f35e19
HelpScoutBeacon: fix useClickOutside usage
AquiGorka May 16, 2019
78c75dc
HelpScoutBeacon: refactor dep to comply with linter suggestion
AquiGorka May 16, 2019
0be4af1
HelpScoutBeacon: fix issue and animation on smaller screens
AquiGorka May 16, 2019
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
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -40,6 +40,7 @@
"react-container-dimensions": "^1.3.3",
"react-dom": "^16.8.6",
"react-dropzone": "^10.1.3",
"react-helmet": "^5.2.1",
"react-onclickout": "^2.0.8",
"react-spring": "^7.2.10",
"react-with-gesture": "^4.0.4",
Expand Down
3 changes: 3 additions & 0 deletions src/App.js
Expand Up @@ -20,6 +20,7 @@ import { ModalProvider } from './components/ModalManager/ModalManager'
import { IdentityProvider } from './components/IdentityManager/IdentityManager'
import { LocalIdentityModalProvider } from './components/LocalIdentityModal/LocalIdentityModalManager'
import LocalIdentityModal from './components/LocalIdentityModal/LocalIdentityModal'
import HelpScoutBeacon from './components/HelpScoutBeacon/HelpScoutBeacon'
import { isKnownRepo } from './repo-utils'
import {
APP_MODE_START,
Expand Down Expand Up @@ -472,6 +473,8 @@ class App extends React.Component {
walletProviderId={walletProviderId}
/>
</div>

<HelpScoutBeacon />
</ActivityProvider>
</FavoriteDaosProvider>
</LocalIdentityModalProvider>
Expand Down
67 changes: 67 additions & 0 deletions src/components/HelpScoutBeacon/BeaconHeadScripts.js
@@ -0,0 +1,67 @@
import React, { useEffect } from 'react'
import PropTypes from 'prop-types'
import { Helmet } from 'react-helmet'
import { noop, GU } from '../../utils'

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'"
Copy link
Contributor

Choose a reason for hiding this comment

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

We should probably make this an environment variable.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is not a secret, in the end it is part of your frontend if that is your concern.

Copy link
Contributor

Choose a reason for hiding this comment

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

It’s more that it’s a dynamic part, that (IMO) belong outside of the source code. For example, we should be able to change it without releasing a new version, or we might want to let people changing it for their own (e.g. in case of a fork).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do we have a mechanism for something like this? Or set up manually adding the env var to the build process and dev?

Copy link
Contributor

@bpierre bpierre May 10, 2019

Choose a reason for hiding this comment

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

We do! https://github.com/aragon/aragon/blob/master/src/local-settings.js

Maybe we could also not render it if the variable is not set?

const BEACON_INIT = "window.Beacon('init'," + HELPSCOUT_ID + ')'

const BeaconHeadScripts = React.memo(({ optedIn, onReady }) => {
useEffect(() => {
let timeout = null
if (optedIn) {
;(function isBeaconReady() {
if (window.Beacon) {
onReady()
return
}
timeout = setTimeout(isBeaconReady, 100)
})()

return () => clearTimeout(timeout)
}
}, [optedIn, onReady])

if (!optedIn) {
return null
}

return (
<Helmet>
Copy link
Contributor

@bpierre bpierre May 10, 2019

Choose a reason for hiding this comment

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

What would you think about trying to skip react-helmet and the inline scripts? This seem to work for me:

// We could use styled-components’ createGlobalStyle for CSS
const HelpscoutStyle = createGlobalStyle`
  /* … */
`

// And this could replace the two inline scripts
function initBeacon(helpscoutId) {
  if (window.Beacon) {
    return window.Beacon
  }

  window.Beacon = (method, options, data) => {
    if (window.Beacon.readyQueue) {
      window.Beacon.readyQueue.push({ method, options, data })
    }
  }
  window.Beacon.readyQueue = [{ method: 'init', options: helpscoutId }]

  const insertScript = () => {
    const script = document.createElement('script')
    script.async = true
    script.src = 'https://beacon-v2.helpscout.net'
    document.body.appendChild(script)
  }

  if (document.readyState === 'complete') {
    insertScript()
  } else {
    window.addEventListener('load', insertScript)
  }

  return window.Beacon
}

function useHelpScoutBeacon(optedIn) {
  const [beacon, setBeacon] = useState(null)

  useEffect(() => {
    if (optedIn && !beacon) {
      setBeacon(() => initBeacon(HELPSCOUT_ID))
    }

    // Here we could destroy it, but I’m not sure if HelpScout’s
    // destroy / init actions are fully sync, so better leave it always on once it’s here.
    // return () => if (beacon) beacon('destroy') 
  }, [optedIn, beacon])

  return beacon
}

// it might not make sense at this point to keep this component :D 
const BeaconHeadScripts = ({ optedIn }) => {
  useHelpScoutBeacon(optedIn)
  return optedIn ? <HelpscoutBeaconStyle /> : null
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Your suggestion is a specific implementation for HelpScout, I avoided doing so for readibility and having to maintain that part of the codebase into the future.
Keeping the inline scripts makes it easier to update if there's any need to do so and we could use the same mechanism for something else we might want to inject instead of having to do independent components for each one.

Copy link
Contributor

@bpierre bpierre May 10, 2019

Choose a reason for hiding this comment

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

Your suggestion is a specific implementation for HelpScout, I avoided doing so for readibility and having to maintain that part of the codebase into the future.

It’s doing what their inline script is doing, so I’m not too worried about that. The important thing it gives us is control over it, so we can know exactly when it is ready. But it’s true that it might be a problem in the future if they ask their users to update the init script, so I let you decide!

Keeping the inline scripts makes it easier to update if there's any need to do so and we could use the same mechanism for something else we might want to inject instead of having to do independent components for each one.

I understand, but I really hope injecting anything else is not going to be needed! Until then I consider this to be an exception to the rule, but running code that we don’t control relying on a remote HTTP server is something we should avoid as much as possible.

One thing we could still do to avoid using react-helmet (to avoid increasing the bundle size for something that we can do with little code) would be to inject the scripts ourselves:

// /!\ Untested code

function useHelpScoutBeacon(optedIn) {
  const [beacon, setBeacon] = useState(null)

  // Load the script if it doesn’t exist yet and optedIn
  // is true, then set the value of Beacon.
  useEffect(() => {
    let scripts = []

    if (optedIn && !beacon) {
      if (!window.Beacon) {
        scripts = [BEACON_EMBED, BEACON_INIT].map(source => {
          const script = document.createElement('script')
          script.innerHTML = source
          return document.body.appendChild(script)
        })
      }
      setBeacon(window.Beacon)
    }

    return () => scripts.forEach(s => s.remove())
  }, [optedIn, beacon])

  return beacon
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Until then I consider this to be an exception to the rule, but running code that we don’t control relying on a remote HTTP server is something we should avoid as much as possible.

Agree 100%, but for a feature that I understand to be "let them have it and see if they use it", let's not worry about being too optimized for now :).

Copy link
Contributor

@bpierre bpierre May 10, 2019

Choose a reason for hiding this comment

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

Yes totally, I should have mentioned here that the original intention was to avoid using react-helmet by using createGlobalStyle and document.createElement, but the other one is to explicitly re-render when Beacon is initialized: #758 (comment)

To rephrase my comment: I don’t think we need a generic mechanism to load external scripts (either react-helmet or a custom hook like useInlineScript()) for future cases: we can stay specific here, because we don’t (and shouldn’t 😆) plan to add more, and this feature might be removed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If we end up removing this feature then removing react-helmet will be easy, I originally tried with an implementation similar to your last comment @bpierre and could not get it to work.
I understand your desire to remove it, is it that strong an opinion?

Copy link
Contributor

Choose a reason for hiding this comment

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

Let’s keep it for now, but it’s definitely a a module we should try to eliminate IMO: it’s easy to replace with a simple document.createElement("script"), it has several nested dependencies, and it adds 6.4k (gzipped) to the app.

<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>
)
})

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

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

export default BeaconHeadScripts