Skip to content

Commit

Permalink
Creating cookie consent component (#1127)
Browse files Browse the repository at this point in the history
**Related Ticket:**[ Cookie Consent
Form](https://github.com/orgs/NASA-IMPACT/projects/17/views/6?pane=issue&itemId=62543604)

### Description of Changes
USWDS Alert component used as a cookie consent form. This form should
render on any page of the site if a user has not consented to the use of
cookies. If a user _Accepts Cookies_ or _Declines Cookies_ a cookie will
be made to catalog the response by the user. The cookie will expire
after 3 months and the form will re-render for the user.

### Notes & Questions About Changes
Figma Designs:
https://www.figma.com/design/xaZSp74DKFGYm0k2w2BbVb/Shared-VEDA-file?node-id=0-1&t=wGNpe3xrUyLTU1pB-1

CookieConsent form is stored behind a feature flag to allow for each
instance to opt in to leveraging the component. To turn the
CookieConsentForm **ON** in the `veda-ui/.env` set
`COOKIE_CONSENT_FORM='TRUE'`

To change contents of the consent form change the contents of
`cookieConsentForm` within the `veda-ui/mock/veda.config.js` to reflect
desired content. To add link in the content, it must be added in the
following format `[Link text](URL)`

**Example prop:** '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)`'

### Validation / Testing
Run unit test CookieConsent.spec.js. To test in browser, run locally
navigate to browser cookies you should see the following values for
specific interactions:
- **Decline Cookie:** {"responded":true,"answer":false}
- **Accept Cookie:** {"responded":true,"answer":true}
- **Close out:** {"responded":false,"answer":false}
  • Loading branch information
snmln authored Sep 26, 2024
2 parents 95df663 + 3bbd537 commit 8967eaf
Show file tree
Hide file tree
Showing 16 changed files with 302 additions and 25 deletions.
3 changes: 2 additions & 1 deletion .env
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ API_STAC_ENDPOINT='https://staging.openveda.cloud/api/stac'
GOOGLE_FORM = 'https://docs.google.com/forms/d/e/1FAIpQLSfGcd3FDsM3kQIOVKjzdPn4f88hX8RZ4Qef7qBsTtDqxjTSkg/viewform?embedded=true'

FEATURE_NEW_EXPLORATION = 'TRUE'
SHOW_CONFIGURABLE_COLOR_MAP = 'FALSE'
SHOW_CONFIGURABLE_COLOR_MAP = 'FALSE'

63 changes: 63 additions & 0 deletions app/scripts/components/common/cookie-consent/cookieConsent.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React from 'react';
import '@testing-library/jest-dom';

import { render, screen, fireEvent } from '@testing-library/react';
import { COOKIE_CONSENT_KEY } from './utils';
import { CookieConsent } from './index';

describe('Cookie consent form should render with correct content.', () => {
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)'
};
const onFormInteraction = jest.fn();
beforeEach(() => {
render(
<CookieConsent {...cookieData} onFormInteraction={onFormInteraction} />
);
});
it('Renders correct content', () => {
expect(
screen.getByRole('link', { name: 'Privacy Policy' })
).toHaveAttribute('href', 'https://www.nasa.gov/privacy/#cookies');
expect(
screen.getByRole('button', { name: 'Decline Cookies' })
).toBeInTheDocument();
expect(
screen.getByRole('button', { name: 'Accept Cookies' })
).toBeInTheDocument();
expect(screen.getByText('Cookie Consent')).toBeInTheDocument();
expect(
screen.getByText(
'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'
)
).toBeInTheDocument();
});

it('Check correct cookie initialization', () => {
const resultCookie = document.cookie;

expect(resultCookie).toBe(
`${COOKIE_CONSENT_KEY}={"responded":false,"answer":false}`
);
});

it('Check correct cookie content on Decline click', () => {
const button = screen.getByRole('button', { name: 'Decline Cookies' });
fireEvent.click(button);
const resultCookie = document.cookie;
expect(resultCookie).toBe(
`${COOKIE_CONSENT_KEY}={"responded":true,"answer":false}`
);
});

it('Check correct cookie content on Accept click', () => {
const button = screen.getByRole('button', { name: 'Accept Cookies' });
fireEvent.click(button);
const resultCookie = document.cookie;

expect(resultCookie).toBe(
`${COOKIE_CONSENT_KEY}={"responded":true,"answer":true}`
);
});
});
3 changes: 3 additions & 0 deletions app/scripts/components/common/cookie-consent/index.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.animation--fade-out {
transition: opacity 0.5s ease-in-out 0.125s;
}
108 changes: 108 additions & 0 deletions app/scripts/components/common/cookie-consent/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import React, { useState, useEffect } from 'react';
import { Icon } from '@trussworks/react-uswds';
import { COOKIE_CONSENT_KEY } from './utils';
import {
USWDSAlert,
USWDSButton,
USWDSButtonGroup
} from '$components/common/uswds';

import './index.scss';

interface CookieConsentProps {
title?: string | undefined;
copy?: string | undefined;
onFormInteraction: () => void;
}

function addAttribute (copy) {
return copy.replaceAll('<a', `<a target="_blank" rel="noopener" role="link"`);
}

export const CookieConsent = ({
title,
copy,
onFormInteraction
}: CookieConsentProps) => {
const [cookieConsentResponded, SetCookieConsentResponded] =
useState<boolean>(false);
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()}`;
};

useEffect(() => {
const cookieValue = {
responded: cookieConsentResponded,
answer: cookieConsentAnswer
};
setCookie(cookieValue, closeConsent);
onFormInteraction();
// Ignoring setcookie for now sine it will make infinite rendering
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [cookieConsentResponded, cookieConsentAnswer, closeConsent, onFormInteraction]);

return (
<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' : 'opacity-1'
}`}
>
<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
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>
);
};
2 changes: 2 additions & 0 deletions app/scripts/components/common/cookie-consent/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const NO_COOKIE = 'NO COOKIE';
export const COOKIE_CONSENT_KEY = `veda--CookieConsent`;
62 changes: 56 additions & 6 deletions app/scripts/components/common/layout-root/index.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
import React, { ReactNode, useContext, useCallback } from 'react';
import React, { ReactNode, useContext, useCallback, useEffect } from 'react';
import { useDeepCompareEffect } from 'use-deep-compare';
import styled from 'styled-components';
import { Outlet } from 'react-router';
import { reveal } from '@devseed-ui/animation';
import { getCookieConsentFromVedaConfig } from 'veda';
import MetaTags from '../meta-tags';
import PageFooter from '../page-footer';
import Banner from '../banner';
import { CookieConsent } from '../cookie-consent';
import { COOKIE_CONSENT_KEY, NO_COOKIE } from '../cookie-consent/utils';

import { LayoutRootContext } from './context';

import { useGoogleTagManager } from '$utils/use-google-tag-manager';
import { setGoogleTagManager } from '$utils/use-google-tag-manager';

import NavWrapper from '$components/common/nav-wrapper';
import Logo from '$components/common/page-header/logo';
import { mainNavItems, subNavItems} from '$components/common/page-header/default-config';
import {
mainNavItems,
subNavItems
} from '$components/common/page-header/default-config';

const appTitle = process.env.APP_TITLE;
const appDescription = process.env.APP_DESCRIPTION;
Expand All @@ -35,17 +42,50 @@ const PageBody = styled.div`
`;

function LayoutRoot(props: { children?: ReactNode }) {
const cookieConsentContent = getCookieConsentFromVedaConfig();
const readCookie = (name) => {
const nameEQ = name + '=';
const attribute = document.cookie.split(';');
for (let i = 0; i < attribute.length; i++) {
let c = attribute[i];
while (c.charAt(0) == ' ') c = c.substring(1, c.length);
if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length);
}
return null;
};

const getCookie = () => {
const cookie = readCookie(COOKIE_CONSENT_KEY);
if (cookie) {
const cookieContents = JSON.parse(cookie);
if (cookieContents.answer) setGoogleTagManager();
return cookieContents;
}
return NO_COOKIE;
};

const showForm = () => {
const cookieContents = getCookie();
if (cookieContents === NO_COOKIE) {
return true;
} else {
return !cookieContents.responded;
}
};
const { children } = props;

useGoogleTagManager();

useEffect(() => {
!cookieConsentContent && setGoogleTagManager();
}, []);

const { title, thumbnail, description, banner, hideFooter } =
useContext(LayoutRootContext);

const truncatedTitle =
title?.length > 32 ? `${title.slice(0, 32)}...` : title;

const fullTitle = truncatedTitle ? `${truncatedTitle} — ` : '';

return (
<Page>
<MetaTags
Expand All @@ -54,10 +94,20 @@ function LayoutRoot(props: { children?: ReactNode }) {
thumbnail={thumbnail}
/>
{banner && <Banner appTitle={title} {...banner} />}
<NavWrapper mainNavItems={mainNavItems} subNavItems={subNavItems} logo={<Logo />} />
<NavWrapper
mainNavItems={mainNavItems}
subNavItems={subNavItems}
logo={<Logo />}
/>
<PageBody id={PAGE_BODY_ID} tabIndex={-1}>
<Outlet />
{children}
{cookieConsentContent && showForm() && (
<CookieConsent
{...cookieConsentContent}
onFormInteraction={getCookie}
/>
)}
</PageBody>
<PageFooter isHidden={hideFooter} />
</Page>
Expand Down
6 changes: 6 additions & 0 deletions app/scripts/components/common/uswds/alert.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import React from "react";
import { Alert } from "@trussworks/react-uswds";

export function USWDSAlert (props) {
return <Alert {...props} />;
}
9 changes: 9 additions & 0 deletions app/scripts/components/common/uswds/button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React from "react";
import { Button, ButtonGroup} from "@trussworks/react-uswds";

export function USWDSButton (props) {
return <Button {...props} />;
}
export function USWDSButtonGroup (props) {
return <ButtonGroup {...props} />;
}
4 changes: 4 additions & 0 deletions app/scripts/components/common/uswds/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { USWDSAlert } from './alert';
export { USWDSButtonGroup, USWDSButton } from './button';
export { USWDSLink } from './link';
export { USWDSBanner, USWDSBannerContent } from './banner';
6 changes: 6 additions & 0 deletions app/scripts/components/common/uswds/link.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import React from "react";
import { Link } from "@trussworks/react-uswds";

export function USWDSLink (props) {
return <Link {...props} />;
}
13 changes: 7 additions & 6 deletions app/scripts/components/home/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
ContentOverride
} from '$components/common/page-overrides';


const homeContent = getOverride('homeContent');

const Connections = styled(Hug)`
Expand Down Expand Up @@ -112,10 +113,10 @@ const getCoverProps = () => {

return author
? {
...coverProps,
attributionAuthor: author.name,
attributionUrl: author.url
}
...coverProps,
attributionAuthor: author.name,
attributionUrl: author.url
}
: coverProps;
} else {
return {
Expand All @@ -138,9 +139,8 @@ function RootHome() {
<PageMainContent>
<LayoutProps
title='Welcome'
banner={renderBanner? {...banner}: null}
banner={renderBanner ? { ...banner } : null}
/>

<ComponentOverride with='homeHero'>
<PageHeroHome
title={homeContent?.data.title ?? `Welcome to the ${appTitle}`}
Expand Down Expand Up @@ -171,6 +171,7 @@ function RootHome() {
</ComponentOverride>

<ContentOverride with='homeContent'>

<Audience />

<FeaturedStories />
Expand Down
7 changes: 6 additions & 1 deletion app/scripts/styles/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,10 @@
@use "usa-layout-grid";
@use "usa-banner";
@use "usa-button";
@use "usa-card";
@use "usa-alert";
@use "usa-button-group";
@use "usa-icon";
@use "usa-card";
@use "usa-modal";


7 changes: 2 additions & 5 deletions app/scripts/utils/use-google-tag-manager.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect } from 'react';

import TagManager from 'react-gtm-module';

const gtMCode = process.env.GOOGLE_TAG_MANAGER_ID;
Expand All @@ -13,11 +13,8 @@ const tagManagerArgs = {
cookies_win:'x'
};

export function useGoogleTagManager() {

useEffect(() => {
export function setGoogleTagManager() {
if (gtMCode) {
TagManager.initialize(tagManagerArgs);
}
}, []);
}
Loading

0 comments on commit 8967eaf

Please sign in to comment.