diff --git a/package-lock.json b/package-lock.json index c8bec853d22..f2eb5ed13f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -136,6 +136,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", diff --git a/package.json b/package.json index 500f3628047..177644b2189 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/apps/__export-wins-review/view.njk b/src/apps/__export-wins-review/view.njk index 1375fe88064..33d71549d04 100644 --- a/src/apps/__export-wins-review/view.njk +++ b/src/apps/__export-wins-review/view.njk @@ -14,6 +14,7 @@ padding: 0; } + {% include "_includes/google-tag-manager-snippet.njk" %} {% include "_includes/csp-nonce.njk" %} diff --git a/src/client/components/Task/RecentResult.js b/src/client/components/Task/RecentResult.js new file mode 100644 index 00000000000..20ee811ecb7 --- /dev/null +++ b/src/client/components/Task/RecentResult.js @@ -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), +}) diff --git a/src/client/export-win-review.jsx b/src/client/export-win-review.jsx index e9ed9d19003..69f15b44a91 100644 --- a/src/client/export-win-review.jsx +++ b/src/client/export-win-review.jsx @@ -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' @@ -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, @@ -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( - + + } /> + ) }) diff --git a/src/client/modules/ExportWins/Review/CookiePage/index.jsx b/src/client/modules/ExportWins/Review/CookiePage/index.jsx new file mode 100644 index 00000000000..2edac4bf55b --- /dev/null +++ b/src/client/modules/ExportWins/Review/CookiePage/index.jsx @@ -0,0 +1,55 @@ +import React from 'react' +import { H2 } from 'govuk-react' + +import { FieldRadios, Form } from '../../../../components' +import Layout from '../Layout' + +const CookiePage = () => ( + +

How cookies are used in Export Wins

+

Export wins puts small files (known as 'cookies') onto your computer.

+

+ Cookies are used to measure how to use the website so it can be updated + and improved based on your needs. +

+

+ Export Wins cookies never contain personally identifiable information. +

+

Analytics cookies

+

+ 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: +

+ +

We don't allow Google to use or share our analytics data.

+
({ cookieConsent })} + transformInitialValues={(cookieConsent) => ({ cookieConsent })} + transformPayload={({ cookieConsent }) => cookieConsent} + submitButtonLabel="Save cookie settings" + > + + +
+) + +export default CookiePage diff --git a/src/client/modules/ExportWins/Review/CookiePage/saga.js b/src/client/modules/ExportWins/Review/CookiePage/saga.js new file mode 100644 index 00000000000..6ea9c9017da --- /dev/null +++ b/src/client/modules/ExportWins/Review/CookiePage/saga.js @@ -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 preference + * 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, + }) + } + } +} diff --git a/src/client/modules/ExportWins/Review/CookiePage/tasks.js b/src/client/modules/ExportWins/Review/CookiePage/tasks.js new file mode 100644 index 00000000000..e5d9c6b8f07 --- /dev/null +++ b/src/client/modules/ExportWins/Review/CookiePage/tasks.js @@ -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 +} diff --git a/src/client/modules/ExportWins/Review/Layout.jsx b/src/client/modules/ExportWins/Review/Layout.jsx index 3f2bd981c96..04712017aa7 100644 --- a/src/client/modules/ExportWins/Review/Layout.jsx +++ b/src/client/modules/ExportWins/Review/Layout.jsx @@ -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 ." @@ -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 = () => ( + + {(consent) => + consent && ( + + You've {consent === 'granted' ? 'accepted' : 'rejected'} additional + cookies. You can change your cookie settings at any time. + + ) + } + +) + const Layout = ({ children, title, supertitle, headingContent }) => ( + + + {(persistedConsent) => ( + + {(updatedConsent) => + !persistedConsent && + !updatedConsent && ( + +

Cookies

+

+ We’d like to use analytics cookies so we can understand how + you use Export Wins and make improvements. +

+

+ We also use essential cookies to remember if you’ve accepted + analytics cookies. +

+ + + {(getTask) => { + const setPreference = (value) => + getTask( + 'save cookie preference', + 'cookieConsent' + ).start({ + payload: value, + onSuccessDispatch: 'FORM__RESOLVED', + }) + return ( + <> + + + + ) + }} + + View cookies + +
+ ) + } +
+ )} +
- Department for Business & Trade + Department for Business and Trade

{supertitle}

{title} + {headingContent}
{children}
@@ -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', }} />
diff --git a/src/client/modules/ExportWins/Review/ThankYou.jsx b/src/client/modules/ExportWins/Review/ThankYou.jsx index b736b59d01c..4977375d5bb 100644 --- a/src/client/modules/ExportWins/Review/ThankYou.jsx +++ b/src/client/modules/ExportWins/Review/ThankYou.jsx @@ -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, diff --git a/src/client/modules/ExportWins/Review/index.jsx b/src/client/modules/ExportWins/Review/index.jsx index 91657749c68..68e0001941b 100644 --- a/src/client/modules/ExportWins/Review/index.jsx +++ b/src/client/modules/ExportWins/Review/index.jsx @@ -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 { @@ -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)} > @@ -398,11 +399,9 @@ const Review = () => { export default () => ( - } - /> - } /> - } /> + } /> + } /> + } /> + } /> ) diff --git a/src/client/reducers.js b/src/client/reducers.js index 3b85e2eb1fa..b5e53b489d0 100644 --- a/src/client/reducers.js +++ b/src/client/reducers.js @@ -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, @@ -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, diff --git a/src/client/root-saga.js b/src/client/root-saga.js index 56629acceac..3af3afc7c59 100644 --- a/src/client/root-saga.js +++ b/src/client/root-saga.js @@ -13,6 +13,7 @@ import { writeMyInvestmentsToSession, readMyInvestmentsFromSession, } from './components/MyInvestmentProjects/sagas' +import { cookiePreferenceChangeSaga } from './modules/ExportWins/Review/CookiePage/saga' export default (tasks) => { return function* rootSaga() { @@ -25,5 +26,6 @@ export default (tasks) => { yield fork(readAnnouncementLinkFromLocalStorage) yield fork(readMyInvestmentsFromSession) yield fork(writeMyInvestmentsToSession) + yield fork(cookiePreferenceChangeSaga) } } diff --git a/src/templates/_includes/google-tag-manager-snippet.njk b/src/templates/_includes/google-tag-manager-snippet.njk index f2854dfcb2c..774e630411f 100644 --- a/src/templates/_includes/google-tag-manager-snippet.njk +++ b/src/templates/_includes/google-tag-manager-snippet.njk @@ -1,16 +1,27 @@ {% if GOOGLE_TAG_MANAGER_KEY %}