Adapt cookie banner for GDPR (#1814)

* Update cookie banner action and reducer

* Add cookie banner new assets

* Update cookie banner

* Update cookie banner migration

* Disable cookie banner and intercom in desktop app

* Use google analytics hook in Open Safe component

Co-authored-by: Daniel Sanchez <daniel.sanchez@gnosis.pm>
This commit is contained in:
Germán Martínez 2021-02-10 11:51:20 +01:00 committed by GitHub
parent 7caec9c29c
commit aba414ab79
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 209 additions and 126 deletions

View File

@ -44,7 +44,7 @@ matrix:
if: (branch = master AND NOT type = pull_request) OR tag IS present if: (branch = master AND NOT type = pull_request) OR tag IS present
cache: cache:
npm: false npm: false
yarn: false yarn: true
before_script: before_script:
- if [[ -n "$TRAVIS_TAG" ]]; then export REACT_APP_ENV='production'; fi; - if [[ -n "$TRAVIS_TAG" ]]; then export REACT_APP_ENV='production'; fi;
- if [ $TRAVIS_PULL_REQUEST != "false" ]; then export PUBLIC_URL="/${REACT_APP_NETWORK}/app"; fi; - if [ $TRAVIS_PULL_REQUEST != "false" ]; then export PUBLIC_URL="/${REACT_APP_NETWORK}/app"; fi;
@ -54,7 +54,6 @@ before_install:
- sudo apt-get -y install python3-pip python3-dev libusb-1.0-0-dev libudev-dev - sudo apt-get -y install python3-pip python3-dev libusb-1.0-0-dev libudev-dev
- pip install awscli --upgrade --user - pip install awscli --upgrade --user
script: script:
- yarn lint:check
- yarn prettier:check - yarn prettier:check
- yarn test:coverage - yarn test:coverage
- yarn build - yarn build

View File

@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<g fill="none" fill-rule="evenodd">
<g>
<g>
<path d="M0 0H16V16H0z" transform="translate(-260 -550) translate(260 550)"/>
<path fill="#F02525" d="M8 5.996c.553 0 1 .447 1 1v.998c0 .553-.447 1-1 1-.552 0-1-.447-1-1v-.998c0-.553.448-1 1-1M8 9.895c.607 0 1.101.492 1.101 1.1 0 .607-.494 1.1-1.1 1.1-.608 0-1.1-.493-1.1-1.1 0-.608.492-1.1 1.1-1.1" transform="translate(-260 -550) translate(260 550)"/>
<path fill="#F02525" d="M8 0c-.383 0-.766.193-.975.581L.133 13.373c-.396.734.138 1.624.974 1.624h13.786c.836 0 1.37-.89.974-1.624L8.974.581C8.766.193 8.384 0 8 0m0 3l5.386 9.997H2.613L8 3" transform="translate(-260 -550) translate(260 550)"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 843 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -1,10 +1,8 @@
import Checkbox from '@material-ui/core/Checkbox' import Checkbox from '@material-ui/core/Checkbox'
import FormControlLabel from '@material-ui/core/FormControlLabel' import FormControlLabel from '@material-ui/core/FormControlLabel'
import { makeStyles } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles'
import cn from 'classnames' import React, { ReactElement, useEffect, useState } from 'react'
import React, { useEffect, useState, useCallback } from 'react'
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
import Button from 'src/components/layout/Button' import Button from 'src/components/layout/Button'
import Link from 'src/components/layout/Link' import Link from 'src/components/layout/Link'
import { COOKIES_KEY } from 'src/logic/cookies/model/cookie' import { COOKIES_KEY } from 'src/logic/cookies/model/cookie'
@ -13,7 +11,9 @@ import { cookieBannerOpen } from 'src/logic/cookies/store/selectors'
import { loadFromCookie, saveCookie } from 'src/logic/cookies/utils' import { loadFromCookie, saveCookie } from 'src/logic/cookies/utils'
import { mainFontFamily, md, primary, screenSm } from 'src/theme/variables' import { mainFontFamily, md, primary, screenSm } from 'src/theme/variables'
import { loadGoogleAnalytics } from 'src/utils/googleAnalytics' import { loadGoogleAnalytics } from 'src/utils/googleAnalytics'
import { loadIntercom } from 'src/utils/intercom' import { closeIntercom, isIntercomLoaded, loadIntercom } from 'src/utils/intercom'
import AlertRedIcon from './assets/alert-red.svg'
import IntercomIcon from './assets/intercom.png'
const isDesktop = process.env.REACT_APP_BUILD_FOR_DESKTOP const isDesktop = process.env.REACT_APP_BUILD_FOR_DESKTOP
@ -27,14 +27,13 @@ const useStyles = makeStyles({
justifyContent: 'center', justifyContent: 'center',
left: '0', left: '0',
minHeight: '200px', minHeight: '200px',
padding: '27px 15px', padding: '30px 15px 45px',
position: 'fixed', position: 'fixed',
width: '100%', width: '100%',
zIndex: '15', zIndex: '999',
}, },
content: { content: {
maxWidth: '100%', maxWidth: '100%',
width: '830px',
}, },
text: { text: {
color: primary, color: primary,
@ -42,19 +41,21 @@ const useStyles = makeStyles({
fontSize: md, fontSize: md,
fontWeight: 'normal', fontWeight: 'normal',
lineHeight: '1.38', lineHeight: '1.38',
margin: '0 0 25px', margin: '0 auto 35px',
textAlign: 'center', textAlign: 'center',
maxWidth: '810px',
}, },
form: { form: {
columnGap: '10px', columnGap: '20px',
display: 'grid', display: 'grid',
gridTemplateColumns: '1fr', gridTemplateColumns: '1fr',
paddingBottom: '30px', paddingBottom: '50px',
rowGap: '10px', rowGap: '15px',
margin: '0 auto',
[`@media (min-width: ${screenSm}px)`]: { [`@media (min-width: ${screenSm}px)`]: {
gridTemplateColumns: '1fr 1fr 1fr', gridTemplateColumns: '1fr 1fr 1fr 1fr 1fr',
paddingBottom: '0', paddingBottom: '0',
rowGap: '5px',
}, },
}, },
formItem: { formItem: {
@ -68,139 +69,199 @@ const useStyles = makeStyles({
textDecoration: 'none', textDecoration: 'none',
}, },
}, },
acceptPreferences: { intercomAlert: {
bottom: '-20px', fontWeight: 'bold',
display: 'flex',
justifyContent: 'center',
padding: '0 0 13px 0',
svg: {
marginRight: '5px',
},
},
intercomImage: {
position: 'fixed',
cursor: 'pointer', cursor: 'pointer',
position: 'absolute', height: '80px',
right: '20px', width: '80px',
textDecoration: 'underline', bottom: '8px',
right: '10px',
[`@media (min-width: ${screenSm}px)`]: { zIndex: '1000',
bottom: '-10px', boxShadow: '1px 2px 10px 0 var(rgba(40, 54, 61, 0.18))',
},
'&:hover': {
textDecoration: 'none',
},
}, },
} as any) } as any)
const CookiesBanner = () => { interface CookiesBannerFormProps {
alertMessage: boolean
}
const CookiesBanner = (): ReactElement => {
const classes = useStyles() const classes = useStyles()
const dispatch = useDispatch() const dispatch = useDispatch()
const [showAnalytics, setShowAnalytics] = useState(false) const [showAnalytics, setShowAnalytics] = useState(false)
const [showIntercom, setShowIntercom] = useState(false)
const [localNecessary, setLocalNecessary] = useState(true) const [localNecessary, setLocalNecessary] = useState(true)
const [localAnalytics, setLocalAnalytics] = useState(false) const [localAnalytics, setLocalAnalytics] = useState(false)
const showBanner = useSelector(cookieBannerOpen) const [localIntercom, setLocalIntercom] = useState(false)
const acceptCookiesHandler = useCallback(async () => { const showBanner = useSelector(cookieBannerOpen)
const newState = {
acceptedNecessary: true,
acceptedAnalytics: !isDesktop,
}
await saveCookie(COOKIES_KEY, newState, 365)
dispatch(openCookieBanner(false))
setShowAnalytics(!isDesktop)
}, [dispatch])
useEffect(() => { useEffect(() => {
async function fetchCookiesFromStorage() { async function fetchCookiesFromStorage() {
const cookiesState = await loadFromCookie(COOKIES_KEY) const cookiesState = await loadFromCookie(COOKIES_KEY)
if (cookiesState) { if (!cookiesState) {
const { acceptedAnalytics, acceptedNecessary } = cookiesState dispatch(openCookieBanner(true))
} else {
const { acceptedIntercom, acceptedAnalytics, acceptedNecessary } = cookiesState
if (acceptedIntercom === undefined) {
const newState = {
acceptedNecessary,
acceptedAnalytics,
acceptedIntercom: acceptedAnalytics,
}
const expDays = acceptedAnalytics ? 365 : 7
await saveCookie(COOKIES_KEY, newState, expDays)
setLocalIntercom(newState.acceptedIntercom)
setShowIntercom(newState.acceptedIntercom)
} else {
setLocalIntercom(acceptedIntercom)
setShowIntercom(acceptedIntercom)
}
setLocalAnalytics(acceptedAnalytics) setLocalAnalytics(acceptedAnalytics)
setLocalNecessary(acceptedNecessary) setLocalNecessary(acceptedNecessary)
const openBanner = acceptedNecessary === false || showBanner
dispatch(openCookieBanner(openBanner))
setShowAnalytics(acceptedAnalytics)
} else {
dispatch(openCookieBanner(true))
} }
} }
fetchCookiesFromStorage() fetchCookiesFromStorage()
}, [dispatch, showBanner]) }, [showAnalytics, showIntercom])
useEffect(() => { const acceptCookiesHandler = async () => {
if (isDesktop && showBanner) acceptCookiesHandler() const newState = {
}, [acceptCookiesHandler, showBanner]) acceptedNecessary: true,
acceptedAnalytics: !isDesktop,
acceptedIntercom: true,
}
await saveCookie(COOKIES_KEY, newState, 365)
setShowAnalytics(!isDesktop)
setShowIntercom(true)
dispatch(openCookieBanner(false))
}
const closeCookiesBannerHandler = async () => { const closeCookiesBannerHandler = async () => {
const newState = { const newState = {
acceptedNecessary: true, acceptedNecessary: true,
acceptedAnalytics: localAnalytics, acceptedAnalytics: localAnalytics,
acceptedIntercom: localIntercom,
} }
const expDays = localAnalytics ? 365 : 7 const expDays = localAnalytics ? 365 : 7
await saveCookie(COOKIES_KEY, newState, expDays) await saveCookie(COOKIES_KEY, newState, expDays)
setShowAnalytics(localAnalytics) setShowAnalytics(localAnalytics)
setShowIntercom(localIntercom)
if (!localIntercom && isIntercomLoaded()) {
closeIntercom()
}
dispatch(openCookieBanner(false)) dispatch(openCookieBanner(false))
} }
const cookieBannerContent = ( if (showAnalytics && !isDesktop) {
<div className={classes.container}> loadGoogleAnalytics()
<span }
className={cn(classes.acceptPreferences, classes.text)}
onClick={closeCookiesBannerHandler} if (showIntercom) {
onKeyDown={closeCookiesBannerHandler} loadIntercom()
role="button" }
tabIndex={0}
data-testid="accept-preferences" const CookiesBannerForm = (props: CookiesBannerFormProps) => {
> const { alertMessage } = props
Accept preferences &gt; return (
</span> <div className={classes.container}>
<div className={classes.content}> <div className={classes.content}>
<p className={classes.text}> {alertMessage && (
We use cookies to give you the best experience and to help improve our website. Please read our{' '} <div className={classes.intercomAlert}>
<Link className={classes.link} to="https://gnosis-safe.io/cookie"> <img src={AlertRedIcon} />
Cookie Policy You attempted to open the customer support chat. Please accept the customer support cookie.
</Link>{' '} </div>
for more information. By clicking &quot;Accept all&quot;, you agree to the storing of cookies on your device )}
to enhance site navigation, analyze site usage and provide customer support. <p className={classes.text}>
</p> We use cookies to provide you with the best experience and to help improve our website and application.
<div className={classes.form}> Please read our{' '}
<div className={classes.formItem}> <Link className={classes.link} to="https://gnosis-safe.io/cookie">
<FormControlLabel Cookie Policy
checked={localNecessary} </Link>{' '}
control={<Checkbox disabled />} for more information. By clicking &quot;Accept all&quot;, you agree to the storing of cookies on your device
disabled to enhance site navigation, analyze site usage and provide customer support.
label="Necessary" </p>
name="Necessary" <div className={classes.form}>
onChange={() => setLocalNecessary((prev) => !prev)} <div className={classes.formItem}>
value={localNecessary} <FormControlLabel
/> checked={localNecessary}
</div> control={<Checkbox disabled />}
<div className={classes.formItem}> disabled
<FormControlLabel label="Necessary"
control={<Checkbox checked={localAnalytics} />} name="Necessary"
label="Analytics" onChange={() => setLocalNecessary((prev) => !prev)}
name="Analytics" value={localNecessary}
onChange={() => setLocalAnalytics((prev) => !prev)} />
value={localAnalytics} </div>
/> <div className={classes.formItem}>
</div> <FormControlLabel
<div className={classes.formItem}> control={<Checkbox checked={localIntercom} />}
<Button label="Customer support"
color="primary" name="Customer support"
component={Link} onChange={() => setLocalIntercom((prev) => !prev)}
minWidth={180} value={localIntercom}
onClick={() => acceptCookiesHandler()} />
variant="outlined" </div>
> <div className={classes.formItem}>
Accept All <FormControlLabel
</Button> control={<Checkbox checked={localAnalytics} />}
label="Analytics"
name="Analytics"
onChange={() => setLocalAnalytics((prev) => !prev)}
value={localAnalytics}
/>
</div>
<div className={classes.formItem}>
<Button
color="primary"
component={Link}
minWidth={180}
onClick={() => closeCookiesBannerHandler()}
variant="outlined"
>
Accept selection
</Button>
</div>
<div className={classes.formItem}>
<Button
color="primary"
component={Link}
minWidth={180}
onClick={() => acceptCookiesHandler()}
variant="contained"
>
Accept all
</Button>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> )
)
if (showAnalytics) {
loadIntercom()
loadGoogleAnalytics()
} }
if (isDesktop) loadIntercom()
return showBanner && !isDesktop ? cookieBannerContent : null return (
<>
{!isDesktop && !showIntercom && (
<img
className={classes.intercomImage}
src={IntercomIcon}
onClick={() => dispatch(openCookieBanner(true, true))}
/>
)}
{!isDesktop && showBanner?.cookieBannerOpen && (
<CookiesBannerForm alertMessage={showBanner?.intercomAlertDisplayed} />
)}
</>
)
} }
export default CookiesBanner export default CookiesBanner

View File

@ -2,6 +2,10 @@ import { createAction } from 'redux-actions'
export const OPEN_COOKIE_BANNER = 'OPEN_COOKIE_BANNER' export const OPEN_COOKIE_BANNER = 'OPEN_COOKIE_BANNER'
export const openCookieBanner = createAction(OPEN_COOKIE_BANNER, (cookieBannerOpen) => ({ export const openCookieBanner = createAction(
cookieBannerOpen, OPEN_COOKIE_BANNER,
})) (cookieBannerOpen, intercomAlertDisplayed = false) => ({
cookieBannerOpen,
intercomAlertDisplayed,
}),
)

View File

@ -1,17 +1,12 @@
import { Map } from 'immutable' import { Map } from 'immutable'
import { handleActions } from 'redux-actions' import { handleActions } from 'redux-actions'
import { OPEN_COOKIE_BANNER } from 'src/logic/cookies/store/actions/openCookieBanner' import { OPEN_COOKIE_BANNER } from 'src/logic/cookies/store/actions/openCookieBanner'
export const COOKIES_REDUCER_ID = 'cookies' export const COOKIES_REDUCER_ID = 'cookies'
export default handleActions( export default handleActions(
{ {
[OPEN_COOKIE_BANNER]: (state, action) => { [OPEN_COOKIE_BANNER]: (state, action) => state.set('cookieBannerOpen', action.payload),
const { cookieBannerOpen } = action.payload
return state.set('cookieBannerOpen', cookieBannerOpen)
},
}, },
Map(), Map(),
) )

View File

@ -1,4 +1,5 @@
import ReactGA from 'react-ga' import ReactGA from 'react-ga'
import { Dispatch } from 'redux'
import addProvider from './addProvider' import addProvider from './addProvider'
@ -8,7 +9,6 @@ import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackb
import { getProviderInfo, getWeb3 } from 'src/logic/wallets/getWeb3' import { getProviderInfo, getWeb3 } from 'src/logic/wallets/getWeb3'
import { makeProvider } from 'src/logic/wallets/store/model/provider' import { makeProvider } from 'src/logic/wallets/store/model/provider'
import { updateStoredTransactionsStatus } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers' import { updateStoredTransactionsStatus } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers'
import { Dispatch } from 'redux'
export const processProviderResponse = (dispatch, provider) => { export const processProviderResponse = (dispatch, provider) => {
const walletRecord = makeProvider(provider) const walletRecord = makeProvider(provider)

View File

@ -1,8 +1,10 @@
import { Loader } from '@gnosis.pm/safe-react-components' import { Loader } from '@gnosis.pm/safe-react-components'
import queryString from 'query-string' import queryString from 'query-string'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import ReactGA from 'react-ga'
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
import { useLocation } from 'react-router-dom'
import { PromiEvent, TransactionReceipt } from 'web3-core'
import { SafeDeployment } from 'src/routes/opening' import { SafeDeployment } from 'src/routes/opening'
import { InitialValuesForm, Layout } from 'src/routes/open/components/Layout' import { InitialValuesForm, Layout } from 'src/routes/open/components/Layout'
import Page from 'src/components/layout/Page' import Page from 'src/components/layout/Page'
@ -23,8 +25,7 @@ import { loadFromStorage, removeFromStorage, saveToStorage } from 'src/utils/sto
import { userAccountSelector } from 'src/logic/wallets/store/selectors' import { userAccountSelector } from 'src/logic/wallets/store/selectors'
import { SafeRecordProps } from 'src/logic/safe/store/models/safe' import { SafeRecordProps } from 'src/logic/safe/store/models/safe'
import { addOrUpdateSafe } from 'src/logic/safe/store/actions/addOrUpdateSafe' import { addOrUpdateSafe } from 'src/logic/safe/store/actions/addOrUpdateSafe'
import { PromiEvent, TransactionReceipt } from 'web3-core' import { useAnalytics } from 'src/utils/googleAnalytics'
import { useLocation } from 'react-router-dom'
const SAFE_PENDING_CREATION_STORAGE_KEY = 'SAFE_PENDING_CREATION_STORAGE_KEY' const SAFE_PENDING_CREATION_STORAGE_KEY = 'SAFE_PENDING_CREATION_STORAGE_KEY'
@ -121,6 +122,7 @@ const Open = (): React.ReactElement => {
const userAccount = useSelector(userAccountSelector) const userAccount = useSelector(userAccountSelector)
const dispatch = useDispatch() const dispatch = useDispatch()
const location = useLocation() const location = useLocation()
const { trackEvent } = useAnalytics()
useEffect(() => { useEffect(() => {
// #122: Allow to migrate an old Multisig by passing the parameters to the URL. // #122: Allow to migrate an old Multisig by passing the parameters to the URL.
@ -179,7 +181,7 @@ const Open = (): React.ReactElement => {
await dispatch(addOrUpdateSafe(safeProps)) await dispatch(addOrUpdateSafe(safeProps))
ReactGA.event({ trackEvent({
category: 'User', category: 'User',
action: 'Created a safe', action: 'Created a safe',
}) })

View File

@ -1,11 +1,15 @@
import { INTERCOM_ID } from 'src/utils/constants' import { INTERCOM_ID } from 'src/utils/constants'
let intercomLoaded = false
export const isIntercomLoaded = () => intercomLoaded
// eslint-disable-next-line consistent-return // eslint-disable-next-line consistent-return
export const loadIntercom = () => { export const loadIntercom = (): void => {
const APP_ID = INTERCOM_ID const APP_ID = INTERCOM_ID
if (!APP_ID) { if (!APP_ID) {
console.error('[Intercom] - In order to use Intercom you need to add an appID') console.error('[Intercom] - In order to use Intercom you need to add an appID')
return null return
} }
const d = document const d = document
const s = d.createElement('script') const s = d.createElement('script')
@ -20,5 +24,12 @@ export const loadIntercom = () => {
app_id: APP_ID, app_id: APP_ID,
consent: true, consent: true,
}) })
intercomLoaded = true
} }
} }
export const closeIntercom = (): void => {
if (!isIntercomLoaded()) return
intercomLoaded = false
;(window as any).Intercom('shutdown')
}