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

Add infrastructure for consent banner and link #3191

Merged
merged 61 commits into from
May 13, 2023
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
ee22baa
parent 9149f2f211cfea68bfa9f5c770fec3a79d31744b
eastandwestwind Apr 25, 2023
06e46cb
fixing some conflicts from rebase
eastandwestwind May 8, 2023
d4bef32
Refactor ConsentBannerButton into a Preact FunctionComponent function
NevilleS May 8, 2023
7655725
Move ConsentBannerButton to a new "components" folder
NevilleS May 8, 2023
88e12ef
adds duplicate banner demo page that allows for config to be injected…
eastandwestwind May 8, 2023
19ffcbd
cypress tests are working for consent banner
eastandwestwind May 9, 2023
69f672a
regenerate package-lock, add back turbo output cache
eastandwestwind May 9, 2023
cfa8110
fix eslint configs
eastandwestwind May 9, 2023
92c7e85
remove public/lib file, rename css vars
eastandwestwind May 9, 2023
71ee51e
restructure some components to functional, removes link component
eastandwestwind May 9, 2023
55eccc0
inject accept/reject events from fides parent for the banner
eastandwestwind May 9, 2023
bf59c50
Add note about aliasing react imports
NevilleS May 9, 2023
39b1618
Update tsconfig to include tsx
allisonking May 9, 2023
af0282e
Swap href instead of using onClick for anchor component
allisonking May 9, 2023
e5723b5
Export debugLog as a function instead of default
allisonking May 9, 2023
a815fd2
clarify diff between banner demo pages
eastandwestwind May 9, 2023
878e00c
Update clients/fides-js/src/lib/cookie.ts
eastandwestwind May 9, 2023
5f64996
Fix accept all and reject all logic
allisonking May 9, 2023
1acf4ae
Run prettier
allisonking May 9, 2023
b6f8e26
Run prettier on privacy center
allisonking May 9, 2023
db0ce36
Run prettier on PC README
allisonking May 9, 2023
902ae80
refactor to inject legacy consent, consent experience, geolocation, a…
eastandwestwind May 10, 2023
675eff9
banner props should default properly
eastandwestwind May 10, 2023
9c26448
move eslint config, update banner to support banner title
eastandwestwind May 10, 2023
fe992d7
remove unneeded npm dev dependency in fides-js
eastandwestwind May 10, 2023
5ac8512
re-enable some eslint rules
eastandwestwind May 10, 2023
482b7ec
Get cypress to run again
allisonking May 10, 2023
1962cd4
Update cypress test to use win.fidesConfig
allisonking May 10, 2023
d33ffab
Allow visitConsentDemo to hang out with its friends
allisonking May 10, 2023
333adaa
Merge branch 'main' into banner-infra
allisonking May 10, 2023
83d2271
Format
allisonking May 10, 2023
6cb4b2e
Update package-lock.json
allisonking May 10, 2023
fb407ed
remove unneeded tslint config
eastandwestwind May 10, 2023
4e2866d
remove passing consent defaults into init, fix circular deps
eastandwestwind May 10, 2023
d8f7377
Patch package-lock for turbo in different architectures
allisonking May 10, 2023
428b183
Autoformat
allisonking May 10, 2023
f3755b2
fix jest in fides-js
eastandwestwind May 10, 2023
21b7883
Only render ConsentBanner after mount
allisonking May 11, 2023
5fd0574
do not render banner by default
eastandwestwind May 11, 2023
7be8b35
config.options is required by package
eastandwestwind May 11, 2023
48d4a21
Merge branch 'main' of github.com:ethyca/fides into banner-infra
eastandwestwind May 12, 2023
c9ca6da
add to changelog
eastandwestwind May 12, 2023
7fa2e94
remove unneeded deps
eastandwestwind May 12, 2023
f0cac32
remove unneeded todo, update privacy center readme
eastandwestwind May 12, 2023
d66c260
rename isDisabled to isOverlayDisabled
eastandwestwind May 12, 2023
2bca847
small renaming, refactor todos into callback func
eastandwestwind May 12, 2023
fc31dc4
Refactor consent.tsx to render App
allisonking May 11, 2023
409d3f3
cleanup after cherry-picking to add new App component
eastandwestwind May 12, 2023
8d1627b
prettier
eastandwestwind May 12, 2023
d461a27
add logic to prevent retrieving geolocation under certain conditions,…
eastandwestwind May 12, 2023
bd35fe9
add check for DOM loaded before adding listener
eastandwestwind May 12, 2023
1b50997
Make the CHANGELOG entry more exciting!
NevilleS May 12, 2023
15160ee
Unbundle the FidesConfig and pass explicit props to initOverlay
NevilleS May 12, 2023
0f8769e
Rename App -> Overlay
NevilleS May 12, 2023
adfee4e
Add a prop for manage preferences link label
NevilleS May 12, 2023
bfb7461
Fix gitignore
NevilleS May 12, 2023
077db9a
Add tests for makeConsentDefaults
NevilleS May 13, 2023
bdf9ab5
Add tests for setConsentCookie
NevilleS May 13, 2023
6d06ab2
Remove TODO
NevilleS May 13, 2023
590c826
Format
NevilleS May 13, 2023
6920461
Merge branch 'main' of github.com:ethyca/fides into banner-infra
NevilleS May 13, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 consent overlay, add bundled script to `/fides.js` route in privacy-center [#3191](https://github.com/ethyca/fides/pull/3191)
eastandwestwind marked this conversation as resolved.
Show resolved Hide resolved

## [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
3 changes: 0 additions & 3 deletions clients/fides-js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,6 @@
"eslint-config-airbnb-typescript": "^17.0.0",
"eslint-config-preact": "^1.3.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-react": "^7.32.2",
"jest": "^29.5.0",
"jest-environment-jsdom": "^29.5.0",
Expand Down
50 changes: 50 additions & 0 deletions clients/fides-js/src/components/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { h } from "preact";
import { FidesConfig } from "../lib/consent-types";
import ConsentBanner from "./ConsentBanner";
import { getConsentContext } from "../lib/consent-context";
import {
makeConsentDefaults,
setConsentCookieAcceptAll,
setConsentCookieRejectAll,
} from "../lib/cookie";

const App = ({ config }: { config: FidesConfig }) => {
eastandwestwind marked this conversation as resolved.
Show resolved Hide resolved
const context = getConsentContext();
const consentDefaults = makeConsentDefaults({
config: config.consent,
context,
});

const onAcceptAll = () => {
setConsentCookieAcceptAll(consentDefaults);
// TODO: save to Fides consent request API
// eslint-disable-next-line no-console
console.error(
"Could not save consent record to Fides API, not implemented!"
);
};

const onRejectAll = () => {
setConsentCookieRejectAll(consentDefaults);
// TODO: save to Fides consent request API
// eslint-disable-next-line no-console
console.error(
"Could not save consent record to Fides API, not implemented!"
);
};

return (
<ConsentBanner
bannerTitle={config.experience?.banner_title}
bannerDescription={config.experience?.banner_description}
confirmationButtonLabel={config.experience?.confirmation_button_label}
rejectButtonLabel={config.experience?.reject_button_label}
privacyCenterUrl={config.options.privacyCenterUrl}
onAcceptAll={onAcceptAll}
onRejectAll={onRejectAll}
waitBeforeShow={100}
/>
);
};

export default App;
21 changes: 9 additions & 12 deletions clients/fides-js/src/components/ConsentBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ 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 {
bannerTitle?: string;
Expand All @@ -26,19 +27,25 @@ const ConsentBanner: FunctionComponent<BannerProps> = ({
waitBeforeShow,
}) => {
const [isShown, setIsShown] = useState(false);
const hasMounted = useHasMounted();
eastandwestwind marked this conversation as resolved.
Show resolved Hide resolved

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

const navigateToPrivacyCenter = (): void => {
if (privacyCenterUrl) {
window.location.assign(privacyCenterUrl);
}
};
// TODO: support option to specify top/bottom
// TODO: add banner title

if (!hasMounted) {
return null;
}

return (
<div
id="fides-consent-banner"
Expand Down Expand Up @@ -75,11 +82,6 @@ const ConsentBanner: FunctionComponent<BannerProps> = ({
onClick={() => {
onRejectAll();
setIsShown(false);
// TODO: save to Fides consent request API
// eslint-disable-next-line no-console
console.error(
"Could not save consent record to Fides API, not implemented!"
);
}}
/>
<ConsentBannerButton
Expand All @@ -88,11 +90,6 @@ const ConsentBanner: FunctionComponent<BannerProps> = ({
onClick={() => {
onAcceptAll();
setIsShown(false);
// TODO: save to Fides consent request API
// eslint-disable-next-line no-console
console.error(
"Could not save consent record to Fides API, not implemented!"
);
}}
/>
</div>
Expand Down
17 changes: 10 additions & 7 deletions clients/fides-js/src/fides.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ import { gtm } from "./integrations/gtm";
import { meta } from "./integrations/meta";
import { shopify } from "./integrations/shopify";
import { getConsentContext } from "./lib/consent-context";
import { initFidesConsent } from "./lib/consent";
import { initOverlay } from "./lib/consent";
import {
CookieKeyConsent,
CookieIdentity,
Expand All @@ -55,7 +55,6 @@ import {
makeConsentDefaults,
} from "./lib/cookie";
import {
defaultFidesOptions,
ExperienceConfig,
FidesConfig,
FidesOptions,
Expand Down Expand Up @@ -90,9 +89,6 @@ let _Fides: Fides;
* Initialize the global Fides object with the given configuration values
*/
const init = async (config: FidesConfig) => {
// passed-in config should override default options
// eslint-disable-next-line no-param-reassign
config.options = { ...defaultFidesOptions, ...config.options };
// Configure the default consent values
const context = getConsentContext();
const consentDefaults = makeConsentDefaults({
Expand All @@ -103,7 +99,8 @@ const init = async (config: FidesConfig) => {
// Load any existing user preferences from the browser cookie
const cookie = getOrMakeFidesCookie(consentDefaults);

await initFidesConsent(config);
await initOverlay(config);

// Initialize the window.Fides object
_Fides.consent = cookie.consent;
_Fides.fides_meta = cookie.fides_meta;
Expand All @@ -120,7 +117,13 @@ _Fides = {
consent: {},
experience: undefined,
geolocation: {},
options: { ...defaultFidesOptions, privacyCenterUrl: "" },
options: {
debug: true,
isOverlayDisabled: true,
isGeolocationEnabled: false,
geolocationApiUrl: "",
privacyCenterUrl: "",
},
fides_meta: {},
identity: {},
gtm,
Expand Down
10 changes: 2 additions & 8 deletions clients/fides-js/src/lib/consent-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export interface FidesConfig {
// Set the consent defaults from a "legacy" Privacy Center config.json.
consent?: ConsentConfig;
// Set the "experience" to be used for this Fides.js instance -- overrides the "legacy" config.
// If set, Fides.js will fetch neither experience config nor user geolocation.
// If not set, Fides.js will fetch its own experience config.
experience?: ExperienceConfig;
// Set the geolocation for this Fides.js instance. If *not* set, Fides.js will fetch its own geolocation.
Expand All @@ -12,19 +13,12 @@ export interface FidesConfig {
options: FidesOptions;
}

export const defaultFidesOptions = {
debug: false,
isDisabled: false,
isGeolocationEnabled: false,
geolocationApiUrl: "cdn-api.ethyca.com/location",
};

export type FidesOptions = {
// Whether or not debug log statements should be enabled
debug: boolean;

// Whether or not the banner should be globally disabled
isDisabled: boolean;
isOverlayDisabled: boolean;

// Whether user geolocation should be enabled. Requires geolocationApiUrl
isGeolocationEnabled: boolean;
Expand Down
113 changes: 67 additions & 46 deletions clients/fides-js/src/lib/consent.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import { h, render } from "preact";
import {
makeConsentDefaults,
setConsentCookieAcceptAll,
setConsentCookieRejectAll,
} from "./cookie";
import ConsentBanner from "../components/ConsentBanner";

import { FidesConfig, FidesOptions, UserGeolocation } from "./consent-types";
import { debugLog } from "./consent-utils";
import { getConsentContext } from "./consent-context";

import App from "../components/App";

/**
* Validate the config options
Expand Down Expand Up @@ -44,11 +40,32 @@ const validateBannerOptions = (config: FidesConfig): boolean => {
return true;
};

/**
* Construct user geolocation str to be ingested by Fides API
* Returns null if geolocation cannot be constructed by provided params
*/
const constructLocation = (
debug: boolean,
geoLocation: UserGeolocation
): string | null => {
debugLog(debug, "validating getLocation...");
if (geoLocation.location) {
return geoLocation.location;
}
if (geoLocation.country && geoLocation.region) {
return `${geoLocation.country}-${geoLocation.region}`;
}
eastandwestwind marked this conversation as resolved.
Show resolved Hide resolved
debugLog(
debug,
"cannot construct user location from provided geoLocation params..."
);
return null;
};

/**
* Fetch the user's geolocation from an external API
*/
const getLocation = async (options: FidesOptions): Promise<UserGeolocation> => {
// assumes that isGeolocationEnabled is true
debugLog(options.debug, "Running getLocation...");
const { geolocationApiUrl } = options;

Expand Down Expand Up @@ -97,12 +114,15 @@ const getLocation = async (options: FidesOptions): Promise<UserGeolocation> => {
};

/**
* Initialize the Fides Consent Banner or Link, with optional extraOptions to override defaults.
* Initialize the Fides Consent overlay components.
*
* Includes fetching location and experience, if not provided, setting up banner and modal,
* showing/hiding links, and showing/hiding the banner.
*
* (see the type definition of ConsentBannerOptions for what options are available)
*/
export const initFidesConsent = async (config: FidesConfig): Promise<void> => {
debugLog(config.options.debug, "Initializing Fides consent...");
export const initOverlay = async (config: FidesConfig): Promise<void> => {
eastandwestwind marked this conversation as resolved.
Show resolved Hide resolved
debugLog(config.options.debug, "Initializing Fides consent overlays...");

debugLog(
config.options.debug,
Expand All @@ -113,55 +133,47 @@ export const initFidesConsent = async (config: FidesConfig): Promise<void> => {
return Promise.reject(new Error("Invalid banner options"));
}

if (config.options.isDisabled) {
if (config.options.isOverlayDisabled) {
debugLog(
config.options.debug,
"Fides consent banner is disabled, skipping banner initialization!"
);
return Promise.resolve();
}

document.addEventListener("DOMContentLoaded", () => {
async function afterDomIsLoaded() {
debugLog(config.options.debug, "DOM fully loaded and parsed");

try {
debugLog(
config.options.debug,
"Adding Fides consent banner CSS & HTML into the DOM..."
);
if (config.options.isGeolocationEnabled) {
getLocation(config.options)
.then(() => {
// todo- get applicable notices using location
})
.catch(() => {
// if something goes wrong with location api, we still want to render notices
});
let userLocation: UserGeolocation | undefined = config.geolocation;
if (
!config.experience &&
!userLocation &&
config.options.isGeolocationEnabled
) {
userLocation = await getLocation(config.options);
if (constructLocation(config.options.debug, userLocation)) {
// todo- get applicable notices using geoLocation
debugLog(config.options.debug, "User location found.", userLocation);
} else {
debugLog(
config.options.debug,
"User location could not be constructed from location params.",
userLocation
);
}
} else {
debugLog(
config.options.debug,
"Geolocation must be enabled if config.geolocation is not provided!"
);
}
const context = getConsentContext();
const consentDefaults = makeConsentDefaults({
config: config.consent,
context,
});
const onAcceptAll = () => {
setConsentCookieAcceptAll(consentDefaults);
};
const onRejectAll = () => {
setConsentCookieRejectAll(consentDefaults);
};
render(
<ConsentBanner
bannerTitle={config.experience?.banner_title}
bannerDescription={config.experience?.banner_description}
confirmationButtonLabel={config.experience?.confirmation_button_label}
rejectButtonLabel={config.experience?.reject_button_label}
privacyCenterUrl={config.options.privacyCenterUrl}
onAcceptAll={onAcceptAll}
onRejectAll={onRejectAll}
waitBeforeShow={100}
/>,
document.body
);

render(<App config={config} />, document.body);
const consentLinkEl = document.getElementById("fides-consent-link");
if (
consentLinkEl &&
Expand All @@ -183,6 +195,15 @@ export const initFidesConsent = async (config: FidesConfig): Promise<void> => {
} catch (e) {
debugLog(config.options.debug, e);
}
});
}
if (document?.readyState !== "complete") {
debugLog(config.options.debug, "DOM not loaded, adding event listener");
document.addEventListener("DOMContentLoaded", async () => {
await afterDomIsLoaded();
});
} else {
await afterDomIsLoaded();
}

return Promise.resolve();
};