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

Simplify logic for cookie consent #1241

Merged
merged 3 commits into from
Nov 5, 2024
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
37 changes: 14 additions & 23 deletions app/scripts/components/common/cookie-consent/cookieConsent.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,19 @@ import '@testing-library/jest-dom';
import { render, screen, fireEvent } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom'; // For testing
import { createMemoryHistory } from 'history';
import * as utils from './utils';

import * as utils from './utils';
import CookieConsent from './index';
const lodash = require('lodash');

describe('Cookie consent form should render with correct content.', () => {
const setDisplayCookieConsent = jest.fn();
const setGoogleTagManager = jest.fn();
const cookieData = {
title: 'Cookie Consent',
copy: '<p>We use cookies to enhance your browsing experience and to help us understand how our website is used. These cookies allow us to collect data on site usage and improve our services based on your interactions. To learn more about it, see our <a href="https://www.nasa.gov/privacy/#cookies">Privacy Policy</a></p>We use cookies to enhance your browsing experience and to help us understand how our website is used. These cookies allow us to collect data on site usage and improve our services based on your interactions. To learn more about it, see our [Privacy Policy](https://www.nasa.gov/privacy/#cookies)'
copy: '<p>We use cookies to enhance your browsing experience and to help us understand how our website is used. These cookies allow us to collect data on site usage and improve our services based on your interactions. To learn more about it, see our <a href="https://www.nasa.gov/privacy/#cookies">Privacy Policy</a></p>We use cookies to enhance your browsing experience and to help us understand how our website is used. These cookies allow us to collect data on site usage and improve our services based on your interactions. To learn more about it, see our [Privacy Policy](https://www.nasa.gov/privacy/#cookies)',
setDisplayCookieConsent,
setGoogleTagManager
};

const onFormInteraction = jest.fn();
Expand All @@ -24,28 +29,14 @@ describe('Cookie consent form should render with correct content.', () => {
pathname: 'localhost:3000/example/path'
})
}));

lodash.debounce = jest.fn((fn) => fn);

afterEach(() => {
jest.clearAllMocks();
});
it('Check that session item is non existant prior to cookie consent render. Then confirm that cookie consent creates session item.', () => {
expect(sessionStorage.getItem(utils.SESSION_KEY)).toBeNull();
render(
<MemoryRouter history={history}>
<CookieConsent {...cookieData} onFormInteraction={onFormInteraction} />
</MemoryRouter>
);
expect(sessionStorage.getItem(utils.SESSION_KEY)).toBe(`true`);
});
it('Check that getcookie is only called once on render and session item is true', () => {
const spy = jest.spyOn(utils, 'getCookie');

render(
<MemoryRouter history={history}>
<CookieConsent {...cookieData} onFormInteraction={onFormInteraction} />
</MemoryRouter>
);
expect(spy).toHaveBeenCalledTimes(1);
expect(sessionStorage.getItem(utils.SESSION_KEY)).toBe(`true`);
// Clear cookies after each test
document.cookie = `${utils.COOKIE_CONSENT_KEY}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
});

it('Renders correct content', () => {
Expand Down Expand Up @@ -91,8 +82,8 @@ describe('Cookie consent form should render with correct content.', () => {
<CookieConsent {...cookieData} onFormInteraction={onFormInteraction} />
</MemoryRouter>
);
const button = screen.getByRole('button', { name: 'Accept Cookies' });
fireEvent.click(button);
const acceptButton = screen.getByRole('button', { name: 'Accept Cookies' });
fireEvent.click(acceptButton);
const resultCookie = document.cookie;

expect(resultCookie).toBe(
Expand Down
185 changes: 94 additions & 91 deletions app/scripts/components/common/cookie-consent/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React, { useState, useEffect } from 'react';
import { debounce } from 'lodash';
import { Icon } from '@trussworks/react-uswds';

import { COOKIE_CONSENT_KEY, SESSION_KEY, getCookie } from './utils';
import { setCookie, getCookie } from './utils';
import {
USWDSAlert,
USWDSButton,
Expand All @@ -14,7 +15,7 @@ interface CookieConsentProps {
title?: string | undefined;
copy?: string | undefined;
pathname: string;
sessionStart: string | undefined;
setDisplayCookieConsentForm: (boolean) => void;
setGoogleTagManager: () => void;
}

Expand All @@ -25,126 +26,128 @@ function addAttribute(copy) {
export const CookieConsent = ({
title,
copy,
sessionStart,
pathname,
setDisplayCookieConsentForm,
setGoogleTagManager
}: CookieConsentProps) => {
const [cookieConsentResponded, SetCookieConsentResponded] =
const [cookieConsentResponded, setCookieConsentResponded] =
useState<boolean>(false);
const [cookieConsentAnswer, SetCookieConsentAnswer] =
// Debounce the setDisplayCookieConsentForm function
const debouncedSetCookieConsentResponded = debounce(
setCookieConsentResponded,
500
);

const [cookieConsentAnswer, setCookieConsentAnswer] =
useState<boolean>(false);
const [closeConsent, setCloseConsent] = useState<boolean>(false);

//Setting expiration date for cookie to expire and re-ask user for consent.
const setCookieExpiration = () => {
const today = new Date();
today.setMonth(today.getMonth() + 3);
return today.toUTCString();
};

const setCookie = (cookieValue, closeConsent) => {
document.cookie = `${COOKIE_CONSENT_KEY}=${JSON.stringify(
cookieValue
)}; path=/; expires=${closeConsent ? '0' : setCookieExpiration()}`;
};

const setSessionData = () => {
if (typeof window !== 'undefined') {
const checkForSessionDate = window.sessionStorage.getItem(SESSION_KEY);
if (!checkForSessionDate) {
window.sessionStorage.setItem(SESSION_KEY, 'true');
useEffect(() => {
const cookieContents = getCookie();
if (cookieContents) {
if (!cookieContents.responded) {
setCloseConsent(false);
return;
}
cookieContents.answer && setGoogleTagManager();
setCookieConsentResponded(cookieContents.responded);
setCookieConsentAnswer(cookieContents.answer);
setDisplayCookieConsentForm(false);
} else {
setCloseConsent(false);
}
};
// Only run on the first render
}, [setGoogleTagManager, setDisplayCookieConsentForm]);

useEffect(() => {
if (sessionStart !== 'true' && !cookieConsentResponded) {
setSessionData();
getCookie(
SetCookieConsentResponded,
SetCookieConsentAnswer,
setGoogleTagManager
);
}
if (!cookieConsentResponded && closeConsent) {
setCloseConsent(false);
}
// to Rerender on route change
if (!cookieConsentResponded) setCloseConsent(false);
// To render the component when user hasn't answered yet
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pathname]);

useEffect(() => {
// When not responded, do nothing.
if (!cookieConsentResponded) return;
// When answer is accept cookie,
// 1. set up google manager
cookieConsentAnswer && setGoogleTagManager();
// 2. update the cookie value
const cookieValue = {
responded: cookieConsentResponded,
answer: cookieConsentAnswer
};
setCookie(cookieValue, closeConsent);

// Ignoring setcookie for now since it will make infinite rendering
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [cookieConsentResponded, cookieConsentAnswer, closeConsent]);
// 3. Tell the layout that we don't have to render this consent form
// from the next render of layout.
setTimeout(() => {
setDisplayCookieConsentForm(false);
}, 500);
}, [
cookieConsentResponded,
cookieConsentAnswer,
closeConsent,
setDisplayCookieConsentForm,
setGoogleTagManager
]);

return (
<div>
{
// Adding debounce to conditional for animation out
setTimeout(() => {
!cookieConsentResponded;
}, 500) && (
<div
id='cookie-consent'
className={`margin-0 tablet:margin-2 shadow-2 position-fixed z-top maxw-full tablet:maxw-tablet-lg animation--fade-out right-0 bottom-0 ${
cookieConsentResponded || closeConsent
? ' opacity-0 z-bottom pointer-events--none'
: 'opacity-1 z-top'
}`}
{!cookieConsentResponded && (
<div
id='cookie-consent'
className={`margin-0 tablet:margin-2 shadow-2 position-fixed z-top maxw-full tablet:maxw-tablet-lg animation--fade-out right-0 bottom-0 ${
closeConsent
? ' opacity-0 z-bottom pointer-events--none'
: 'opacity-1 z-top'
}`}
>
<USWDSAlert
type='info'
heading={title && title}
headingLevel='h2'
noIcon={true}
className='radius-lg'
>
<USWDSAlert
type='info'
heading={title && title}
headingLevel='h2'
noIcon={true}
className='radius-lg'
<USWDSButton
type='button '
className='width-3 height-3 padding-0 position-absolute right-2 top-2'
onClick={() => {
setCloseConsent(true);
}}
unstyled
>
<Icon.Close size={3} />
</USWDSButton>

{copy && (
<div dangerouslySetInnerHTML={{ __html: addAttribute(copy) }} />
)}
<USWDSButtonGroup className='padding-top-2'>
<USWDSButton
type='button '
className='width-3 height-3 padding-0 position-absolute right-2 top-2'
onClick={() => {
debouncedSetCookieConsentResponded(true);
setCookieConsentAnswer(false);
setCloseConsent(true);
}}
unstyled
outline={true}
type='button'
>
<Icon.Close size={3} />
Decline Cookies
</USWDSButton>

{copy && (
<div dangerouslySetInnerHTML={{ __html: addAttribute(copy) }} />
)}
<USWDSButtonGroup className='padding-top-2'>
<USWDSButton
onClick={() => {
SetCookieConsentResponded(true);
SetCookieConsentAnswer(false);
}}
outline={true}
type='button'
>
Decline Cookies
</USWDSButton>
<USWDSButton
onClick={() => {
SetCookieConsentResponded(true);
SetCookieConsentAnswer(true);
}}
type='button'
>
Accept Cookies
</USWDSButton>
</USWDSButtonGroup>
</USWDSAlert>
</div>
)
}
<USWDSButton
onClick={() => {
debouncedSetCookieConsentResponded(true);
setCookieConsentAnswer(true);
setCloseConsent(true);
}}
type='button'
>
Accept Cookies
</USWDSButton>
</USWDSButtonGroup>
</USWDSAlert>
</div>
)}
</div>
);
};
Expand Down
23 changes: 15 additions & 8 deletions app/scripts/components/common/cookie-consent/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,23 @@ export const readCookie = (name: string): string => {
);
};

export const getCookie = (
SetCookieConsentResponded,
SetCookieConsentAnswer,
setGoogleTagManager
) => {
export const getCookie = () => {
const cookie = readCookie(COOKIE_CONSENT_KEY);
if (cookie) {
const cookieContents = JSON.parse(cookie);
if (cookieContents.answer) setGoogleTagManager();
SetCookieConsentResponded(cookieContents.responded);
SetCookieConsentAnswer(cookieContents.answer);
return cookieContents;
}
};

//Setting expiration date for cookie to expire and re-ask user for consent.
export const setCookieExpiration = () => {
const today = new Date();
today.setMonth(today.getMonth() + 3);
return today.toUTCString();
};

export const setCookie = (cookieValue, closeConsent) => {
document.cookie = `${COOKIE_CONSENT_KEY}=${JSON.stringify(
cookieValue
)}; path=/; expires=${closeConsent ? '0' : setCookieExpiration()}`;
};
13 changes: 5 additions & 8 deletions app/scripts/components/common/layout-root/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@ import { reveal } from '@devseed-ui/animation';
import { getBannerFromVedaConfig, getCookieConsentFromVedaConfig } from 'veda';
import MetaTags from '../meta-tags';
import PageFooter from '../page-footer';
// import Banner from '../banner';
// import { CookieConsent } from '../cookie-consent';
import { SESSION_KEY } from '../cookie-consent/utils';
const Banner = React.lazy(() => import('../banner'));
const CookieConsent = React.lazy(() => import('../cookie-consent'));

Expand Down Expand Up @@ -54,13 +51,13 @@ function LayoutRoot(props: { children?: ReactNode }) {
const cookieConsentContent = getCookieConsentFromVedaConfig();
const bannerContent = getBannerFromVedaConfig();
const { children } = props;
const [sessionStart, setSesstionStart] = useState<string | undefined>();
const sessionItem = window.sessionStorage.getItem(SESSION_KEY);
const [displayCookieConsentForm, setDisplayCookieConsentForm] =
useState<boolean>(true);
const { pathname } = useLocation();

useEffect(() => {
// When there is no cookie consent form set up
!cookieConsentContent && setGoogleTagManager();
sessionItem && setSesstionStart(sessionItem);
}, []);

const { title, thumbnail, description, hideFooter } =
Expand Down Expand Up @@ -92,10 +89,10 @@ function LayoutRoot(props: { children?: ReactNode }) {
<PageBody id={PAGE_BODY_ID} tabIndex={-1}>
<Outlet />
{children}
{cookieConsentContent && (
{cookieConsentContent && displayCookieConsentForm && (
<CookieConsent
{...cookieConsentContent}
sessionStart={sessionStart}
setDisplayCookieConsentForm={setDisplayCookieConsentForm}
setGoogleTagManager={setGoogleTagManager}
pathname={pathname}
/>
Expand Down
Loading