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 1 commit
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
41 changes: 24 additions & 17 deletions clients/fides-js/src/components/App.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,28 @@
import { h } from "preact";
import { FidesConfig } from "../lib/consent-types";
import { FunctionComponent, h } from "preact";
import {
ExperienceConfig,
FidesOptions,
UserGeolocation,
} from "../lib/consent-types";
import ConsentBanner from "./ConsentBanner";
import { getConsentContext } from "../lib/consent-context";
import {
makeConsentDefaults,
CookieKeyConsent,
setConsentCookieAcceptAll,
setConsentCookieRejectAll,
} from "../lib/cookie";

const App = ({ config }: { config: FidesConfig }) => {
const context = getConsentContext();
const consentDefaults = makeConsentDefaults({
config: config.consent,
context,
});
export interface OverlayProps {
consentDefaults: CookieKeyConsent;
options: FidesOptions;
experience?: ExperienceConfig;
geolocation?: UserGeolocation;
}

const Overlay: FunctionComponent<OverlayProps> = ({
consentDefaults,
experience,
options,
}) => {
const onAcceptAll = () => {
setConsentCookieAcceptAll(consentDefaults);
// TODO: save to Fides consent request API
Expand All @@ -35,16 +43,15 @@ const App = ({ config }: { config: FidesConfig }) => {

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}
bannerTitle={experience?.banner_title}
bannerDescription={experience?.banner_description}
confirmationButtonLabel={experience?.confirmation_button_label}
rejectButtonLabel={experience?.reject_button_label}
privacyCenterUrl={options.privacyCenterUrl}
onAcceptAll={onAcceptAll}
onRejectAll={onRejectAll}
waitBeforeShow={100}
/>
);
};

export default App;
export default Overlay;
20 changes: 10 additions & 10 deletions clients/fides-js/src/components/ConsentBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,25 @@ import "../lib/banner.module.css";
import { useHasMounted } from "../lib/hooks";

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

const ConsentBanner: FunctionComponent<BannerProps> = ({
bannerTitle = "Manage your consent",
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",
rejectButtonLabel = "Reject All",
privacyCenterUrl,
onAcceptAll,
onRejectAll,
waitBeforeShow,
waitBeforeShow = 100,
}) => {
const [isShown, setIsShown] = useState(false);
const hasMounted = useHasMounted();
eastandwestwind marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
7 changes: 6 additions & 1 deletion clients/fides-js/src/fides.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,12 @@ const init = async (config: FidesConfig) => {
// Load any existing user preferences from the browser cookie
const cookie = getOrMakeFidesCookie(consentDefaults);

await initOverlay(config);
await initOverlay({
consentDefaults,
experience: config.experience,
geolocation: config.geolocation,
options: config.options,
});

// Initialize the window.Fides object
_Fides.consent = cookie.consent;
Expand Down
137 changes: 79 additions & 58 deletions clients/fides-js/src/lib/consent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,32 @@ import { h, render } from "preact";
import { FidesConfig, FidesOptions, UserGeolocation } from "./consent-types";
import { debugLog } from "./consent-utils";

import App from "../components/App";
import Overlay, { OverlayProps } from "../components/App";

/**
* Validate the config options
*/
const validateBannerOptions = (config: FidesConfig): boolean => {
const validateOptions = (options: FidesOptions): boolean => {
// Check if options is an invalid type
if (config.options === undefined || typeof config.options !== "object") {
if (options === undefined || typeof options !== "object") {
return false;
}
// todo- more validation here?

if (!config.options.privacyCenterUrl) {
debugLog(
config.options.debug,
"Invalid banner options: privacyCenterUrl is required!"
);
if (!options.privacyCenterUrl) {
debugLog(options.debug, "Invalid options: privacyCenterUrl is required!");
return false;
}

if (config.options.privacyCenterUrl) {
if (options.privacyCenterUrl) {
try {
// eslint-disable-next-line no-new
new URL(config.options.privacyCenterUrl);
new URL(options.privacyCenterUrl);
} catch (e) {
debugLog(
config.options.debug,
"Invalid banner options: privacyCenterUrl is an invalid URL!",
config
options.debug,
"Invalid options: privacyCenterUrl is an invalid URL!",
options.privacyCenterUrl
);
return false;
}
Expand All @@ -45,8 +42,8 @@ const validateBannerOptions = (config: FidesConfig): boolean => {
* Returns null if geolocation cannot be constructed by provided params
*/
const constructLocation = (
debug: boolean,
geoLocation: UserGeolocation
geoLocation: UserGeolocation,
debug: boolean = false
): string | null => {
debugLog(debug, "validating getLocation...");
if (geoLocation.location) {
Expand All @@ -55,6 +52,9 @@ const constructLocation = (
if (geoLocation.country && geoLocation.region) {
return `${geoLocation.country}-${geoLocation.region}`;
}
if (geoLocation.country) {
eastandwestwind marked this conversation as resolved.
Show resolved Hide resolved
return geoLocation.country;
}
debugLog(
debug,
"cannot construct user location from provided geoLocation params..."
Expand All @@ -65,30 +65,29 @@ const constructLocation = (
/**
* Fetch the user's geolocation from an external API
*/
const getLocation = async (options: FidesOptions): Promise<UserGeolocation> => {
debugLog(options.debug, "Running getLocation...");
const { geolocationApiUrl } = options;
const getLocation = async (
geolocationApiUrl: string,
debug: boolean = false
): Promise<UserGeolocation> => {
debugLog(debug, "Running getLocation...");

if (!geolocationApiUrl) {
debugLog(
options.debug,
debug,
"Location cannot be found due to no configured geoLocationApiUrl."
);
return {};
}

debugLog(
options.debug,
`Calling geolocation API: GET ${geolocationApiUrl}...`
);
debugLog(debug, `Calling geolocation API: GET ${geolocationApiUrl}...`);
const fetchOptions: RequestInit = {
mode: "cors",
};
const response = await fetch(geolocationApiUrl, fetchOptions);

if (!response.ok) {
debugLog(
options.debug,
debug,
"Error getting location from geolocation API, returning {}. Response:",
response
);
Expand All @@ -98,14 +97,14 @@ const getLocation = async (options: FidesOptions): Promise<UserGeolocation> => {
try {
const body = await response.json();
debugLog(
options.debug,
debug,
"Got location response from geolocation API, returning:",
body
);
return body;
} catch (e) {
debugLog(
options.debug,
debug,
"Error parsing response body from geolocation API, returning {}. Response:",
response
);
Expand All @@ -121,88 +120,110 @@ const getLocation = async (options: FidesOptions): Promise<UserGeolocation> => {
*
* (see the type definition of ConsentBannerOptions for what options are available)
*/
export const initOverlay = async (config: FidesConfig): Promise<void> => {
debugLog(config.options.debug, "Initializing Fides consent overlays...");
export const initOverlay = async ({
consentDefaults,
experience,
geolocation,
options,
}: OverlayProps): Promise<void> => {
debugLog(options.debug, "Initializing Fides consent overlays...");

debugLog(
config.options.debug,
options.debug,
"Validating Fides consent banner options...",
config.options
options
);
if (!validateBannerOptions(config)) {
if (!validateOptions(options)) {
return Promise.reject(new Error("Invalid banner options"));
}

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

async function afterDomIsLoaded() {
debugLog(config.options.debug, "DOM fully loaded and parsed");

async function renderFidesOverlay() {
try {
debugLog(
config.options.debug,
"Adding Fides consent banner CSS & HTML into the DOM..."
options.debug,
"Rending Fides overlay CSS & HTML into the DOM..."
);
let userLocation: UserGeolocation | undefined = config.geolocation;

// Fetch the user location (if not pre-loaded)
let userLocation: UserGeolocation | undefined = geolocation;
if (
!config.experience &&
!userLocation &&
config.options.isGeolocationEnabled
options.isGeolocationEnabled &&
options.geolocationApiUrl
) {
userLocation = await getLocation(config.options);
if (constructLocation(config.options.debug, userLocation)) {
userLocation = await getLocation(
options.geolocationApiUrl,
options.debug
);
if (constructLocation(userLocation, options.debug)) {
// todo- get applicable notices using geoLocation
debugLog(config.options.debug, "User location found.", userLocation);
debugLog(options.debug, "User location found.", userLocation);
} else {
debugLog(
config.options.debug,
options.debug,
"User location could not be constructed from location params.",
userLocation
);
}
} else {
debugLog(
config.options.debug,
options.debug,
"Geolocation must be enabled if config.geolocation is not provided!"
);
}

render(<App config={config} />, document.body);
// Render the Overlay to the DOM!
render(
<Overlay
consentDefaults={consentDefaults}
options={options}
experience={experience}
geolocation={userLocation}
/>,
document.body
eastandwestwind marked this conversation as resolved.
Show resolved Hide resolved
);
debugLog(options.debug, "Fides overlay is now showing!");

// Look for a "#fides-consent-link" element in the DOM and update it to link to the Privacy Center
// DEFER: Revisit whether or not this "link" logic is needed
const consentLinkEl = document.getElementById("fides-consent-link");
if (
consentLinkEl &&
consentLinkEl instanceof HTMLAnchorElement &&
config.options.privacyCenterUrl
options.privacyCenterUrl
) {
debugLog(
config.options.debug,
`Fides consent link el found, replacing href with ${config.options.privacyCenterUrl}`
options.debug,
`Fides consent link el found, replacing href with ${options.privacyCenterUrl}`
);
consentLinkEl.href = config.options.privacyCenterUrl;
consentLinkEl.href = options.privacyCenterUrl;
// TODO: depending on notices / experience config, we update onclick of this link to nav to PC or open modal,
// or hide link entirely
} else {
debugLog(config.options.debug, "Fides consent link el not found");
debugLog(options.debug, "Fides consent link el not found");
}

debugLog(config.options.debug, "Fides consent banner is now showing!");
} catch (e) {
debugLog(config.options.debug, e);
debugLog(options.debug, e);
}
}

// Ensure we only render the overlay to the DOM once it's loaded
if (document?.readyState !== "complete") {
debugLog(config.options.debug, "DOM not loaded, adding event listener");
debugLog(options.debug, "DOM not loaded, adding event listener");
document.addEventListener("DOMContentLoaded", async () => {
await afterDomIsLoaded();
debugLog(options.debug, "DOM fully loaded and parsed");
await renderFidesOverlay();
});
} else {
await afterDomIsLoaded();
await renderFidesOverlay();
}

return Promise.resolve();
Expand Down