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
cache:
npm: false
yarn: false
yarn: true
before_script:
- 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;
@ -54,7 +54,6 @@ before_install:
- sudo apt-get -y install python3-pip python3-dev libusb-1.0-0-dev libudev-dev
- pip install awscli --upgrade --user
script:
- yarn lint:check
- yarn prettier:check
- yarn test:coverage
- 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 FormControlLabel from '@material-ui/core/FormControlLabel'
import { makeStyles } from '@material-ui/core/styles'
import cn from 'classnames'
import React, { useEffect, useState, useCallback } from 'react'
import React, { ReactElement, useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import Button from 'src/components/layout/Button'
import Link from 'src/components/layout/Link'
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 { mainFontFamily, md, primary, screenSm } from 'src/theme/variables'
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
@ -27,14 +27,13 @@ const useStyles = makeStyles({
justifyContent: 'center',
left: '0',
minHeight: '200px',
padding: '27px 15px',
padding: '30px 15px 45px',
position: 'fixed',
width: '100%',
zIndex: '15',
zIndex: '999',
},
content: {
maxWidth: '100%',
width: '830px',
},
text: {
color: primary,
@ -42,19 +41,21 @@ const useStyles = makeStyles({
fontSize: md,
fontWeight: 'normal',
lineHeight: '1.38',
margin: '0 0 25px',
margin: '0 auto 35px',
textAlign: 'center',
maxWidth: '810px',
},
form: {
columnGap: '10px',
columnGap: '20px',
display: 'grid',
gridTemplateColumns: '1fr',
paddingBottom: '30px',
rowGap: '10px',
paddingBottom: '50px',
rowGap: '15px',
margin: '0 auto',
[`@media (min-width: ${screenSm}px)`]: {
gridTemplateColumns: '1fr 1fr 1fr',
gridTemplateColumns: '1fr 1fr 1fr 1fr 1fr',
paddingBottom: '0',
rowGap: '5px',
},
},
formItem: {
@ -68,89 +69,121 @@ const useStyles = makeStyles({
textDecoration: 'none',
},
},
acceptPreferences: {
bottom: '-20px',
intercomAlert: {
fontWeight: 'bold',
display: 'flex',
justifyContent: 'center',
padding: '0 0 13px 0',
svg: {
marginRight: '5px',
},
},
intercomImage: {
position: 'fixed',
cursor: 'pointer',
position: 'absolute',
right: '20px',
textDecoration: 'underline',
[`@media (min-width: ${screenSm}px)`]: {
bottom: '-10px',
},
'&:hover': {
textDecoration: 'none',
},
height: '80px',
width: '80px',
bottom: '8px',
right: '10px',
zIndex: '1000',
boxShadow: '1px 2px 10px 0 var(rgba(40, 54, 61, 0.18))',
},
} as any)
const CookiesBanner = () => {
interface CookiesBannerFormProps {
alertMessage: boolean
}
const CookiesBanner = (): ReactElement => {
const classes = useStyles()
const dispatch = useDispatch()
const [showAnalytics, setShowAnalytics] = useState(false)
const [showIntercom, setShowIntercom] = useState(false)
const [localNecessary, setLocalNecessary] = useState(true)
const [localAnalytics, setLocalAnalytics] = useState(false)
const showBanner = useSelector(cookieBannerOpen)
const [localIntercom, setLocalIntercom] = useState(false)
const acceptCookiesHandler = useCallback(async () => {
const newState = {
acceptedNecessary: true,
acceptedAnalytics: !isDesktop,
}
await saveCookie(COOKIES_KEY, newState, 365)
dispatch(openCookieBanner(false))
setShowAnalytics(!isDesktop)
}, [dispatch])
const showBanner = useSelector(cookieBannerOpen)
useEffect(() => {
async function fetchCookiesFromStorage() {
const cookiesState = await loadFromCookie(COOKIES_KEY)
if (cookiesState) {
const { acceptedAnalytics, acceptedNecessary } = cookiesState
if (!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)
setLocalNecessary(acceptedNecessary)
const openBanner = acceptedNecessary === false || showBanner
dispatch(openCookieBanner(openBanner))
setShowAnalytics(acceptedAnalytics)
} else {
dispatch(openCookieBanner(true))
}
}
fetchCookiesFromStorage()
}, [dispatch, showBanner])
}, [showAnalytics, showIntercom])
useEffect(() => {
if (isDesktop && showBanner) acceptCookiesHandler()
}, [acceptCookiesHandler, showBanner])
const acceptCookiesHandler = async () => {
const newState = {
acceptedNecessary: true,
acceptedAnalytics: !isDesktop,
acceptedIntercom: true,
}
await saveCookie(COOKIES_KEY, newState, 365)
setShowAnalytics(!isDesktop)
setShowIntercom(true)
dispatch(openCookieBanner(false))
}
const closeCookiesBannerHandler = async () => {
const newState = {
acceptedNecessary: true,
acceptedAnalytics: localAnalytics,
acceptedIntercom: localIntercom,
}
const expDays = localAnalytics ? 365 : 7
await saveCookie(COOKIES_KEY, newState, expDays)
setShowAnalytics(localAnalytics)
setShowIntercom(localIntercom)
if (!localIntercom && isIntercomLoaded()) {
closeIntercom()
}
dispatch(openCookieBanner(false))
}
const cookieBannerContent = (
if (showAnalytics && !isDesktop) {
loadGoogleAnalytics()
}
if (showIntercom) {
loadIntercom()
}
const CookiesBannerForm = (props: CookiesBannerFormProps) => {
const { alertMessage } = props
return (
<div className={classes.container}>
<span
className={cn(classes.acceptPreferences, classes.text)}
onClick={closeCookiesBannerHandler}
onKeyDown={closeCookiesBannerHandler}
role="button"
tabIndex={0}
data-testid="accept-preferences"
>
Accept preferences &gt;
</span>
<div className={classes.content}>
{alertMessage && (
<div className={classes.intercomAlert}>
<img src={AlertRedIcon} />
You attempted to open the customer support chat. Please accept the customer support cookie.
</div>
)}
<p className={classes.text}>
We use cookies to give you the best experience and to help improve our website. Please read our{' '}
We use cookies to provide you with the best experience and to help improve our website and application.
Please read our{' '}
<Link className={classes.link} to="https://gnosis-safe.io/cookie">
Cookie Policy
</Link>{' '}
@ -169,6 +202,15 @@ const CookiesBanner = () => {
value={localNecessary}
/>
</div>
<div className={classes.formItem}>
<FormControlLabel
control={<Checkbox checked={localIntercom} />}
label="Customer support"
name="Customer support"
onChange={() => setLocalIntercom((prev) => !prev)}
value={localIntercom}
/>
</div>
<div className={classes.formItem}>
<FormControlLabel
control={<Checkbox checked={localAnalytics} />}
@ -183,24 +225,43 @@ const CookiesBanner = () => {
color="primary"
component={Link}
minWidth={180}
onClick={() => acceptCookiesHandler()}
onClick={() => closeCookiesBannerHandler()}
variant="outlined"
>
Accept All
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>
)
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

View File

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

View File

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

View File

@ -1,4 +1,5 @@
import ReactGA from 'react-ga'
import { Dispatch } from 'redux'
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 { makeProvider } from 'src/logic/wallets/store/model/provider'
import { updateStoredTransactionsStatus } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers'
import { Dispatch } from 'redux'
export const processProviderResponse = (dispatch, provider) => {
const walletRecord = makeProvider(provider)

View File

@ -1,8 +1,10 @@
import { Loader } from '@gnosis.pm/safe-react-components'
import queryString from 'query-string'
import React, { useEffect, useState } from 'react'
import ReactGA from 'react-ga'
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 { InitialValuesForm, Layout } from 'src/routes/open/components/Layout'
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 { SafeRecordProps } from 'src/logic/safe/store/models/safe'
import { addOrUpdateSafe } from 'src/logic/safe/store/actions/addOrUpdateSafe'
import { PromiEvent, TransactionReceipt } from 'web3-core'
import { useLocation } from 'react-router-dom'
import { useAnalytics } from 'src/utils/googleAnalytics'
const SAFE_PENDING_CREATION_STORAGE_KEY = 'SAFE_PENDING_CREATION_STORAGE_KEY'
@ -121,6 +122,7 @@ const Open = (): React.ReactElement => {
const userAccount = useSelector(userAccountSelector)
const dispatch = useDispatch()
const location = useLocation()
const { trackEvent } = useAnalytics()
useEffect(() => {
// #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))
ReactGA.event({
trackEvent({
category: 'User',
action: 'Created a safe',
})

View File

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