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

Feat/export wins review cookie page #7008

Merged
merged 11 commits into from
Aug 9, 2024
Merged
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@
"http-server": "^14.1.1",
"image-minimizer-webpack-plugin": "^4.1.0",
"js-beautify": "^1.14.11",
"js-cookie": "^3.0.5",
"jsdom": "^20.0.3",
"jsdom-global": "^3.0.2",
"json-schema-faker": "^0.5.5",
Expand Down
1 change: 1 addition & 0 deletions src/apps/__export-wins-review/view.njk
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
padding: 0;
}
</style>
{% include "_includes/google-tag-manager-snippet.njk" %}
</head>
<body>
{% include "_includes/csp-nonce.njk" %}
Expand Down
28 changes: 28 additions & 0 deletions src/client/components/Task/RecentResult.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { TASK__START } from '../../actions'
import multiinstance from '../../utils/multiinstance'

export default multiinstance({
name: 'Task/RecentResult',
actionPattern: /.*/,
idProp: 'name',
reducer: (state, { type, id, onSuccessDispatch, result }) => {
switch (type) {
case TASK__START:
return {
...state,
[id]: {
...state?.[id],
successActionType: onSuccessDispatch,
},
}
case state?.[id]?.successActionType:
return {
...state,
[id]: { result },
}
default:
return state
}
},
component: ({ children, id, ...props }) => children(props[id]?.result),
})
11 changes: 10 additions & 1 deletion src/client/export-win-review.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import './webpack-csp-nonce'
import React from 'react'
import { createRoot } from 'react-dom/client'
import { Routes, Route } from 'react-router-dom'

import { createProvider } from './createProvider'
import WithoutOurSupport from './components/Resource/WithoutOurSupport'
Expand All @@ -11,6 +12,10 @@ import MarketingSource from './components/Resource/MarketingSource'

import Review from './modules/ExportWins/Review'
import { patchExportWinReview } from './modules/ExportWins/tasks'
import {
loadCookiePreference,
saveCookiePreference,
} from './modules/ExportWins/Review/CookiePage/tasks'

const Provider = createProvider({
...ExportWinReview.tasks,
Expand All @@ -19,12 +24,16 @@ const Provider = createProvider({
...Experience.tasks,
...MarketingSource.tasks,
TASK_PATCH_EXPORT_WIN_REVIEW: patchExportWinReview,
'load cookie preference': loadCookiePreference,
'save cookie preference': saveCookiePreference,
})

window.addEventListener('DOMContentLoaded', () => {
createRoot(document.getElementById('react-app')).render(
<Provider>
<Review />
<Routes>
<Route path="/exportwins/review*" element={<Review />} />
</Routes>
</Provider>
)
})
55 changes: 55 additions & 0 deletions src/client/modules/ExportWins/Review/CookiePage/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React from 'react'
import { H2 } from 'govuk-react'

import { FieldRadios, Form } from '../../../../components'
import Layout from '../Layout'

const CookiePage = () => (
<Layout title="Export Wins cookie policy">
<H2>How cookies are used in Export Wins</H2>
<p>Export wins puts small files (known as 'cookies') onto your computer.</p>
<p>
Cookies are used to measure how to use the website so it can be updated
and improved based on your needs.
</p>
<p>
Export Wins cookies never contain personally identifiable information.
</p>
<H2>Analytics cookies</H2>
<p>
We use Google Analytics software to collect information about how you use
Export Wins. We do this to help make sure the site is meeting the needs of
its users and to help us make improvements. Google Analytics stores
information about:
</p>
<ul>
<li>the pages you visit</li>
<li>how long you spend on each page</li>
<li>how you got to the site</li>
<li>what you click on while you're visiting the site</li>
</ul>
<p>We don't allow Google to use or share our analytics data.</p>
<Form
analyticsFormName="cookie-page-form"
id="cookieConsent"
submissionTaskName="save cookie preference"
initialValuesTaskName="load cookie preference"
submissionTaskResultToValues={(cookieConsent) => ({ cookieConsent })}
transformInitialValues={(cookieConsent) => ({ cookieConsent })}
transformPayload={({ cookieConsent }) => cookieConsent}
submitButtonLabel="Save cookie settings"
>
<FieldRadios
label="Do you want to accept analytics cookies?"
name="cookieConsent"
required="Choose one option"
options={[
{ label: 'Yes', value: 'granted' },
{ label: 'No', value: 'denied' },
]}
/>
</Form>
</Layout>
)

export default CookiePage
26 changes: 26 additions & 0 deletions src/client/modules/ExportWins/Review/CookiePage/saga.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { put, take } from 'redux-saga/effects'
import { eventChannel } from 'redux-saga'

const storageChannel = eventChannel((emit) => {
window.addEventListener('storage', emit)
return () => window.removeEventListener(emit)
})

/**
* This ensures that when the user sets their cookie prefference
paulgain marked this conversation as resolved.
Show resolved Hide resolved
* in one browser tab, all the other tabs will pick up the change.
*/
// TODO: Once Redux state is persisted in session storage, this should not be needed
export function* cookiePreferenceChangeSaga() {
while (true) {
const { key, newValue } = yield take(storageChannel)
if (key === 'cookie-consent') {
yield put({
type: 'RESOURCE',
name: 'load cookie preference',
id: 'cookieConsent',
result: newValue,
})
}
}
}
33 changes: 33 additions & 0 deletions src/client/modules/ExportWins/Review/CookiePage/tasks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import Cookies from 'js-cookie'

export const COOKIE_CONSENT_COOKIE_NAME = 'cookie-consent'
export const GRANTED = 'granted'
export const DENIED = 'denied'

export const loadCookiePreference = () =>
localStorage.getItem(COOKIE_CONSENT_COOKIE_NAME)

export const saveCookiePreference = (payload) => {
if (!window.gtag) {
throw Error(
'window.gtag not defined, you probably forgot to set the GOOGLE_TAG_MANAGER_KEY env var.'
)
}
if (![GRANTED, DENIED].includes(payload)) {
throw Error('Payload must be "granted" or "denied"')
}

localStorage.setItem(COOKIE_CONSENT_COOKIE_NAME, payload)

window.gtag('consent', 'update', {
analytics_storage: payload,
})

if (payload === DENIED) {
for (const cookieName in Cookies.get()) {
Cookies.remove(cookieName)
}
}

return payload
}
86 changes: 83 additions & 3 deletions src/client/modules/ExportWins/Review/Layout.jsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
import React from 'react'
import styled from 'styled-components'
import { H1 } from 'govuk-react'
import { Button, H1, H2, Link } from 'govuk-react'
import { FONT_SIZE, FONT_WEIGHTS, SPACING } from '@govuk-react/constants'

import RecentTaskResult from '../../../components/Task/RecentResult'
import { StyledStatusMessage } from './ThankYou'
import { FormActions } from '../../../components'
import Footer from '../../../components/Footer'
import { BLACK, WHITE, LIGHT_GREY } from '../../../utils/colours'
import Task from '../../../components/Task'
import Resource from '../../../components/Resource/Resource'
import { BLACK, WHITE, LIGHT_GREY, GREEN } from '../../../utils/colours'

const Grid = styled.div({
minHeight: '100vh',
display: 'grid',
gridTemplateRows: 'auto auto 1fr minmax(min-content, 30px)',
gridTemplateRows: 'auto auto auto 1fr minmax(min-content, 30px)',
gridTemplateColumns: `1fr min(100vw, calc(960px + ${SPACING.SCALE_3} * 2)) 1fr`,
gridTemplateAreas: `
". cookie-banner ."
". main-bar ."
". header ."
". main ."
Expand Down Expand Up @@ -62,14 +68,87 @@ const Title = styled(H1)({
marginBottom: SPACING.SCALE_5,
})

const CookieBannerBackground = styled.div({
gridRow: 'cookie-banner',
gridColumn: '1 / -1',
background: '#f3f2f1',
})

const CookieBanner = styled.div({
gridArea: 'cookie-banner',
paddingTop: SPACING.SCALE_3,
})

const CookieConsentConfirmation = () => (
<RecentTaskResult name="save cookie preference" id="cookieConsent">
{(consent) =>
consent && (
<StyledStatusMessage colour={GREEN}>
You've {consent === 'granted' ? 'accepted' : 'rejected'} additional
cookies. You can change your cookie settings at any time.
</StyledStatusMessage>
)
}
</RecentTaskResult>
)

const Layout = ({ children, title, supertitle, headingContent }) => (
<Grid>
<CookieBannerBackground />
<Resource name="load cookie preference" id="cookieConsent">
{(persistedConsent) => (
<RecentTaskResult name="save cookie preference" id="cookieConsent">
{(updatedConsent) =>
!persistedConsent &&
!updatedConsent && (
<CookieBanner>
<H2>Cookies</H2>
<p>
We’d like to use analytics cookies so we can understand how
you use the Design System and make improvements.
paulgain marked this conversation as resolved.
Show resolved Hide resolved
</p>
<p>
We also use essential cookies to remember if you’ve accepted
analytics cookies.
</p>
<FormActions>
<Task>
{(getTask) => {
const setPreference = (value) =>
getTask(
'save cookie preference',
'cookieConsent'
).start({
payload: value,
onSuccessDispatch: 'FORM__RESOLVED',
})
return (
<>
<Button onClick={() => setPreference('granted')}>
Accept analytics cookies
</Button>
<Button onClick={() => setPreference('denied')}>
Reject analytics cookies
</Button>
</>
)
}}
</Task>
<Link href="/exportwins/review/cookies">View cookies</Link>
</FormActions>
</CookieBanner>
)
}
</RecentTaskResult>
)}
</Resource>
<MainBarBackground />
<MainBar>Department for Business & Trade</MainBar>
paulgain marked this conversation as resolved.
Show resolved Hide resolved
<HeaderBackground />
<Header>
<p>{supertitle}</p>
<Title>{title}</Title>
<CookieConsentConfirmation />
{headingContent}
</Header>
<Main>{children}</Main>
Expand All @@ -78,6 +157,7 @@ const Layout = ({ children, title, supertitle, headingContent }) => (
'Privacy Policy':
'https://www.great.gov.uk/privacy-and-cookies/full-privacy-notice/',
'Accessibility Statement': '/exportwins/review/accesibility-statement',
Cookies: '/exportwins/review/cookies',
}}
/>
</Grid>
Expand Down
2 changes: 1 addition & 1 deletion src/client/modules/ExportWins/Review/ThankYou.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { StatusMessage } from '../../../components'

import Layout from './Layout'

const StyledStatusMessage = styled(StatusMessage)({
export const StyledStatusMessage = styled(StatusMessage)({
background: WHITE,
'& > *:first-child': {
marginTop: 0,
Expand Down
13 changes: 6 additions & 7 deletions src/client/modules/ExportWins/Review/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import styled from 'styled-components'
import HR from '../../../components/HR'

import ThankYou from './ThankYou'
import CookiePage from './CookiePage'

import Layout from './Layout'
import {
Expand Down Expand Up @@ -375,7 +376,7 @@ const Review = () => {
submissionTaskName="TASK_PATCH_EXPORT_WIN_REVIEW"
redirectMode="soft"
redirectTo={(_, { agree_with_win }) =>
`/exportwins/review-win/thankyou?agree=${agree_with_win}`
`../thankyou?agree=${agree_with_win}`
}
transformPayload={transformPayload(token)}
>
Expand All @@ -398,11 +399,9 @@ const Review = () => {

export default () => (
<Routes>
<Route
path="/exportwins/review/accesibility-statement"
element={<AccesibilityStatement />}
/>
<Route path="/exportwins/review/:token" element={<Review />} />
<Route path="/exportwins/review-win/thankyou" element={<ThankYou />} />
<Route path="/accesibility-statement" element={<AccesibilityStatement />} />
<Route path="/thankyou" element={<ThankYou />} />
<Route path="/cookies" element={<CookiePage />} />
<Route path="/:token" element={<Review />} />
</Routes>
)
3 changes: 3 additions & 0 deletions src/client/reducers.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,8 @@ import companyActivityReducerNoAs from './modules/Companies/CompanyActivity/redu

import { ResendExportWin } from './modules/ExportWins/Form/ResendExportWin'

import RecentTaskResult from './components/Task/RecentResult'

export const reducers = {
tasks,
[FLASH_MESSAGE_ID]: flashMessageReducer,
Expand All @@ -211,6 +213,7 @@ export const reducers = {
...Form.reducerSpread,
...FieldAddAnother.reducerSpread,
...ResendExportWin.reducerSpread,
...RecentTaskResult.reducerSpread,
[DNB_CHECK_ID]: dnbCheckReducer,
[INVESTMENT_OPPORTUNITIES_LIST_ID]: investmentOpportunitiesListReducer,
[INVESTMENT_OPPORTUNITIES_DETAILS_ID]: investmentOpportunitiesDetailsReducer,
Expand Down
Loading
Loading