Skip to content

Commit

Permalink
Simplify logic for cookie consent (#1241)
Browse files Browse the repository at this point in the history
First of all, I apologize that I could not make this suggestion as a
part of review. It is still challenging for me to make a suggestion
without actually touching the code.

This PR handles several things. The first to fix is cookieconsent form
gradually disappears on the first page load. The second is not depending
on session storage - mainly because I feel like it was diverging the
logic flow - but if I am missing anything, please feel free to close
this. The third is moving out the utility function outside of the
component, so it doesn't have to be declared as a dependency of
useEffect hook.
  • Loading branch information
snmln authored Nov 5, 2024
2 parents 0b1a732 + 2682238 commit 598ac2a
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 130 deletions.
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

0 comments on commit 598ac2a

Please sign in to comment.