Skip to content

Commit

Permalink
Add cookie consent logic (#508)
Browse files Browse the repository at this point in the history
* Add Checkbox component

* Update Notice banner and align logic with Market

* Add Privacy Settings modal

* Fix Cypress tests

* Initialize analytics and sentry

* Add additional check for sentry init

* Prevent default on external link click

* Revert "Prevent default on external link click"

This reverts commit 7c2f4a6.

* Stop propagation of external link in privacy modal

* Omit target and rel props

* Update banner text
  • Loading branch information
Anboias authored Dec 4, 2024
1 parent 3017fe3 commit 887c6bf
Show file tree
Hide file tree
Showing 21 changed files with 564 additions and 174 deletions.
31 changes: 23 additions & 8 deletions cypress/e2e/footer.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,35 @@ import { HOME_PAGE } from '../support/common';
it('renders footer and error reporting', () => {
cy.visit(HOME_PAGE).dataCy('error-reporting').should('exist');

// Disallow error reporting
cy.dataCy('error-reporting').find('input').click();
cy.findByText('Done').click();
// Enable error report and analytics
cy.dataCy('error-reporting')
.findByRole('button', { name: /accept all/i })
.click();
cy.dataCy('error-reporting')
.should('not.exist')
.should(() => {
expect(localStorage.getItem('reportErrors')).to.equal('false');
.then(() => {
expect(localStorage.getItem('allow-error-reporting')).to.equal('true');
expect(localStorage.getItem('allow-analytics')).to.equal('true');
});

// On subsequent page visit there is no notice
cy.reload().should(() => {
expect(localStorage.getItem('reportErrors')).to.equal('false');
// On subsequent page visit the error reporting notice should not be shown
cy.reload().then(() => {
expect(cy.dataCy('error-reporting').should('not.exist'));
});

// Open the privacy settings modal from the footer and disable error reporting and analytics
cy.findByRole('button', { name: /error reporting/i }).click();
cy.findByRole('button', { name: /manage settings/i }).click();
cy.findByRole('checkbox', { name: /allow error reporting/i }).click();
cy.findByRole('checkbox', { name: /allow analytics cookies/i }).click();
cy.findByRole('button', { name: /save settings/i }).click();
cy.dataCy('error-reporting')
.should('not.exist')
.then(() => {
expect(localStorage.getItem('allow-error-reporting')).to.equal('false');
expect(localStorage.getItem('allow-analytics')).to.equal('false');
});

// Footer links should open the pages they link to in a new tab
cy.findByText('Github').should('have.attr', 'target', '_blank');
});
6 changes: 4 additions & 2 deletions cypress/support/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@ export const abbrStr = (str: string) => {
export const EPOCH_LENGTH = 7 * 60 * 60 * 24; // in seconds

export const closeErrorReportingNotice = () => {
cy.dataCy('error-reporting').findByText('Done').click();
cy.findByText('Done').should('not.exist');
cy.dataCy('error-reporting')
.findByRole('button', { name: /accept all/i })
.click();
cy.dataCy('error-reporting').should('not.exist');
};

export const HOME_PAGE = 'http://localhost:3000/#/';
123 changes: 123 additions & 0 deletions src/components/checkbox/checkbox.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
@import '../../styles/variables.module.scss';
@import '../../styles/fonts.module.scss';

.checkbox {
display: flex;
align-items: flex-start;
gap: 8px;

&,
* {
cursor: pointer;
transition: all 0.1s;
}

.checkmark {
display: flex;
align-items: center;
justify-content: center;
min-height: 16px;
min-width: 16px;
height: 16px;
width: 16px;
background-color: $color-gray-50;
border: 1px solid $color-dark-blue-50;
border-radius: 2px;
margin-top: 2px;

svg {
margin: auto;
width: 12px;
height: 12px;
color: $color-gray-50;
}
}

.checkboxTextBlock {
display: flex;
flex-direction: column;
@include font-body-12;

label {
color: $color-dark-blue-800;
}

.description {
color: $color-gray-500;
}
}

&[aria-checked='true'] {
.checkmark {
background-color: $color-dark-blue-400;
border-color: $color-gray-50;
}
}

&:hover:not([aria-disabled='true']) {
.checkmark {
background-color: $color-base-light;
border-color: $color-dark-blue-400;

svg {
color: $color-dark-blue-400;
}
}

.checkboxTextBlock {
label {
color: $color-gray-900;
}

.description {
color: $color-gray-600;
}
}
}

&[aria-disabled='true'] {
pointer-events: none;

.checkmark {
background-color: transparent;
border-color: $color-gray-200;

svg {
color: $color-gray-200;
}
}

.checkboxTextBlock {
label {
color: $color-gray-400;
}

.description {
color: $color-gray-200;
}
}
}
}

@media (min-width: $sm) {
.checkbox {
gap: 12px;

.checkmark {
min-height: 20px;
min-width: 20px;
height: 20px;
width: 20px;
margin-top: 2px;

svg {
width: 16px;
height: 16px;
}
}

.checkboxTextBlock {
@include font-body-9;
}
}
}
44 changes: 44 additions & 0 deletions src/components/checkbox/checkbox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { KeyboardEvent, ReactNode } from 'react';
import { CheckIcon } from '../icons';
import styles from './checkbox.module.scss';

interface Props {
label?: string;
checked: boolean;
children?: ReactNode;
disabled?: boolean;
onChange: (checked: boolean) => void;
}

const CheckBox = ({ label, checked, children, disabled, onChange }: Props) => {
const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
onChange(!checked);
}
};

return (
<div
id={label}
className={styles.checkbox}
tabIndex={0}
role="checkbox"
aria-checked={checked}
aria-disabled={disabled}
onClick={() => {
onChange(!checked);
}}
onKeyDown={handleKeyDown}
>
<span className={styles.checkmark}>{checked && <CheckIcon />}</span>

<div className={styles.checkboxTextBlock}>
<label htmlFor={label}>{label}</label>
{children && <div className={styles.description}>{children}</div>}
</div>
</div>
);
};

export default CheckBox;
2 changes: 2 additions & 0 deletions src/components/checkbox/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default } from './checkbox';
export * from './checkbox';
31 changes: 18 additions & 13 deletions src/components/external-link/external-link.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,37 @@
import { ReactNode, useEffect } from 'react';
import { ComponentPropsWithoutRef, useEffect } from 'react';

interface Props {
className?: string;
interface Props extends Omit<ComponentPropsWithoutRef<'a'>, 'target' | 'rel'> {
href: string;
children: ReactNode;
}

const ExternalLink = (props: Props) => {
const { className, children } = props;
const { children, href: incomingHref, ...rest } = props;

let href = props.href.trim();
const urlRegex = /^https?:\/\//i; // Starts with https:// or http:// (case insensitive)
if (!urlRegex.test(href)) {
href = 'about:blank';
}
const href = cleanHref(incomingHref);

useEffect(() => {
if (process.env.NODE_ENV === 'development' && href === 'about:blank') {
// eslint-disable-next-line no-console
console.warn(`An invalid URL has been provided: "${props.href}". Only https:// or http:// URLs are allowed.`);
console.warn(`An invalid URL has been provided: "${incomingHref}". Only https:// or http:// URLs are allowed.`);
}
}, [href, props.href]);
}, [href, incomingHref]);

return (
<a href={href} className={className} target="_blank" rel="noopener noreferrer">
<a target="_blank" rel="noopener noreferrer" href={href} {...rest}>
{children}
</a>
);
};

const cleanHref = (href: string) => {
const urlRegex = /^https?:\/\//i; // Starts with https:// or http:// (case insensitive)
const trimmedHref = href.trim();

if (!urlRegex.test(trimmedHref)) {
return 'about:blank';
}

return trimmedHref;
};

export default ExternalLink;
15 changes: 15 additions & 0 deletions src/components/icons/check.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { ComponentProps } from 'react';

export const CheckIcon = (props: ComponentProps<'svg'>) => {
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
d="M13 4.25L6.125 11.125L3 8"
stroke="currentColor"
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
};
5 changes: 3 additions & 2 deletions src/components/icons/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { CheckCircleFillIcon } from './check-circle-fill';
import { CheckboxRadioIcon } from './checkbox-radio';
import { CheckCircleFillIcon } from './check-circle-fill';
import { CheckIcon } from './check';
import { CloseIcon } from './close';
import { CrossIcon } from './cross';
import { HelpOutlineIcon } from './help-outline';
import { InfoCircleIcon } from './info-circle';

export { CheckCircleFillIcon, CheckboxRadioIcon, CloseIcon, CrossIcon, HelpOutlineIcon, InfoCircleIcon };
export { CheckCircleFillIcon, CheckboxRadioIcon, CheckIcon, CloseIcon, CrossIcon, HelpOutlineIcon, InfoCircleIcon };
Loading

0 comments on commit 887c6bf

Please sign in to comment.