Skip to content

Commit

Permalink
Add infrastructure for consent banner and link (#3191)
Browse files Browse the repository at this point in the history
Co-authored-by: Neville Samuell <neville@ethyca.com>
Co-authored-by: Allison King <allisonjuliaking@gmail.com>
Co-authored-by: Allison King <allison@ethyca.com>
  • Loading branch information
4 people committed May 13, 2023
1 parent 061d65d commit bbbf319
Show file tree
Hide file tree
Showing 26 changed files with 21,738 additions and 7,902 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ The types of changes are:
### Added

- Add an automated test to check for `/fides-consent.js` backwards compatibility [#3289](https://github.com/ethyca/fides/pull/3289)
- Add infrastructure for "overlay" consent components (Preact, CSS bundling, etc.) and initial version of consent banner [#3191](https://github.com/ethyca/fides/pull/3191)

## [2.13.0](https://github.com/ethyca/fides/compare/2.12.1...2.13.0)

Expand Down
2 changes: 1 addition & 1 deletion clients/cypress-e2e/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Admin UI will be found at `localhost:3000` and Privacy Center at `localhost:3001
Then, in this folder:

```
turbo run cy:run
npm run cy:run
```

### Environment variables
Expand Down
15 changes: 14 additions & 1 deletion clients/fides-js/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,26 @@
{
"extends": ["airbnb", "airbnb-typescript/base", "prettier"],
"extends": ["preact", "airbnb", "airbnb-typescript/base", "prettier"],
"parserOptions": {
"project": "./tsconfig.json"
},
"rules": {
"react/jsx-filename-extension": [
2,
{ "extensions": [",.js", ".jsx", ".ts", ".tsx"] }
],
"react/function-component-definition": [
2,
{
"namedComponents": "arrow-function",
"unnamedComponents": "arrow-function"
}
],
"curly": ["error", "all"],
"nonblock-statement-body-position": ["error", "below"],
"import/prefer-default-export": "off",
"import/extensions": "off",
"react/react-in-jsx-scope": "off",
"react/prop-types": "off",
"no-param-reassign": [
"error",
{
Expand Down
102 changes: 101 additions & 1 deletion clients/fides-js/__tests__/lib/cookie.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@ import {
CookieKeyConsent,
FidesCookie,
getOrMakeFidesCookie,
makeConsentDefaults,
makeFidesCookie,
saveFidesCookie,
} from "~/lib/cookie";
setConsentCookieAcceptAll,
setConsentCookieRejectAll,
} from "../../src/lib/cookie";
import type { ConsentConfig } from "../../src/lib/consent-config";
import type { ConsentContext } from "../../src/lib/consent-context";

// Setup mock date
const MOCK_DATE = "2023-01-01T12:00:00.000Z";
Expand Down Expand Up @@ -167,3 +172,98 @@ describe("saveFidesCookie", () => {
expect(mockSetCookie.mock.calls[0][2]).toHaveProperty("domain", expected);
});
});

describe("makeConsentDefaults", () => {
const config: ConsentConfig = {
options: [
{
cookieKeys: ["default_undefined"],
fidesDataUseKey: "provide.service",
},
{
cookieKeys: ["default_true"],
default: true,
fidesDataUseKey: "improve.system",
},
{
cookieKeys: ["default_false"],
default: false,
fidesDataUseKey: "personalize.system",
},
{
cookieKeys: ["default_true_with_gpc_false"],
default: { value: true, globalPrivacyControl: false },
fidesDataUseKey: "advertising.third_party",
},
{
cookieKeys: ["default_false_with_gpc_true"],
default: { value: false, globalPrivacyControl: true },
fidesDataUseKey: "third_party_sharing.payment_processing",
},
],
};

describe("when global privacy control is not present", () => {
const context: ConsentContext = {};

it("returns the default consent values by key", () => {
expect(makeConsentDefaults({ config, context })).toEqual({
default_true: true,
default_false: false,
default_true_with_gpc_false: true,
default_false_with_gpc_true: false,
});
});
});

describe("when global privacy control is set", () => {
const context: ConsentContext = {
globalPrivacyControl: true,
};

it("returns the default consent values by key", () => {
expect(makeConsentDefaults({ config, context })).toEqual({
default_true: true,
default_false: false,
default_true_with_gpc_false: false,
default_false_with_gpc_true: true,
});
});
});
});

describe("setConsentCookie", () => {
afterEach(() => mockSetCookie.mockClear());

const defaults: CookieKeyConsent = {
default_true: true,
default_false: false,
another_true: true,
another_false: false,
};

it("AcceptAll sets all consent preferences to true", () => {
setConsentCookieAcceptAll(defaults);
expect(mockSetCookie.mock.calls).toHaveLength(1);
const cookie = JSON.parse(mockSetCookie.mock.calls[0][1]);
expect(cookie.consent).toEqual({
default_true: true,
default_false: true,
another_true: true,
another_false: true,
});
});

// NOTE: this will need to be updated for notice-only preferences!
it("RejectAll sets all consent preferences to false", () => {
setConsentCookieRejectAll(defaults);
expect(mockSetCookie.mock.calls).toHaveLength(1);
const cookie = JSON.parse(mockSetCookie.mock.calls[0][1]);
expect(cookie.consent).toEqual({
default_true: false,
default_false: false,
another_true: false,
another_false: false,
});
});
});
7 changes: 6 additions & 1 deletion clients/fides-js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"directory": "clients/fides-js"
},
"dependencies": {
"preact": "^10.13.2",
"typescript-cookie": "^1.0.6",
"uuid": "^9.0.0"
},
Expand All @@ -37,7 +38,9 @@
"eslint": "^8.36.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^17.0.0",
"eslint-config-preact": "^1.3.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-react": "^7.32.2",
"jest": "^29.5.0",
"jest-environment-jsdom": "^29.5.0",
"js-cookie": "^3.0.5",
Expand All @@ -47,7 +50,9 @@
"rollup-plugin-dts": "^5.3.0",
"rollup-plugin-esbuild": "^5.0.0",
"rollup-plugin-filesize": "^10.0.0",
"rollup-plugin-import-css": "^3.2.1",
"ts-jest": "^29.1.0",
"typescript": "^4.9.5"
"typescript": "^4.9.5",
"typescript-plugin-css-modules": "^5.0.1"
}
}
16 changes: 14 additions & 2 deletions clients/fides-js/rollup.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import dts from "rollup-plugin-dts";
import esbuild from "rollup-plugin-esbuild";
import filesize from "rollup-plugin-filesize";
import nodeResolve from "@rollup/plugin-node-resolve";
import css from "rollup-plugin-import-css";

const name = "fides";
const isDev = process.env.NODE_ENV === "development";
Expand All @@ -15,8 +16,11 @@ const GZIP_SIZE_WARN_KB = 15; // log a warning if bundle size exceeds this
export default [
{
input: `src/${name}.ts`,
// DEFER: Add aliases for typical react imports (see https://preactjs.com/guide/v10/getting-started/#aliasing-in-rollup)
// This will be needed if & when we want to leverage other packages written for the React ecosystem
plugins: [
nodeResolve(),
css(),
esbuild({
minify: !isDev,
}),
Expand All @@ -28,6 +32,14 @@ export default [
verbose: true,
hook: "writeBundle",
}),
copy({
// Automatically add the built css to the privacy center's static files for bundling:
targets: [
{ src: `dist/${name}.css`, dest: "../privacy-center/public/lib/" },
],
verbose: true,
hook: "writeBundle",
}),
filesize({
reporter: [
"boxen", // default reporter, which prints a nice CLI output
Expand Down Expand Up @@ -70,7 +82,7 @@ export default [
},
{
input: `src/${name}.ts`,
plugins: [nodeResolve(), esbuild()],
plugins: [nodeResolve(), css(), esbuild()],
output: [
{
// Compatible with ES module imports. Apps in this repo may be able to share the code.
Expand All @@ -82,7 +94,7 @@ export default [
},
{
input: `src/${name}.ts`,
plugins: [dts()],
plugins: [dts(), css()],
output: [
{
file: `dist/${name}.d.ts`,
Expand Down
102 changes: 102 additions & 0 deletions clients/fides-js/src/components/ConsentBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { h, FunctionComponent } from "preact";
import { useState, useEffect } from "preact/hooks";
import { ButtonType } from "../lib/consent-types";
import ConsentBannerButton from "./ConsentBannerButton";
import "../lib/banner.module.css";
import { useHasMounted } from "../lib/hooks";

interface BannerProps {
onAcceptAll: () => void;
onRejectAll: () => void;
privacyCenterUrl: string;
bannerDescription?: string;
bannerTitle?: string;
confirmationButtonLabel?: string;
rejectButtonLabel?: string;
managePreferencesLabel?: string;
waitBeforeShow?: number;
}

const ConsentBanner: FunctionComponent<BannerProps> = ({
onAcceptAll,
onRejectAll,
privacyCenterUrl,
bannerDescription = "This website processes your data respectfully, so we require your consent to use cookies.",
bannerTitle = "Manage your consent",
confirmationButtonLabel = "Accept All",
managePreferencesLabel = "Manage Preferences",
rejectButtonLabel = "Reject All",
waitBeforeShow = 100,
}) => {
const [isShown, setIsShown] = useState(false);
const hasMounted = useHasMounted();

useEffect(() => {
const delayBanner = setTimeout(() => {
setIsShown(true);
}, waitBeforeShow);
return () => clearTimeout(delayBanner);
}, [setIsShown, waitBeforeShow]);

const navigateToPrivacyCenter = (): void => {
if (privacyCenterUrl) {
window.location.assign(privacyCenterUrl);
}
};

if (!hasMounted) {
return null;
}

return (
<div
id="fides-consent-banner"
className={`fides-consent-banner fides-consent-banner-bottom ${
isShown ? "" : "fides-consent-banner-hidden"
} `}
>
<div>
<div
id="fides-consent-banner-title"
className="fides-consent-banner-title"
>
{bannerTitle || ""}
</div>
<div
id="fides-consent-banner-description"
className="fides-consent-banner-description"
>
{bannerDescription || ""}
</div>
</div>
<div
id="fides-consent-banner-buttons"
className="fides-consent-banner-buttons"
>
<ConsentBannerButton
buttonType={ButtonType.TERTIARY}
label={managePreferencesLabel}
onClick={navigateToPrivacyCenter}
/>
<ConsentBannerButton
buttonType={ButtonType.SECONDARY}
label={rejectButtonLabel}
onClick={() => {
onRejectAll();
setIsShown(false);
}}
/>
<ConsentBannerButton
buttonType={ButtonType.PRIMARY}
label={confirmationButtonLabel}
onClick={() => {
onAcceptAll();
setIsShown(false);
}}
/>
</div>
</div>
);
};

export default ConsentBanner;
25 changes: 25 additions & 0 deletions clients/fides-js/src/components/ConsentBannerButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { h, FunctionComponent } from "preact";
import { ButtonType } from "../lib/consent-types";

interface ButtonProps {
buttonType: ButtonType;
label?: string;
onClick?: () => void;
}

const ConsentBannerButton: FunctionComponent<ButtonProps> = ({
buttonType,
label,
onClick,
}) => (
<button
type="button"
id={`fides-consent-banner-button-${buttonType.valueOf()}`}
className={`fides-consent-banner-button fides-consent-banner-button-${buttonType.valueOf()}`}
onClick={onClick}
>
{label || ""}
</button>
);

export default ConsentBannerButton;

0 comments on commit bbbf319

Please sign in to comment.