Skip to content
This repository has been archived by the owner on Nov 10, 2023. It is now read-only.

feat: Cookies and privacy policy #3517

Merged
merged 5 commits into from
Feb 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions src/components/AppLayout/Sidebar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { wrapInSuspense } from 'src/utils/wrapInSuspense'
import ListIcon from 'src/components/List/ListIcon'
import { openCookieBanner } from 'src/logic/cookies/store/actions/openCookieBanner'
import { loadFromCookie } from 'src/logic/cookies/utils'
import { COOKIES_KEY, BannerCookiesType } from 'src/logic/cookies/model/cookie'
import { COOKIES_KEY, BannerCookiesType, COOKIE_IDS } from 'src/logic/cookies/model/cookie'

const StyledDivider = styled(Divider)`
margin: 16px -8px 0;
Expand Down Expand Up @@ -88,8 +88,13 @@ const Sidebar = ({
dispatch(openCookieBanner({ cookieBannerOpen: true }))
return
}
if (!cookiesState.acceptedIntercom) {
dispatch(openCookieBanner({ cookieBannerOpen: true, intercomAlertDisplayed: true }))
if (!cookiesState.acceptedSupportAndUpdates) {
dispatch(
openCookieBanner({
cookieBannerOpen: true,
key: COOKIE_IDS.BEAMER,
}),
)
}
}

Expand Down Expand Up @@ -124,7 +129,7 @@ const Sidebar = ({
<HelpList>
<StyledListItem id="whats-new-button" button onClick={handleClick}>
<ListIcon type="gift" />
<StyledListItemText>Whats new</StyledListItemText>
<StyledListItemText>What&apos;s new</StyledListItemText>
</StyledListItem>

<HelpCenterLink href="https://help.gnosis-safe.io/en/" target="_blank" title="Help Center of Gnosis Safe">
Expand Down
83 changes: 44 additions & 39 deletions src/components/CookiesBanner/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import { ReactElement, useEffect, useRef, 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, BannerCookiesType } from 'src/logic/cookies/model/cookie'
import { openCookieBanner } from 'src/logic/cookies/store/actions/openCookieBanner'
import { cookieBannerOpen } from 'src/logic/cookies/store/selectors'
import { COOKIES_KEY, BannerCookiesType, COOKIE_IDS, COOKIE_ALERTS } from 'src/logic/cookies/model/cookie'
import { closeCookieBanner, openCookieBanner } from 'src/logic/cookies/store/actions/openCookieBanner'
import { cookieBannerState } 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, removeCookies } from 'src/utils/googleAnalytics'
Expand Down Expand Up @@ -93,30 +93,29 @@ const useStyles = makeStyles({
},
} as any)

interface CookiesBannerFormProps {
alertMessage: boolean
}

const CookiesBanner = (): ReactElement => {
const classes = useStyles()
const dispatch = useRef(useDispatch())
const dispatch = useDispatch()
const intercomLoaded = isIntercomLoaded()

const [showAnalytics, setShowAnalytics] = useState(false)
const [showIntercom, setShowIntercom] = useState(false)
const [localNecessary, setLocalNecessary] = useState(true)
const [localAnalytics, setLocalAnalytics] = useState(false)
const [localIntercom, setLocalIntercom] = useState(false)
const [localSupportAndUpdates, setLocalSupportAndUpdates] = useState(false)
const { getAppUrl } = useSafeAppUrl()
const beamerScriptRef = useRef<HTMLScriptElement>()

const showBanner = useSelector(cookieBannerOpen)
const { key, cookieBannerOpen } = useSelector(cookieBannerState)
const newAppUrl = getAppUrl()
const isSafeAppView = newAppUrl !== null

useEffect(() => {
if (showIntercom && !isSafeAppView) {
loadIntercom()

// For use in non-webapps (Mobile and Desktop)
// https://www.getbeamer.com/help/how-to-install-beamer-using-our-api
loadBeamer(beamerScriptRef)
}
}, [showIntercom, isSafeAppView])
Expand All @@ -131,24 +130,24 @@ const CookiesBanner = (): ReactElement => {
async function fetchCookiesFromStorage() {
const cookiesState = await loadFromCookie<BannerCookiesType>(COOKIES_KEY)
if (!cookiesState) {
dispatch.current(openCookieBanner({ cookieBannerOpen: true }))
dispatch(openCookieBanner({ cookieBannerOpen: true }))
} else {
const { acceptedIntercom, acceptedAnalytics, acceptedNecessary } = cookiesState
if (acceptedIntercom === undefined) {
const { acceptedSupportAndUpdates, acceptedAnalytics, acceptedNecessary } = cookiesState
if (acceptedSupportAndUpdates === undefined) {
const newState = {
acceptedNecessary,
acceptedAnalytics,
acceptedIntercom: acceptedAnalytics,
acceptedSupportAndUpdates: acceptedAnalytics,
}
const cookieConfig: CookieAttributes = {
expires: acceptedAnalytics ? 365 : 7,
}
await saveCookie<BannerCookiesType>(COOKIES_KEY, newState, cookieConfig)
setLocalIntercom(newState.acceptedIntercom)
setShowIntercom(newState.acceptedIntercom)
setLocalSupportAndUpdates(newState.acceptedSupportAndUpdates)
setShowIntercom(newState.acceptedSupportAndUpdates)
} else {
setLocalIntercom(acceptedIntercom)
setShowIntercom(acceptedIntercom)
setLocalSupportAndUpdates(acceptedSupportAndUpdates)
setShowIntercom(acceptedSupportAndUpdates)
}
setLocalAnalytics(acceptedAnalytics)
setLocalNecessary(acceptedNecessary)
Expand All @@ -159,56 +158,57 @@ const CookiesBanner = (): ReactElement => {
}
}
fetchCookiesFromStorage()
}, [showAnalytics, showIntercom])
}, [dispatch, showAnalytics, showIntercom])

const acceptCookiesHandler = async () => {
const newState = {
acceptedNecessary: true,
acceptedAnalytics: !isDesktop,
acceptedIntercom: true,
acceptedSupportAndUpdates: true,
}
const cookieConfig: CookieAttributes = {
expires: 365,
}
await saveCookie<BannerCookiesType>(COOKIES_KEY, newState, cookieConfig)
setShowAnalytics(!isDesktop)
setShowIntercom(true)
dispatch.current(openCookieBanner({ cookieBannerOpen: false }))
dispatch(closeCookieBanner())
}

const closeCookiesBannerHandler = async () => {
const newState = {
acceptedNecessary: true,
acceptedAnalytics: localAnalytics,
acceptedIntercom: localIntercom,
acceptedSupportAndUpdates: localSupportAndUpdates,
}
const cookieConfig: CookieAttributes = {
expires: localAnalytics ? 365 : 7,
}
await saveCookie<BannerCookiesType>(COOKIES_KEY, newState, cookieConfig)
setShowAnalytics(localAnalytics)
setShowIntercom(localIntercom)
setShowIntercom(localSupportAndUpdates)

if (!localAnalytics) {
removeCookies()
}

if (!localIntercom && isIntercomLoaded()) {
closeIntercom()
if (!localSupportAndUpdates) {
closeBeamer(beamerScriptRef)
if (isIntercomLoaded()) {
closeIntercom()
}
}
dispatch.current(openCookieBanner({ cookieBannerOpen: false }))
dispatch(closeCookieBanner())
}

const CookiesBannerForm = (props: CookiesBannerFormProps) => {
const { alertMessage } = props
const CookiesBannerForm = () => {
return (
<div data-testid="cookies-banner-form" className={classes.container}>
<div className={classes.content}>
{alertMessage && (
{key && (
<div className={classes.intercomAlert}>
<img src={AlertRedIcon} />
You attempted to open the customer support chat. Please accept the customer support cookie.
{COOKIE_ALERTS[key]}
</div>
)}
<p className={classes.text}>
Expand All @@ -234,11 +234,11 @@ const CookiesBanner = (): ReactElement => {
</div>
<div className={classes.formItem}>
<FormControlLabel
control={<Checkbox checked={localIntercom} />}
label="Customer support"
name="Customer support"
onChange={() => setLocalIntercom((prev) => !prev)}
value={localIntercom}
control={<Checkbox checked={localSupportAndUpdates} />}
label="Community support & updates"
name="Community support & updates"
onChange={() => setLocalSupportAndUpdates((prev) => !prev)}
value={localSupportAndUpdates}
/>
</div>
<div className={classes.formItem}>
Expand Down Expand Up @@ -284,12 +284,17 @@ const CookiesBanner = (): ReactElement => {
<img
className={classes.intercomImage}
src={IntercomIcon}
onClick={() => dispatch.current(openCookieBanner({ cookieBannerOpen: true, intercomAlertDisplayed: true }))}
onClick={() =>
dispatch(
openCookieBanner({
cookieBannerOpen: true,
key: COOKIE_IDS.INTERCOM,
}),
)
}
/>
)}
{!isDesktop && showBanner?.cookieBannerOpen && (
<CookiesBannerForm alertMessage={showBanner?.intercomAlertDisplayed} />
)}
{!isDesktop && cookieBannerOpen && <CookiesBannerForm />}
</>
)
}
Expand Down
17 changes: 14 additions & 3 deletions src/logic/cookies/model/cookie.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
export const COOKIES_KEY = 'COOKIES'
export const COOKIES_KEY_INTERCOM = `${COOKIES_KEY}_INTERCOM`

export enum COOKIE_IDS {
INTERCOM = 'INTERCOM',
BEAMER = 'BEAMER',
}

export const COOKIE_ALERTS: Record<COOKIE_IDS, string> = {
INTERCOM: 'You attempted to open the customer support chat. Please accept the community support & updates cookies',
BEAMER: "You attempted to open the What's New section. Please accept the community support & updates cookies.",
}

export type BannerCookiesType = {
acceptedNecessary: boolean
acceptedIntercom: boolean
acceptedSupportAndUpdates: boolean
acceptedAnalytics: boolean
}
export type IntercomCookieType = {
userId: string
}
export const COOKIES_KEY = 'COOKIES'
export const COOKIES_KEY_INTERCOM = `${COOKIES_KEY}_INTERCOM`
8 changes: 6 additions & 2 deletions src/logic/cookies/store/actions/openCookieBanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import { createAction } from 'redux-actions'

import { OpenCookieBannerPayload } from 'src/logic/cookies/store/reducer/cookies'

export const OPEN_COOKIE_BANNER = 'OPEN_COOKIE_BANNER'
export enum COOKIE_ACTIONS {
OPEN_BANNER = 'cookies/openCookieBanner',
CLOSE_BANNER = 'cookies/closeCookieBanner',
}

export const openCookieBanner = createAction<OpenCookieBannerPayload>(OPEN_COOKIE_BANNER)
export const openCookieBanner = createAction<OpenCookieBannerPayload>(COOKIE_ACTIONS.OPEN_BANNER)
export const closeCookieBanner = createAction(COOKIE_ACTIONS.CLOSE_BANNER)
27 changes: 19 additions & 8 deletions src/logic/cookies/store/reducer/cookies.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,32 @@
import { Map } from 'immutable'
import { handleActions } from 'redux-actions'
import { OPEN_COOKIE_BANNER } from 'src/logic/cookies/store/actions/openCookieBanner'

import { COOKIE_IDS } from 'src/logic/cookies/model/cookie'
import { COOKIE_ACTIONS } from 'src/logic/cookies/store/actions/openCookieBanner'

export const COOKIES_REDUCER_ID = 'cookies'

type CookieState = Map<string, any>
export type CookieState = {
cookieBannerOpen: boolean
key?: COOKIE_IDS
}

const initialState: CookieState = {
cookieBannerOpen: false,
key: undefined,
}

export type OpenCookieBannerPayload = { cookieBannerOpen: boolean; intercomAlertDisplayed?: boolean }
export type OpenCookieBannerPayload = CookieState

const cookiesReducer = handleActions<CookieState, OpenCookieBannerPayload>(
{
[OPEN_COOKIE_BANNER]: (state, action) => {
const { intercomAlertDisplayed = false, cookieBannerOpen } = action.payload
return state.set('cookieBannerOpen', { intercomAlertDisplayed, cookieBannerOpen })
[COOKIE_ACTIONS.OPEN_BANNER]: (_, action) => {
return action.payload
},
[COOKIE_ACTIONS.CLOSE_BANNER]: () => {
return initialState
},
},
Map(),
initialState,
)

export default cookiesReducer
5 changes: 3 additions & 2 deletions src/logic/cookies/store/selectors/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { COOKIES_REDUCER_ID } from 'src/logic/cookies/store/reducer/cookies'
import { CookieState, COOKIES_REDUCER_ID } from 'src/logic/cookies/store/reducer/cookies'
import { AppReduxState } from 'src/store'

export const cookieBannerOpen = (state) => state[COOKIES_REDUCER_ID].get('cookieBannerOpen')
export const cookieBannerState = (state: AppReduxState): CookieState => state[COOKIES_REDUCER_ID]
4 changes: 2 additions & 2 deletions src/store/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
nftAssetReducer,
nftTokensReducer,
} from 'src/logic/collectibles/store/reducer/collectibles'
import cookiesReducer, { COOKIES_REDUCER_ID } from 'src/logic/cookies/store/reducer/cookies'
import cookiesReducer, { CookieState, COOKIES_REDUCER_ID } from 'src/logic/cookies/store/reducer/cookies'
import currentSessionReducer, {
CurrentSessionState,
CURRENT_SESSION_REDUCER_ID,
Expand Down Expand Up @@ -112,7 +112,7 @@ export type AppReduxState = CombinedState<{
[PENDING_TRANSACTIONS_ID]: PendingTransactionsState
[NOTIFICATIONS_REDUCER_ID]: Map<string, Notification>
[CURRENCY_REDUCER_ID]: CurrencyValuesState
[COOKIES_REDUCER_ID]: Map<string, any>
[COOKIES_REDUCER_ID]: CookieState
[ADDRESS_BOOK_REDUCER_ID]: AddressBookState
[CURRENT_SESSION_REDUCER_ID]: CurrentSessionState
[CONFIG_REDUCER_ID]: ConfigState
Expand Down