From b9bd5161bc325fda8ab3c0453f08b9444040df2a Mon Sep 17 00:00:00 2001 From: Allison King Date: Wed, 31 May 2023 09:02:10 -0700 Subject: [PATCH 01/24] Render privacy center experience from privacy notices (#3340) --- CHANGELOG.md | 2 +- .../cypress-e2e/cypress/e2e/smoke_test.cy.ts | 10 +- .../privacy-center/app/server-environment.ts | 6 +- .../consent/ConfigDrivenConsent.tsx | 190 +++++++++++++++ .../components/{ => consent}/ConsentCard.tsx | 2 +- .../components/consent/ConsentDescription.tsx | 45 ++++ .../components/consent/ConsentHeading.tsx | 34 +++ .../ConsentItem.tsx} | 62 ++--- .../components/consent/ConsentToggles.tsx | 21 ++ .../consent/NoticeDrivenConsent.tsx | 209 ++++++++++++++++ .../components/consent/SaveCancel.tsx | 28 +++ .../cypress/e2e/consent-notices.cy.ts | 191 +++++++++++++++ .../privacy-center/cypress/e2e/consent.cy.ts | 29 ++- .../fixtures/config/config_consent.json | 3 + .../cypress/fixtures/consent/experience.json | 88 +++++++ .../cypress/fixtures/consent/geolocation.json | 6 + .../fixtures/consent/privacy_preferences.json | 50 ++++ .../cypress/support/commands.ts | 18 ++ .../features/common/api.slice.ts | 2 +- .../features/common/settings.slice.ts | 17 +- .../features/consent/consent.slice.ts | 125 +++++++++- .../features/consent/helpers.ts | 46 ++++ .../privacy-center/features/consent/hooks.ts | 56 +++++ .../privacy-center/features/consent/types.ts | 6 + clients/privacy-center/pages/consent.tsx | 225 +++--------------- clients/privacy-center/pages/index.tsx | 2 +- clients/privacy-center/types/api/index.ts | 16 ++ .../types/api/models/ComponentType.ts | 11 + .../types/api/models/ConsentMechanism.ts | 12 + .../types/api/models/ConsentMethod.ts | 12 + .../types/api/models/ConsentOptionCreate.ts | 13 + .../models/CurrentPrivacyPreferenceSchema.ts | 18 ++ .../types/api/models/DeliveryMechanism.ts | 11 + .../types/api/models/EnforcementLevel.ts | 12 + .../api/models/ExperienceConfigResponse.ts | 31 +++ .../types/api/models/Identity.ts | 1 + .../models/Page_PrivacyExperienceResponse_.ts | 12 + .../api/models/PrivacyExperienceResponse.ts | 26 ++ .../api/models/PrivacyNoticeHistorySchema.ts | 30 +++ .../types/api/models/PrivacyNoticeRegion.ts | 86 +++++++ .../types/api/models/PrivacyNoticeResponse.ts | 31 +++ ...rivacyNoticeResponseWithUserPreferences.ts | 37 +++ .../api/models/PrivacyPreferencesRequest.ts | 21 ++ .../types/api/models/UserConsentPreference.ts | 12 + 44 files changed, 1612 insertions(+), 253 deletions(-) create mode 100644 clients/privacy-center/components/consent/ConfigDrivenConsent.tsx rename clients/privacy-center/components/{ => consent}/ConsentCard.tsx (90%) create mode 100644 clients/privacy-center/components/consent/ConsentDescription.tsx create mode 100644 clients/privacy-center/components/consent/ConsentHeading.tsx rename clients/privacy-center/components/{ConsentItemCard.tsx => consent/ConsentItem.tsx} (60%) create mode 100644 clients/privacy-center/components/consent/ConsentToggles.tsx create mode 100644 clients/privacy-center/components/consent/NoticeDrivenConsent.tsx create mode 100644 clients/privacy-center/components/consent/SaveCancel.tsx create mode 100644 clients/privacy-center/cypress/e2e/consent-notices.cy.ts create mode 100644 clients/privacy-center/cypress/fixtures/consent/experience.json create mode 100644 clients/privacy-center/cypress/fixtures/consent/geolocation.json create mode 100644 clients/privacy-center/cypress/fixtures/consent/privacy_preferences.json create mode 100644 clients/privacy-center/features/consent/hooks.ts create mode 100644 clients/privacy-center/types/api/models/ComponentType.ts create mode 100644 clients/privacy-center/types/api/models/ConsentMechanism.ts create mode 100644 clients/privacy-center/types/api/models/ConsentMethod.ts create mode 100644 clients/privacy-center/types/api/models/ConsentOptionCreate.ts create mode 100644 clients/privacy-center/types/api/models/CurrentPrivacyPreferenceSchema.ts create mode 100644 clients/privacy-center/types/api/models/DeliveryMechanism.ts create mode 100644 clients/privacy-center/types/api/models/EnforcementLevel.ts create mode 100644 clients/privacy-center/types/api/models/ExperienceConfigResponse.ts create mode 100644 clients/privacy-center/types/api/models/Page_PrivacyExperienceResponse_.ts create mode 100644 clients/privacy-center/types/api/models/PrivacyExperienceResponse.ts create mode 100644 clients/privacy-center/types/api/models/PrivacyNoticeHistorySchema.ts create mode 100644 clients/privacy-center/types/api/models/PrivacyNoticeRegion.ts create mode 100644 clients/privacy-center/types/api/models/PrivacyNoticeResponse.ts create mode 100644 clients/privacy-center/types/api/models/PrivacyNoticeResponseWithUserPreferences.ts create mode 100644 clients/privacy-center/types/api/models/PrivacyPreferencesRequest.ts create mode 100644 clients/privacy-center/types/api/models/UserConsentPreference.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a55f07434..7403ed66e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,7 @@ The types of changes are: ## [Unreleased](https://github.com/ethyca/fides/compare/2.14.0...main) ### Added - +- Privacy center can now render its consent values based on Privacy Notices and Privacy Experiences [#3340](https://github.com/ethyca/fides/pull/3340) - Add Google Tag Manager and Privacy Center ENV vars to sample app [#2949](https://github.com/ethyca/fides/pull/2949) ### Fixed diff --git a/clients/cypress-e2e/cypress/e2e/smoke_test.cy.ts b/clients/cypress-e2e/cypress/e2e/smoke_test.cy.ts index 763c02828b..aa1fa1d597 100644 --- a/clients/cypress-e2e/cypress/e2e/smoke_test.cy.ts +++ b/clients/cypress-e2e/cypress/e2e/smoke_test.cy.ts @@ -88,24 +88,24 @@ describe("Smoke test", () => { // - Data Sales or Sharing => true // - Email Marketing => true // - Product Analytics => true - cy.getByTestId(`consent-item-card-advertising`).within(() => { + cy.getByTestId(`consent-item-advertising`).within(() => { cy.contains("Data Sales or Sharing"); cy.getRadio("true").should("be.checked"); cy.getRadio("false").should("not.be.checked"); }); - cy.getByTestId(`consent-item-card-advertising.first_party`).within(() => { + cy.getByTestId(`consent-item-advertising.first_party`).within(() => { cy.contains("Email Marketing"); cy.getRadio("true").should("be.checked"); cy.getRadio("false").should("not.be.checked"); }); - cy.getByTestId(`consent-item-card-improve`).within(() => { + cy.getByTestId(`consent-item-improve`).within(() => { cy.contains("Product Analytics"); cy.getRadio("true").should("be.checked"); cy.getRadio("false").should("not.be.checked"); }); // Opt-out of data sales / sharing - cy.getByTestId(`consent-item-card-advertising`).within(() => { + cy.getByTestId(`consent-item-advertising`).within(() => { cy.getRadio("false").check({ force: true }); }); cy.contains("Save").click(); @@ -119,7 +119,7 @@ describe("Smoke test", () => { cy.get("input#email").type("jenny@example.com"); cy.get("button").contains("Continue").click(); }); - cy.getByTestId(`consent-item-card-advertising`).within(() => { + cy.getByTestId(`consent-item-advertising`).within(() => { cy.getRadio("true").should("not.be.checked"); cy.getRadio("false").should("be.checked"); }); diff --git a/clients/privacy-center/app/server-environment.ts b/clients/privacy-center/app/server-environment.ts index 217becfb5d..3e955ec371 100644 --- a/clients/privacy-center/app/server-environment.ts +++ b/clients/privacy-center/app/server-environment.ts @@ -251,9 +251,9 @@ export const loadPrivacyCenterEnvironment = // Overlay options DEBUG: process.env.FIDES_PRIVACY_CENTER__DEBUG === "true" || false, - IS_OVERLAY_DISABLED: - process.env.FIDES_PRIVACY_CENTER__IS_OVERLAY_DISABLED === "false" || - true, + IS_OVERLAY_DISABLED: process.env.FIDES_PRIVACY_CENTER__IS_OVERLAY_DISABLED + ? process.env.FIDES_PRIVACY_CENTER__IS_OVERLAY_DISABLED === "true" + : true, IS_GEOLOCATION_ENABLED: process.env.FIDES_PRIVACY_CENTER__IS_GEOLOCATION_ENABLED === "true" || false, diff --git a/clients/privacy-center/components/consent/ConfigDrivenConsent.tsx b/clients/privacy-center/components/consent/ConfigDrivenConsent.tsx new file mode 100644 index 0000000000..b5c049e314 --- /dev/null +++ b/clients/privacy-center/components/consent/ConfigDrivenConsent.tsx @@ -0,0 +1,190 @@ +import { Divider, Stack, useToast } from "@fidesui/react"; +import React, { useCallback, useEffect, useMemo } from "react"; +import { getConsentContext, resolveConsentValue } from "fides-js"; +import { useAppDispatch, useAppSelector } from "~/app/hooks"; +import { + changeConsent, + selectFidesKeyToConsent, + useUpdateConsentRequestPreferencesDeprecatedMutation, +} from "~/features/consent/consent.slice"; +import { getGpcStatus } from "~/features/consent/helpers"; + +import { useConfig } from "~/features/common/config.slice"; +import { GpcStatus } from "~/features/consent/types"; +import { inspectForBrowserIdentities } from "~/common/browser-identities"; +import { useLocalStorage } from "~/common/hooks"; +import { ConsentPreferences } from "~/types/api"; +import { useRouter } from "next/router"; +import { ErrorToastOptions, SuccessToastOptions } from "~/common/toast-options"; +import ConsentItem from "./ConsentItem"; +import SaveCancel from "./SaveCancel"; + +const ConfigDrivenConsent = ({ + storePreferences, +}: { + storePreferences: (data: ConsentPreferences) => void; +}) => { + const config = useConfig(); + const consentOptions = useMemo( + () => config.consent?.page.consentOptions ?? [], + [config] + ); + const toast = useToast(); + const router = useRouter(); + const dispatch = useAppDispatch(); + const consentContext = useMemo(() => getConsentContext(), []); + const fidesKeyToConsent = useAppSelector(selectFidesKeyToConsent); + const [consentRequestId] = useLocalStorage("consentRequestId", ""); + const [verificationCode] = useLocalStorage("verificationCode", ""); + const [ + updateConsentRequestPreferencesMutationTrigger, + updateConsentRequestPreferencesMutationResult, + ] = useUpdateConsentRequestPreferencesDeprecatedMutation(); + + /** + * Update the consent choices on the backend. + */ + const saveUserConsentOptions = useCallback(() => { + const consent = consentOptions.map((option) => { + const defaultValue = resolveConsentValue(option.default, consentContext); + const value = fidesKeyToConsent[option.fidesDataUseKey] ?? defaultValue; + const gpcStatus = getGpcStatus({ + value, + consentOption: option, + consentContext, + }); + + return { + data_use: option.fidesDataUseKey, + data_use_description: option.description, + opt_in: value, + has_gpc_flag: gpcStatus !== GpcStatus.NONE, + conflicts_with_gpc: gpcStatus === GpcStatus.OVERRIDDEN, + }; + }); + + const executableOptions = consentOptions.map((option) => ({ + data_use: option.fidesDataUseKey, + executable: option.executable ?? false, + })); + + const browserIdentity = inspectForBrowserIdentities(); + + updateConsentRequestPreferencesMutationTrigger({ + id: consentRequestId, + body: { + code: verificationCode, + policy_key: config.consent?.page.policy_key, + consent, + executable_options: executableOptions, + browser_identity: browserIdentity, + }, + }); + }, [ + config, + consentContext, + consentOptions, + consentRequestId, + fidesKeyToConsent, + updateConsentRequestPreferencesMutationTrigger, + verificationCode, + ]); + + const toastError = useCallback( + ({ + title = "An error occurred while retrieving user consent preferences.", + error, + }: { + title?: string; + error?: any; + }) => { + toast({ + title, + description: error?.data?.detail, + ...ErrorToastOptions, + }); + }, + [toast] + ); + + useEffect(() => { + if (updateConsentRequestPreferencesMutationResult.isError) { + toastError({ + title: "An error occurred while saving user consent preferences", + error: updateConsentRequestPreferencesMutationResult.error, + }); + return; + } + + if (updateConsentRequestPreferencesMutationResult.isSuccess) { + storePreferences(updateConsentRequestPreferencesMutationResult.data); + toast({ + title: "Your consent preferences have been saved", + ...SuccessToastOptions, + }); + router.push("/"); + } + }, [ + updateConsentRequestPreferencesMutationResult, + storePreferences, + toastError, + toast, + router, + ]); + + const handleCancel = () => { + router.push("/"); + }; + + const items = useMemo( + () => + consentOptions.map((option) => { + const defaultValue = resolveConsentValue( + option.default, + consentContext + ); + const value = fidesKeyToConsent[option.fidesDataUseKey] ?? defaultValue; + const gpcStatus = getGpcStatus({ + value, + consentOption: option, + consentContext, + }); + + return { + ...option, + value, + gpcStatus, + }; + }), + [consentContext, consentOptions, fidesKeyToConsent] + ); + + return ( + + {items.map((item, index) => { + const { fidesDataUseKey, highlight, url, name, description } = item; + const handleChange = (value: boolean) => { + dispatch(changeConsent({ key: fidesDataUseKey, value })); + }; + return ( + + {index > 0 ? : null} + + + ); + })} + + + ); +}; + +export default ConfigDrivenConsent; diff --git a/clients/privacy-center/components/ConsentCard.tsx b/clients/privacy-center/components/consent/ConsentCard.tsx similarity index 90% rename from clients/privacy-center/components/ConsentCard.tsx rename to clients/privacy-center/components/consent/ConsentCard.tsx index 6406a81526..731a1b07d2 100644 --- a/clients/privacy-center/components/ConsentCard.tsx +++ b/clients/privacy-center/components/consent/ConsentCard.tsx @@ -1,5 +1,5 @@ import React from "react"; -import Card from "./Card"; +import Card from "~/components/Card"; type ConsentCardProps = { title: string; diff --git a/clients/privacy-center/components/consent/ConsentDescription.tsx b/clients/privacy-center/components/consent/ConsentDescription.tsx new file mode 100644 index 0000000000..5a9587692d --- /dev/null +++ b/clients/privacy-center/components/consent/ConsentDescription.tsx @@ -0,0 +1,45 @@ +import { Box, Text, TextProps } from "@fidesui/react"; +import { useAppSelector } from "~/app/hooks"; +import { useConfig } from "~/features/common/config.slice"; +import { selectIsNoticeDriven } from "~/features/common/settings.slice"; +import { selectPrivacyExperience } from "~/features/consent/consent.slice"; + +const TEXT_PROPS: TextProps = { + fontSize: ["small", "medium"], + fontWeight: "medium", + maxWidth: 624, + textAlign: "center", + color: "gray.600", +}; + +const ConsentDescription = () => { + const config = useConfig(); + const isNoticeDriven = useAppSelector(selectIsNoticeDriven); + const experience = useAppSelector(selectPrivacyExperience); + + if (!isNoticeDriven) { + return ( + + + {config.consent?.page.description} + + {config.consent?.page.description_subtext?.map((paragraph, index) => ( + + {paragraph} + + ))} + + ); + } + return ( + + {experience?.experience_config?.component_description} + + ); +}; + +export default ConsentDescription; diff --git a/clients/privacy-center/components/consent/ConsentHeading.tsx b/clients/privacy-center/components/consent/ConsentHeading.tsx new file mode 100644 index 0000000000..325c9f3954 --- /dev/null +++ b/clients/privacy-center/components/consent/ConsentHeading.tsx @@ -0,0 +1,34 @@ +import { Heading } from "@fidesui/react"; +import { useMemo } from "react"; +import { useAppSelector } from "~/app/hooks"; +import { useConfig } from "~/features/common/config.slice"; +import { selectIsNoticeDriven } from "~/features/common/settings.slice"; +import { selectPrivacyExperience } from "~/features/consent/consent.slice"; + +const ConsentHeading = () => { + const config = useConfig(); + const isNoticeDriven = useAppSelector(selectIsNoticeDriven); + const experience = useAppSelector(selectPrivacyExperience); + + const headingText = useMemo(() => { + if (!isNoticeDriven) { + return config.consent?.page.title; + } + + return experience?.experience_config?.component_title; + }, [config, isNoticeDriven, experience]); + + return ( + + {headingText} + + ); +}; + +export default ConsentHeading; diff --git a/clients/privacy-center/components/ConsentItemCard.tsx b/clients/privacy-center/components/consent/ConsentItem.tsx similarity index 60% rename from clients/privacy-center/components/ConsentItemCard.tsx rename to clients/privacy-center/components/consent/ConsentItem.tsx index fecbd737ac..6a3d0ba318 100644 --- a/clients/privacy-center/components/ConsentItemCard.tsx +++ b/clients/privacy-center/components/consent/ConsentItem.tsx @@ -12,32 +12,39 @@ import { ExternalLinkIcon, } from "@fidesui/react"; -import { ConfigConsentOption } from "~/types/config"; -import { useAppDispatch } from "~/app/hooks"; -import { changeConsent } from "~/features/consent/consent.slice"; import { GpcStatus } from "~/features/consent/types"; import { GpcBadge, GpcInfo } from "~/features/consent/GpcMessages"; -type ConsentItemProps = { - option: ConfigConsentOption; +export type ConsentItemProps = { + id: string; + name: string; + description: string; + highlight?: boolean; + url?: string; value: boolean; gpcStatus: GpcStatus; + onChange: (value: boolean) => void; }; -const ConsentItemCard = ({ option, value, gpcStatus }: ConsentItemProps) => { - const { name, description, highlight, url, fidesDataUseKey } = option; - - const dispatch = useAppDispatch(); - +const ConsentItem = ({ + id, + name, + description, + highlight, + url, + value, + gpcStatus, + onChange, +}: ConsentItemProps) => { const handleRadioChange = (radioValue: string) => { - dispatch(changeConsent({ option, value: radioValue === "true" })); + onChange(radioValue === "true"); }; return ( { - + {description} - - - - - Find out more about this consent - - - - + {url ? ( + + + + Find out more about this consent + + + + + ) : null} @@ -95,4 +103,4 @@ const ConsentItemCard = ({ option, value, gpcStatus }: ConsentItemProps) => { ); }; -export default ConsentItemCard; +export default ConsentItem; diff --git a/clients/privacy-center/components/consent/ConsentToggles.tsx b/clients/privacy-center/components/consent/ConsentToggles.tsx new file mode 100644 index 0000000000..a0e5c87878 --- /dev/null +++ b/clients/privacy-center/components/consent/ConsentToggles.tsx @@ -0,0 +1,21 @@ +import { useSettings } from "~/features/common/settings.slice"; +import { ConsentPreferences } from "~/types/api"; + +import ConfigDrivenConsent from "./ConfigDrivenConsent"; +import NoticeDrivenConsent from "./NoticeDrivenConsent"; + +const ConsentToggles = ({ + storePreferences, +}: { + storePreferences: (data: ConsentPreferences) => void; +}) => { + const settings = useSettings(); + const { IS_OVERLAY_DISABLED } = settings; + + if (IS_OVERLAY_DISABLED) { + return ; + } + return ; +}; + +export default ConsentToggles; diff --git a/clients/privacy-center/components/consent/NoticeDrivenConsent.tsx b/clients/privacy-center/components/consent/NoticeDrivenConsent.tsx new file mode 100644 index 0000000000..8dd233a707 --- /dev/null +++ b/clients/privacy-center/components/consent/NoticeDrivenConsent.tsx @@ -0,0 +1,209 @@ +import { Divider, Stack, useToast } from "@fidesui/react"; +import React, { useEffect, useMemo, useState } from "react"; +import { + ConsentContext, + CookieKeyConsent, + getConsentContext, + getOrMakeFidesCookie, + saveFidesCookie, +} from "fides-js"; +import { useAppSelector } from "~/app/hooks"; +import { + selectCurrentConsentPreferences, + selectUserRegion, + selectPrivacyExperience, + useUpdatePrivacyPreferencesMutation, +} from "~/features/consent/consent.slice"; +import { + getGpcStatusFromNotice, + transformUserPreferenceToBoolean, +} from "~/features/consent/helpers"; + +import { + ConsentMethod, + ConsentOptionCreate, + PrivacyNoticeResponseWithUserPreferences, + PrivacyPreferencesRequest, + UserConsentPreference, +} from "~/types/api"; +import { useRouter } from "next/router"; +import { inspectForBrowserIdentities } from "~/common/browser-identities"; +import { NoticeHistoryIdToPreference } from "~/features/consent/types"; +import { ErrorToastOptions, SuccessToastOptions } from "~/common/toast-options"; +import { useLocalStorage } from "~/common/hooks"; +import ConsentItem from "./ConsentItem"; +import SaveCancel from "./SaveCancel"; + +const resolveConsentValue = ( + notice: PrivacyNoticeResponseWithUserPreferences, + context: ConsentContext +) => { + const gpcEnabled = + !!notice.has_gpc_flag && context.globalPrivacyControl === true; + if (gpcEnabled) { + return UserConsentPreference.OPT_OUT; + } + return notice.default_preference; +}; + +const NoticeDrivenConsent = () => { + const router = useRouter(); + const toast = useToast(); + const [consentRequestId] = useLocalStorage("consentRequestId", ""); + const [verificationCode] = useLocalStorage("verificationCode", ""); + const consentContext = useMemo(() => getConsentContext(), []); + const experience = useAppSelector(selectPrivacyExperience); + const serverPreferences = useAppSelector(selectCurrentConsentPreferences); + const cookie = getOrMakeFidesCookie(); + const { fides_user_device_id: fidesUserDeviceId } = cookie.identity; + const [updatePrivacyPreferencesMutationTrigger] = + useUpdatePrivacyPreferencesMutation(); + const region = useAppSelector(selectUserRegion); + + const initialDraftPreferences = useMemo(() => { + const newPreferences = { ...serverPreferences }; + Object.entries(serverPreferences).forEach(([key, value]) => { + if (!value) { + const notices = experience?.privacy_notices ?? []; + const notice = notices.filter( + (n) => n.privacy_notice_history_id === key + )[0]; + const defaultValue = notice + ? resolveConsentValue(notice, consentContext) + : UserConsentPreference.OPT_OUT; + newPreferences[key] = defaultValue; + } + }); + return newPreferences; + }, [serverPreferences, experience, consentContext]); + + const [draftPreferences, setDraftPreferences] = + useState(initialDraftPreferences); + + useEffect(() => { + setDraftPreferences(initialDraftPreferences); + }, [initialDraftPreferences]); + + const items = useMemo(() => { + if (!experience) { + return []; + } + const { privacy_notices: notices } = experience; + if (!notices || notices.length === 0) { + return []; + } + + return notices.map((notice) => { + const preference = draftPreferences[notice.privacy_notice_history_id]; + const value = transformUserPreferenceToBoolean(preference); + const gpcStatus = getGpcStatusFromNotice({ + value, + notice, + consentContext, + }); + + return { + name: notice.name || "", + description: notice.description || "", + id: notice.id, + historyId: notice.privacy_notice_history_id, + highlight: false, + url: undefined, + value, + gpcStatus, + }; + }); + }, [consentContext, experience, draftPreferences]); + + const handleCancel = () => { + router.push("/"); + }; + + const handleSave = async () => { + const browserIdentities = inspectForBrowserIdentities(); + const deviceIdentity = { fides_user_device_id: fidesUserDeviceId }; + const identities = browserIdentities + ? { ...deviceIdentity, ...browserIdentities } + : deviceIdentity; + + const preferences: ConsentOptionCreate[] = Object.entries( + draftPreferences + ).map(([key, value]) => ({ + privacy_notice_history_id: key, + preference: value ?? UserConsentPreference.OPT_OUT, + })); + + const payload: PrivacyPreferencesRequest = { + browser_identity: identities, + preferences, + user_geography: region, + privacy_experience_history_id: experience?.privacy_experience_history_id, + method: ConsentMethod.BUTTON, + code: verificationCode, + }; + + const result = await updatePrivacyPreferencesMutationTrigger({ + id: consentRequestId, + body: payload, + }); + if ("error" in result) { + toast({ + title: "An error occurred while saving user consent preferences", + description: result.error, + ...ErrorToastOptions, + }); + return; + } + const noticeKeyMap = new Map( + result.data.map((preference) => [ + preference.privacy_notice_history.notice_key || "", + transformUserPreferenceToBoolean(preference.preference), + ]) + ); + const consentCookieKey: CookieKeyConsent = Object.fromEntries(noticeKeyMap); + toast({ + title: "Your consent preferences have been saved", + ...SuccessToastOptions, + }); + // Save the cookie and window obj on success + window.Fides.consent = consentCookieKey; + const updatedCookie = { ...cookie, consent: consentCookieKey }; + saveFidesCookie(updatedCookie); + router.push("/"); + }; + + return ( + + {items.map((item, index) => { + const { id, highlight, url, name, description, historyId } = item; + const handleChange = (value: boolean) => { + const pref = value + ? UserConsentPreference.OPT_IN + : UserConsentPreference.OPT_OUT; + setDraftPreferences({ + ...draftPreferences, + ...{ [historyId]: pref }, + }); + }; + return ( + + {index > 0 ? : null} + + + ); + })} + + + ); +}; + +export default NoticeDrivenConsent; diff --git a/clients/privacy-center/components/consent/SaveCancel.tsx b/clients/privacy-center/components/consent/SaveCancel.tsx new file mode 100644 index 0000000000..9f4aebf424 --- /dev/null +++ b/clients/privacy-center/components/consent/SaveCancel.tsx @@ -0,0 +1,28 @@ +import { Button, Stack } from "@fidesui/react"; + +const SaveCancel = ({ + onSave, + onCancel, +}: { + onSave: () => void; + onCancel: () => void; +}) => ( + + + + +); + +export default SaveCancel; diff --git a/clients/privacy-center/cypress/e2e/consent-notices.cy.ts b/clients/privacy-center/cypress/e2e/consent-notices.cy.ts new file mode 100644 index 0000000000..d9509159e6 --- /dev/null +++ b/clients/privacy-center/cypress/e2e/consent-notices.cy.ts @@ -0,0 +1,191 @@ +import { + ConsentOptionCreate, + PrivacyNoticeResponseWithUserPreferences, +} from "~/types/api"; +import { CONSENT_COOKIE_NAME, FidesCookie } from "fides-js"; +import { API_URL } from "../support/constants"; + +const VERIFICATION_CODE = "112358"; +const PRIVACY_NOTICE_ID_1 = "pri_b4360591-3cc7-400d-a5ff-a9f095ab3061"; +const PRIVACY_NOTICE_ID_2 = "pri_b558ab1f-5367-4f0d-94b1-ec06a81ae821"; +const GEOLOCATION_API_URL = "https://www.example.com/location"; +const SETTINGS = { + IS_OVERLAY_DISABLED: false, + IS_GEOLOCATION_ENABLED: true, + GEOLOCATION_API_URL, +}; + +describe("Privacy notice driven consent", () => { + beforeEach(() => { + // Seed local storage with verification data + cy.window().then((win) => { + win.localStorage.setItem( + "consentRequestId", + JSON.stringify("consent-request-id") + ); + win.localStorage.setItem( + "verificationCode", + JSON.stringify(VERIFICATION_CODE) + ); + }); + + // Intercept sending identity data to the backend to access /consent page + cy.intercept( + "POST", + `${API_URL}/consent-request/consent-request-id/verify`, + { fixture: "consent/verify" } + ).as("postConsentRequestVerify"); + + // Location intercept + cy.intercept("GET", GEOLOCATION_API_URL, { + fixture: "consent/geolocation.json", + }).as("getGeolocation"); + + // Experience intercept + cy.intercept("GET", `${API_URL}/privacy-experience/*`, { + fixture: "consent/experience.json", + }).as("getExperience"); + + // Patch privacy preference intercept + cy.intercept( + "PATCH", + `${API_URL}/consent-request/consent-request-id/privacy-preferences*`, + { + fixture: "consent/privacy_preferences.json", + } + ).as("patchPrivacyPreference"); + }); + + describe("when user has not consented before", () => { + beforeEach(() => { + cy.visit("/consent"); + cy.getByTestId("consent"); + cy.overrideSettings(SETTINGS); + }); + + it("populates its header from the experience config", () => { + cy.wait("@getExperience"); + cy.getByTestId("consent-heading").contains("Privacy notice driven"); + cy.getByTestId("consent-description").contains( + "Manage all of your notices here." + ); + }); + + it("renders from privacy notices when there is no initial data", () => { + cy.wait("@getExperience").then((interception) => { + const { url } = interception.request; + expect(url).contains("fides_user_device_id"); + expect(url).contains("region=us_ca"); + }); + // Opt in, so should default to not checked + cy.getByTestId(`consent-item-${PRIVACY_NOTICE_ID_1}`).within(() => { + cy.getRadio().should("not.be.checked"); + }); + // Opt out, so should default to checked + cy.getByTestId(`consent-item-${PRIVACY_NOTICE_ID_2}`).within(() => { + cy.getRadio().should("be.checked"); + }); + + // Opt in to the opt in notice + cy.getByTestId(`consent-item-${PRIVACY_NOTICE_ID_1}`).within(() => { + cy.getRadio().should("not.be.checked").check({ force: true }); + cy.getRadio().should("be.checked"); + }); + + cy.getByTestId("save-btn").click(); + cy.wait("@patchPrivacyPreference").then((interception) => { + const { body } = interception.request; + const { preferences, code, method } = body; + expect(method).to.eql("button"); + expect(code).to.eql(VERIFICATION_CODE); + expect( + preferences.map((p: ConsentOptionCreate) => p.preference) + ).to.eql(["opt_in", "opt_in"]); + cy.waitUntilCookieExists(CONSENT_COOKIE_NAME).then(() => { + cy.getCookie(CONSENT_COOKIE_NAME).then((cookieJson) => { + const cookie = JSON.parse( + decodeURIComponent(cookieJson!.value) + ) as FidesCookie; + expect(body.browser_identity.fides_user_device_id).to.eql( + cookie.identity.fides_user_device_id + ); + const expectedConsent = { data_sales: true, advertising: true }; + const { consent } = cookie; + expect(consent).to.eql(expectedConsent); + cy.window().then((win) => { + expect(win.Fides.consent).to.eql(expectedConsent); + }); + }); + }); + }); + }); + + it("uses the device id found in an already existing cookie", () => { + const uuid = "4fbb6edf-34f6-4717-a6f1-541fd1e5d585"; + const now = "2023-04-28T12:00:00.000Z"; + const cookie = { + identity: { fides_user_device_id: uuid }, + fides_meta: { version: "0.9.0", createdAt: now }, + consent: {}, + }; + cy.setCookie(CONSENT_COOKIE_NAME, JSON.stringify(cookie)); + + cy.wait("@getExperience").then((interception) => { + const { url } = interception.request; + expect(url).contains(`fides_user_device_id=${uuid}`); + }); + // Make sure the same uuid propagates to the backend and to the updated cookie + cy.getByTestId("save-btn").click(); + cy.wait("@patchPrivacyPreference").then((interception) => { + const { body } = interception.request; + cy.getCookie(CONSENT_COOKIE_NAME).then((cookieJson) => { + const savedCookie = JSON.parse( + decodeURIComponent(cookieJson!.value) + ) as FidesCookie; + expect(body.browser_identity.fides_user_device_id).to.eql( + savedCookie.identity.fides_user_device_id + ); + }); + }); + }); + }); + + describe("when user has consented before", () => { + it("renders from privacy notices when user has consented before", () => { + cy.fixture("consent/experience.json").then((experience) => { + const newExperience = { ...experience }; + const notices = newExperience.items[0].privacy_notices; + newExperience.items[0].privacy_notices = notices.map( + (notice: PrivacyNoticeResponseWithUserPreferences) => ({ + ...notice, + ...{ current_preference: "opt_in" }, + }) + ); + cy.intercept("GET", `${API_URL}/privacy-experience/*`, { + body: newExperience, + }).as("getExperienceWithConsentHistory"); + }); + // Visit the consent page with notices enabled + cy.visit("/consent"); + cy.getByTestId("consent"); + cy.overrideSettings(SETTINGS); + // Both notices should be checked + cy.wait("@getExperienceWithConsentHistory"); + cy.getByTestId(`consent-item-${PRIVACY_NOTICE_ID_1}`).within(() => { + cy.getRadio().should("be.checked"); + }); + cy.getByTestId(`consent-item-${PRIVACY_NOTICE_ID_2}`).within(() => { + cy.getRadio().should("be.checked"); + }); + + cy.getByTestId("save-btn").click(); + cy.wait("@patchPrivacyPreference").then((interception) => { + const { body } = interception.request; + const { preferences } = body; + expect( + preferences.map((p: ConsentOptionCreate) => p.preference) + ).to.eql(["opt_in", "opt_in"]); + }); + }); + }); +}); diff --git a/clients/privacy-center/cypress/e2e/consent.cy.ts b/clients/privacy-center/cypress/e2e/consent.cy.ts index 79e9136b74..c5d3dd6466 100644 --- a/clients/privacy-center/cypress/e2e/consent.cy.ts +++ b/clients/privacy-center/cypress/e2e/consent.cy.ts @@ -154,26 +154,35 @@ describe("Consent settings", () => { cy.visit("/consent"); cy.getByTestId("consent"); cy.loadConfigFixture("config/config_consent.json").as("config"); + cy.overrideSettings({ IS_OVERLAY_DISABLED: true }); + }); + + it("populates its header and description from config", () => { + cy.getByTestId("consent-heading").contains("Manage your consent"); + cy.getByTestId("consent-description").contains( + "Test your consent preferences" + ); + cy.getByTestId("consent-description").contains("When you use our"); }); it("lets the user update their consent", () => { cy.getByTestId("consent"); - cy.getByTestId(`consent-item-card-advertising.first_party`).within(() => { + cy.getByTestId(`consent-item-advertising.first_party`).within(() => { cy.contains("Test advertising.first_party"); cy.getRadio().should("not.be.checked"); }); - cy.getByTestId(`consent-item-card-improve`).within(() => { + cy.getByTestId(`consent-item-improve`).within(() => { cy.getRadio().should("be.checked"); }); // Without GPC, this defaults to true. - cy.getByTestId(`consent-item-card-collect.gpc`).within(() => { + cy.getByTestId(`consent-item-collect.gpc`).within(() => { cy.getRadio().should("be.checked"); }); // Consent to an item that was opted-out. - cy.getByTestId(`consent-item-card-advertising`).within(() => { + cy.getByTestId(`consent-item-advertising`).within(() => { cy.getRadio().should("not.be.checked").check({ force: true }); }); cy.getByTestId("save-btn").click(); @@ -214,9 +223,6 @@ describe("Consent settings", () => { cy.setCookie("_ga", gaCookieValue); cy.setCookie("ljt_readerID", sovrnCookieValue); - cy.visit("/consent"); - cy.getByTestId("consent"); - cy.getByTestId("save-btn").click(); cy.wait("@patchConsentPreferences").then((interception) => { @@ -229,10 +235,10 @@ describe("Consent settings", () => { it("reflects their choices using fides.js", () => { // Opt-out of items default to opt-in. - cy.getByTestId(`consent-item-card-advertising`).within(() => { + cy.getByTestId(`consent-item-advertising`).within(() => { cy.getRadio("false").check({ force: true }); }); - cy.getByTestId(`consent-item-card-improve`).within(() => { + cy.getByTestId(`consent-item-improve`).within(() => { cy.getRadio("false").check({ force: true }); }); cy.getByTestId("save-btn").click(); @@ -279,11 +285,12 @@ describe("Consent settings", () => { cy.visit("/consent?globalPrivacyControl=true"); cy.getByTestId("consent"); cy.loadConfigFixture("config/config_consent.json").as("config"); + cy.overrideSettings({ IS_OVERLAY_DISABLED: true }); }); it("applies the GPC defaults", () => { cy.getByTestId("gpc-banner"); - cy.getByTestId(`consent-item-card-collect.gpc`).within(() => { + cy.getByTestId(`consent-item-collect.gpc`).within(() => { cy.contains("GPC test"); cy.getRadio().should("not.be.checked"); cy.getByTestId("gpc-badge").should("contain", GpcStatus.APPLIED); @@ -306,7 +313,7 @@ describe("Consent settings", () => { it("lets the user consent to override GPC", () => { cy.getByTestId("gpc-banner"); - cy.getByTestId(`consent-item-card-collect.gpc`).within(() => { + cy.getByTestId(`consent-item-collect.gpc`).within(() => { cy.contains("GPC test"); cy.getRadio().should("not.be.checked").check({ force: true }); cy.getByTestId("gpc-badge").should("contain", GpcStatus.OVERRIDDEN); diff --git a/clients/privacy-center/cypress/fixtures/config/config_consent.json b/clients/privacy-center/cypress/fixtures/config/config_consent.json index 48fdf2381e..55cb29b2c9 100644 --- a/clients/privacy-center/cypress/fixtures/config/config_consent.json +++ b/clients/privacy-center/cypress/fixtures/config/config_consent.json @@ -55,6 +55,9 @@ } ], "description": "Test your consent preferences, including defaults, cookie keys, and GPC signals.", + "description_subtext": [ + "When you use our services, you're trusting us with your information. We understand this is a big responsibility and work hard to protect your information and put you in control." + ], "policy_key": "default_consent_policy", "title": "Manage your consent" } diff --git a/clients/privacy-center/cypress/fixtures/consent/experience.json b/clients/privacy-center/cypress/fixtures/consent/experience.json new file mode 100644 index 0000000000..aa01173da3 --- /dev/null +++ b/clients/privacy-center/cypress/fixtures/consent/experience.json @@ -0,0 +1,88 @@ +{ + "items": [ + { + "disabled": false, + "component": "privacy_center", + "delivery_mechanism": "link", + "region": "us_ca", + "experience_config": { + "acknowledgement_button_label": "ok", + "banner_description": null, + "banner_title": "Manage preferences", + "component": "privacy_center", + "component_description": "Manage all of your notices here.", + "component_title": "Privacy notice driven", + "confirmation_button_label": "sounds good", + "created_at": "2023-05-23T15:52:59.598163+00:00", + "delivery_mechanism": "link", + "disabled": false, + "experience_config_history_id": "pri_1eb390e7-06c3-45ee-8028-8fe3ec284ebe", + "id": "pri_e379836a-3155-425b-a350-ccb70dd771d6", + "is_default": false, + "link_label": "Manage preferences", + "regions": ["eu_fr", "eu_it"], + "reject_button_label": "nah", + "updated_at": "2023-05-23T15:52:59.598163+00:00", + "version": 1.0 + }, + "id": "pri_26937b89-493b-41a7-b8f8-7aaed3845620", + "created_at": "2023-05-23T22:34:33.716828+00:00", + "updated_at": "2023-05-23T22:34:33.716828+00:00", + "version": 1.0, + "privacy_experience_history_id": "pri_4dece382-11c6-4302-8d69-e3d8edfe483e", + "privacy_notices": [ + { + "name": "Advertising", + "notice_key": "advertising", + "description": "Ensures you are correctly notifying the user about your advertising practices and for appropriate locations, collecting the users consent preferences.", + "internal_description": null, + "origin": null, + "regions": ["eu_ie", "eu_fr", "us_ca"], + "consent_mechanism": "opt_in", + "data_uses": ["advertising.first_party.contextual"], + "enforcement_level": "system_wide", + "disabled": false, + "has_gpc_flag": false, + "displayed_in_privacy_center": true, + "displayed_in_overlay": true, + "displayed_in_api": false, + "id": "pri_b4360591-3cc7-400d-a5ff-a9f095ab3061", + "created_at": "2023-05-23T22:34:33.689913+00:00", + "updated_at": "2023-05-24T20:11:14.064534+00:00", + "version": 3.0, + "privacy_notice_history_id": "pri_07ace6f0-211d-4822-8f02-b9573b19b8d6", + "default_preference": "opt_out", + "current_preference": null, + "outdated_preference": null + }, + { + "name": "Data Sales", + "notice_key": "data_sales", + "description": "Provide opt-out consent for the use of data in ways that may be considered “data sales” under US state privacy regulations.", + "internal_description": null, + "origin": null, + "regions": ["us_ca", "us_co"], + "consent_mechanism": "opt_out", + "data_uses": ["advertising.third_party.personalized"], + "enforcement_level": "frontend", + "disabled": false, + "has_gpc_flag": false, + "displayed_in_privacy_center": true, + "displayed_in_overlay": false, + "displayed_in_api": false, + "id": "pri_b558ab1f-5367-4f0d-94b1-ec06a81ae821", + "created_at": "2023-05-23T22:34:33.692850+00:00", + "updated_at": "2023-05-23T22:34:33.692850+00:00", + "version": 1.0, + "privacy_notice_history_id": "pri_371e6355-9327-4294-8b42-c0db5bf53cb0", + "default_preference": "opt_in", + "current_preference": null, + "outdated_preference": null + } + ] + } + ], + "total": 1, + "page": 1, + "size": 50 +} diff --git a/clients/privacy-center/cypress/fixtures/consent/geolocation.json b/clients/privacy-center/cypress/fixtures/consent/geolocation.json new file mode 100644 index 0000000000..10a9712b6a --- /dev/null +++ b/clients/privacy-center/cypress/fixtures/consent/geolocation.json @@ -0,0 +1,6 @@ +{ + "country": "US", + "ip": "199.999.999.999:99999", + "location": "US-CA", + "region": "CA" +} diff --git a/clients/privacy-center/cypress/fixtures/consent/privacy_preferences.json b/clients/privacy-center/cypress/fixtures/consent/privacy_preferences.json new file mode 100644 index 0000000000..9067594641 --- /dev/null +++ b/clients/privacy-center/cypress/fixtures/consent/privacy_preferences.json @@ -0,0 +1,50 @@ +[ + { + "id": "cur_8dd7a394-0808-4578-a10b-732ca3482299", + "preference": "opt_in", + "privacy_notice_history": { + "name": "Data Sales", + "notice_key": "data_sales", + "description": "Provide opt-out consent for the use of data in ways that may be considered “data sales” under US state privacy regulations.", + "internal_description": null, + "origin": null, + "regions": ["us_ca", "us_co"], + "consent_mechanism": "opt_out", + "data_uses": ["advertising.third_party.personalized"], + "enforcement_level": "frontend", + "disabled": false, + "has_gpc_flag": false, + "displayed_in_privacy_center": true, + "displayed_in_overlay": false, + "displayed_in_api": false, + "id": "pri_371e6355-9327-4294-8b42-c0db5bf53cb0", + "version": 1.0, + "privacy_notice_id": "pri_b558ab1f-5367-4f0d-94b1-ec06a81ae821" + }, + "privacy_preference_history_id": "pri_e1f47400-f285-438e-8cf2-fabec67787be" + }, + { + "id": "cur_91597784-a836-4013-9288-b98294d6a3c9", + "preference": "opt_in", + "privacy_notice_history": { + "name": "Advertising", + "notice_key": "advertising", + "description": "Ensures you are correctly notifying the user about your advertising practices and for appropriate locations, collecting the users consent preferences.", + "internal_description": null, + "origin": null, + "regions": ["eu_ie", "eu_fr", "us_ca"], + "consent_mechanism": "opt_in", + "data_uses": ["advertising.first_party.contextual"], + "enforcement_level": "system_wide", + "disabled": false, + "has_gpc_flag": false, + "displayed_in_privacy_center": true, + "displayed_in_overlay": true, + "displayed_in_api": false, + "id": "pri_07ace6f0-211d-4822-8f02-b9573b19b8d6", + "version": 3.0, + "privacy_notice_id": "pri_b4360591-3cc7-400d-a5ff-a9f095ab3061" + }, + "privacy_preference_history_id": "pri_9c496ef2-0953-48d5-975d-8ee29b7d723a" + } +] diff --git a/clients/privacy-center/cypress/support/commands.ts b/clients/privacy-center/cypress/support/commands.ts index bd59f775d7..239b2d10c1 100644 --- a/clients/privacy-center/cypress/support/commands.ts +++ b/clients/privacy-center/cypress/support/commands.ts @@ -4,6 +4,7 @@ import "cypress-wait-until"; import type { AppDispatch } from "~/app/store"; import type { FidesConfig } from "fides-js"; +import type { PrivacyCenterClientSettings } from "~/app/server-environment"; Cypress.Commands.add("getByTestId", (selector, ...args) => cy.get(`[data-testid='${selector}']`, ...args) @@ -36,6 +37,12 @@ Cypress.Commands.add("loadConfigFixture", (fixtureName: string, ...args) => { }); }); +Cypress.Commands.add("overrideSettings", (settings) => { + cy.dispatch({ type: "settings/overrideSettings", payload: settings }).then( + () => settings + ); +}); + Cypress.Commands.add("visitConsentDemo", (options?: FidesConfig) => { cy.visit("/fides-js-components-demo.html", { onBeforeLoad: (win) => { @@ -115,6 +122,17 @@ declare global { * @example cy.visitConsentDemo(fidesConfig); */ visitConsentDemo(options?: FidesConfig): Chainable; + /** + * Custom command to load a Privacy Center settings object into the app + * + * Warning: similar to loadConfigFixture, subsequent page loads will reset this setting + * back to the defaults. + * + * @example cy.overrideSettings({IS_OVERLAY_DISABLED: false}) + */ + overrideSettings( + settings: Partial + ): Chainable; } } } diff --git a/clients/privacy-center/features/common/api.slice.ts b/clients/privacy-center/features/common/api.slice.ts index d143edebee..19fe2eac1b 100644 --- a/clients/privacy-center/features/common/api.slice.ts +++ b/clients/privacy-center/features/common/api.slice.ts @@ -32,6 +32,6 @@ const dynamicBaseQuery: BaseQueryFn = async (args, api, extraOptions) => { export const baseApi = createApi({ reducerPath: "baseApi", baseQuery: dynamicBaseQuery, - tagTypes: [], + tagTypes: ["Privacy Experience"], endpoints: () => ({}), }); diff --git a/clients/privacy-center/features/common/settings.slice.ts b/clients/privacy-center/features/common/settings.slice.ts index d681469607..3ce7c4c8ce 100644 --- a/clients/privacy-center/features/common/settings.slice.ts +++ b/clients/privacy-center/features/common/settings.slice.ts @@ -1,4 +1,4 @@ -import { createSlice, PayloadAction } from "@reduxjs/toolkit"; +import { createSelector, createSlice, PayloadAction } from "@reduxjs/toolkit"; import { useAppSelector } from "~/app/hooks"; import { PrivacyCenterClientSettings } from "~/app/server-environment"; @@ -26,6 +26,17 @@ export const settingsSlice = createSlice({ } draftState.settings = payload; }, + /** + * Override existing settings with passed in values + * + * Used for tests + */ + overrideSettings( + draftState, + { payload }: PayloadAction + ) { + draftState.settings = { ...draftState.settings, ...payload }; + }, }, }); @@ -39,3 +50,7 @@ export const useSettings = (): PrivacyCenterClientSettings => { } return settings; }; +export const selectIsNoticeDriven = createSelector( + selectSettings, + (settings) => settings.settings?.IS_OVERLAY_DISABLED === false +); diff --git a/clients/privacy-center/features/consent/consent.slice.ts b/clients/privacy-center/features/consent/consent.slice.ts index e455b37480..a8b045e544 100644 --- a/clients/privacy-center/features/consent/consent.slice.ts +++ b/clients/privacy-center/features/consent/consent.slice.ts @@ -1,15 +1,21 @@ import { createSelector, createSlice, PayloadAction } from "@reduxjs/toolkit"; +import { constructFidesRegionString, UserGeolocation } from "fides-js"; import type { RootState } from "~/app/store"; import { VerificationType } from "~/components/modals/types"; import { baseApi } from "~/features/common/api.slice"; import { + ComponentType, ConsentPreferences, ConsentPreferencesWithVerificationCode, + CurrentPrivacyPreferenceSchema, + Page_PrivacyExperienceResponse_, + PrivacyNoticeRegion, + PrivacyPreferencesRequest, } from "~/types/api"; -import { ConfigConsentOption } from "~/types/config"; +import { selectSettings } from "../common/settings.slice"; -import { FidesKeyToConsent } from "./types"; +import { FidesKeyToConsent, NoticeHistoryIdToPreference } from "./types"; export const consentApi = baseApi.injectEndpoints({ endpoints: (build) => ({ @@ -36,7 +42,11 @@ export const consentApi = baseApi.injectEndpoints({ >({ query: ({ id }) => `${VerificationType.ConsentRequest}/${id}/preferences`, }), - updateConsentRequestPreferences: build.mutation< + /** + * This endpoint is deprecated in favor of + * /consent-request/{id}/privacy-preferences + * */ + updateConsentRequestPreferencesDeprecated: build.mutation< ConsentPreferences, { id: string; body: ConsentPreferencesWithVerificationCode } >({ @@ -47,13 +57,48 @@ export const consentApi = baseApi.injectEndpoints({ credentials: "include", }), }), + getPrivacyExperience: build.query< + Page_PrivacyExperienceResponse_, + { region: PrivacyNoticeRegion; fides_user_device_id?: string } + >({ + query: (payload) => ({ + url: "privacy-experience/", + params: { + component: ComponentType.PRIVACY_CENTER, + has_notices: true, + show_disabled: false, + ...payload, + }, + }), + providesTags: ["Privacy Experience"], + }), + updatePrivacyPreferences: build.mutation< + CurrentPrivacyPreferenceSchema[], + { id: string; body: PrivacyPreferencesRequest } + >({ + query: ({ id, body }) => ({ + url: `${VerificationType.ConsentRequest}/${id}/privacy-preferences`, + method: "PATCH", + body, + }), + invalidatesTags: ["Privacy Experience"], + }), + getUserGeolocation: build.query({ + query: (url) => ({ + url, + method: "GET", + }), + }), }), }); export const { usePostConsentRequestVerificationMutation, useLazyGetConsentRequestPreferencesQuery, - useUpdateConsentRequestPreferencesMutation, + useUpdateConsentRequestPreferencesDeprecatedMutation, + useGetPrivacyExperienceQuery, + useUpdatePrivacyPreferencesMutation, + useGetUserGeolocationQuery, } = consentApi; type State = { @@ -61,11 +106,14 @@ type State = { fidesKeyToConsent: FidesKeyToConsent; /** The consent choices stored on the server (returned by the most recent API call). */ persistedFidesKeyToConsent: FidesKeyToConsent; + /** User id based on the device */ + fidesUserDeviceId: string | undefined; }; const initialState: State = { fidesKeyToConsent: {}, persistedFidesKeyToConsent: {}, + fidesUserDeviceId: undefined, }; export const consentSlice = createSlice({ @@ -75,10 +123,10 @@ export const consentSlice = createSlice({ changeConsent( draftState, { - payload: { option, value }, - }: PayloadAction<{ option: ConfigConsentOption; value: boolean }> + payload: { key, value }, + }: PayloadAction<{ key: string; value: boolean }> ) { - draftState.fidesKeyToConsent[option.fidesDataUseKey] = value; + draftState.fidesKeyToConsent[key] = value; }, /** @@ -100,12 +148,19 @@ export const consentSlice = createSlice({ consent.opt_in; }); }, + + setFidesUserDeviceId(draftState, { payload }: PayloadAction) { + draftState.fidesUserDeviceId = payload; + }, }, }); export const { reducer } = consentSlice; -export const { changeConsent, updateUserConsentPreferencesFromApi } = - consentSlice.actions; +export const { + changeConsent, + updateUserConsentPreferencesFromApi, + setFidesUserDeviceId, +} = consentSlice.actions; export const selectConsentState = (state: RootState) => state.consent; @@ -118,3 +173,55 @@ export const selectPersistedFidesKeyToConsent = createSelector( selectConsentState, (state) => state.persistedFidesKeyToConsent ); + +// Privacy experience +export const selectFidesUserDeviceId = createSelector( + selectConsentState, + (state) => state.fidesUserDeviceId +); + +export const selectUserRegion = createSelector( + [(RootState) => RootState, selectSettings], + (RootState, settingsState) => { + const { settings } = settingsState; + if (settings?.IS_GEOLOCATION_ENABLED && settings?.GEOLOCATION_API_URL) { + const geolocation = consentApi.endpoints.getUserGeolocation.select( + settings.GEOLOCATION_API_URL + )(RootState)?.data; + return constructFidesRegionString(geolocation) as PrivacyNoticeRegion; + } + return undefined; + } +); + +export const selectPrivacyExperience = createSelector( + [(RootState) => RootState, selectUserRegion, selectFidesUserDeviceId], + (RootState, region, deviceId) => { + if (!region) { + return undefined; + } + return consentApi.endpoints.getPrivacyExperience.select({ + region, + fides_user_device_id: deviceId, + })(RootState)?.data?.items[0]; + } +); + +const emptyConsentPreferences: NoticeHistoryIdToPreference = {}; +export const selectCurrentConsentPreferences = createSelector( + selectPrivacyExperience, + (experience) => { + if ( + !experience || + !experience.privacy_notices || + !experience.privacy_notices.length + ) { + return emptyConsentPreferences; + } + const preferences: NoticeHistoryIdToPreference = {}; + experience.privacy_notices.forEach((notice) => { + preferences[notice.privacy_notice_history_id] = notice.current_preference; + }); + return preferences; + } +); diff --git a/clients/privacy-center/features/consent/helpers.ts b/clients/privacy-center/features/consent/helpers.ts index d306677fea..7ead36d8b3 100644 --- a/clients/privacy-center/features/consent/helpers.ts +++ b/clients/privacy-center/features/consent/helpers.ts @@ -9,6 +9,10 @@ import { LegacyConsentConfig, ConsentConfig, } from "~/types/config"; +import { + PrivacyNoticeResponseWithUserPreferences, + UserConsentPreference, +} from "~/types/api"; import { FidesKeyToConsent, GpcStatus } from "./types"; /** @@ -98,3 +102,45 @@ export const getGpcStatus = ({ return GpcStatus.OVERRIDDEN; }; + +export const getGpcStatusFromNotice = ({ + value, + notice, + consentContext, +}: { + value: boolean; + notice: PrivacyNoticeResponseWithUserPreferences; + consentContext: ConsentContext; +}) => { + // If GPC is not enabled, it won't be applied at all. + if (!consentContext.globalPrivacyControl || !notice.has_gpc_flag) { + return GpcStatus.NONE; + } + + if (!value) { + return GpcStatus.APPLIED; + } + + return GpcStatus.OVERRIDDEN; +}; + +/** + * Convert a user consent preference into true/false + */ +export const transformUserPreferenceToBoolean = ( + preference: UserConsentPreference | undefined +) => { + if (!preference) { + return false; + } + if (preference === UserConsentPreference.OPT_OUT) { + return false; + } + if (preference === UserConsentPreference.OPT_IN) { + return true; + } + if (preference === UserConsentPreference.ACKNOWLEDGE) { + return true; + } + return false; +}; diff --git a/clients/privacy-center/features/consent/hooks.ts b/clients/privacy-center/features/consent/hooks.ts new file mode 100644 index 0000000000..3f7e5f32fd --- /dev/null +++ b/clients/privacy-center/features/consent/hooks.ts @@ -0,0 +1,56 @@ +import { getOrMakeFidesCookie } from "fides-js"; +import { useEffect } from "react"; +import { useAppDispatch, useAppSelector } from "~/app/hooks"; +import { PrivacyNoticeRegion } from "~/types/api"; +import { + selectIsNoticeDriven, + useSettings, +} from "~/features/common/settings.slice"; +import { + useGetPrivacyExperienceQuery, + setFidesUserDeviceId, + useGetUserGeolocationQuery, + selectUserRegion, +} from "./consent.slice"; + +/** + * Subscribes to the relevant privacy experience. + * + * 1. Queries for the user's geolocation using geolocation settings + * 2. Gets the device ID from the cookie + * 3. Queries for the experience, which requires both location and device ID. + * By calling this hook, the selector for experiences should then be populated. + * + * const experience = useAppSelector(selectPrivacyExperience); + * + * Skips the subscription if notices are not enabled via settings or if + * there is no region available. + */ +export const useSubscribeToPrivacyExperienceQuery = () => { + const dispatch = useAppDispatch(); + const { IS_GEOLOCATION_ENABLED, GEOLOCATION_API_URL } = useSettings(); + const cookie = getOrMakeFidesCookie(); + const { fides_user_device_id: fidesUserDeviceId } = cookie.identity; + + const skipFetchExperience = !useAppSelector(selectIsNoticeDriven); + const skipFetchGeolocation = + skipFetchExperience || !IS_GEOLOCATION_ENABLED || !GEOLOCATION_API_URL; + + useGetUserGeolocationQuery(GEOLOCATION_API_URL, { + skip: skipFetchGeolocation, + }); + + useEffect(() => { + dispatch(setFidesUserDeviceId(fidesUserDeviceId)); + }, [dispatch, fidesUserDeviceId]); + + const region = useAppSelector(selectUserRegion); + const params = { + // Casting should be safe because we skip in the hook below if region does not exist + region: region as PrivacyNoticeRegion, + fides_user_device_id: fidesUserDeviceId, + }; + useGetPrivacyExperienceQuery(params, { + skip: !region || skipFetchExperience, + }); +}; diff --git a/clients/privacy-center/features/consent/types.ts b/clients/privacy-center/features/consent/types.ts index cec957aaf1..37df806718 100644 --- a/clients/privacy-center/features/consent/types.ts +++ b/clients/privacy-center/features/consent/types.ts @@ -1,7 +1,13 @@ +import { UserConsentPreference } from "~/types/api"; + export type FidesKeyToConsent = { [fidesKey: string]: boolean | undefined; }; +export type NoticeHistoryIdToPreference = { + [historyId: string]: UserConsentPreference | undefined; +}; + export enum GpcStatus { /** GPC is not relevant for the consent option. */ NONE = "none", diff --git a/clients/privacy-center/pages/consent.tsx b/clients/privacy-center/pages/consent.tsx index 16f6eb097b..e571ec3861 100644 --- a/clients/privacy-center/pages/consent.tsx +++ b/clients/privacy-center/pages/consent.tsx @@ -1,11 +1,4 @@ -import { - Button, - Divider, - Heading, - Stack, - Text, - useToast, -} from "@fidesui/react"; +import { Stack, useToast } from "@fidesui/react"; import type { NextPage } from "next"; import { useRouter } from "next/router"; import React, { useCallback, useEffect, useMemo } from "react"; @@ -13,32 +6,32 @@ import React, { useCallback, useEffect, useMemo } from "react"; import { FidesCookie, getConsentContext, - resolveConsentValue, saveFidesCookie, getOrMakeFidesCookie, } from "fides-js"; import { useAppDispatch, useAppSelector } from "~/app/hooks"; -import { inspectForBrowserIdentities } from "~/common/browser-identities"; + import { useLocalStorage } from "~/common/hooks"; -import { ErrorToastOptions, SuccessToastOptions } from "~/common/toast-options"; -import ConsentItemCard from "~/components/ConsentItemCard"; +import { ErrorToastOptions } from "~/common/toast-options"; import { updateConsentOptionsFromApi, useConfig, } from "~/features/common/config.slice"; import { - selectFidesKeyToConsent, selectPersistedFidesKeyToConsent, updateUserConsentPreferencesFromApi, useLazyGetConsentRequestPreferencesQuery, usePostConsentRequestVerificationMutation, - useUpdateConsentRequestPreferencesMutation, } from "~/features/consent/consent.slice"; -import { getGpcStatus, makeCookieKeyConsent } from "~/features/consent/helpers"; +import { makeCookieKeyConsent } from "~/features/consent/helpers"; import { useGetIdVerificationConfigQuery } from "~/features/id-verification"; import { ConsentPreferences } from "~/types/api"; import { GpcBanner } from "~/features/consent/GpcMessages"; -import { GpcStatus } from "~/features/consent/types"; +import ConsentToggles from "~/components/consent/ConsentToggles"; +import { useSubscribeToPrivacyExperienceQuery } from "~/features/consent/hooks"; +import ConsentHeading from "~/components/consent/ConsentHeading"; +import ConsentDescription from "~/components/consent/ConsentDescription"; +import { selectIsNoticeDriven } from "~/features/common/settings.slice"; const Consent: NextPage = () => { const [consentRequestId] = useLocalStorage("consentRequestId", ""); @@ -46,7 +39,6 @@ const Consent: NextPage = () => { const router = useRouter(); const toast = useToast(); const dispatch = useAppDispatch(); - const fidesKeyToConsent = useAppSelector(selectFidesKeyToConsent); const persistedFidesKeyToConsent = useAppSelector( selectPersistedFidesKeyToConsent ); @@ -55,6 +47,7 @@ const Consent: NextPage = () => { () => config.consent?.page.consentOptions ?? [], [config] ); + useSubscribeToPrivacyExperienceQuery(); const getIdVerificationConfigQueryResult = useGetIdVerificationConfigQuery(); const [ @@ -65,10 +58,7 @@ const Consent: NextPage = () => { getConsentRequestPreferencesQueryTrigger, getConsentRequestPreferencesQueryResult, ] = useLazyGetConsentRequestPreferencesQuery(); - const [ - updateConsentRequestPreferencesMutationTrigger, - updateConsentRequestPreferencesMutationResult, - ] = useUpdateConsentRequestPreferencesMutation(); + const isNoticeDriven = useAppSelector(selectIsNoticeDriven); const consentContext = useMemo(() => getConsentContext(), []); @@ -108,16 +98,27 @@ const Consent: NextPage = () => { /** * The consent cookie is updated only when the "persisted" consent preferences are updated. This * ensures the browser's behavior matches what the server expects. + * + * Notice driven consent does not need to set a new consent object */ useEffect(() => { const cookie: FidesCookie = getOrMakeFidesCookie(); - const newConsent = makeCookieKeyConsent({ - consentOptions, - fidesKeyToConsent: persistedFidesKeyToConsent, - consentContext, - }); - saveFidesCookie({ ...cookie, consent: newConsent }); - }, [consentOptions, persistedFidesKeyToConsent, consentContext]); + if (isNoticeDriven) { + saveFidesCookie(cookie); + } else { + const newConsent = makeCookieKeyConsent({ + consentOptions, + fidesKeyToConsent: persistedFidesKeyToConsent, + consentContext, + }); + saveFidesCookie({ ...cookie, consent: newConsent }); + } + }, [ + consentOptions, + persistedFidesKeyToConsent, + consentContext, + isNoticeDriven, + ]); /** * When the Id verification method is known, trigger the request that will @@ -215,177 +216,15 @@ const Consent: NextPage = () => { redirectToIndex, ]); - /** - * Update the consent choices on the backend. - */ - const saveUserConsentOptions = useCallback(() => { - const consent = consentOptions.map((option) => { - const defaultValue = resolveConsentValue(option.default, consentContext); - const value = fidesKeyToConsent[option.fidesDataUseKey] ?? defaultValue; - const gpcStatus = getGpcStatus({ - value, - consentOption: option, - consentContext, - }); - - return { - data_use: option.fidesDataUseKey, - data_use_description: option.description, - opt_in: value, - has_gpc_flag: gpcStatus !== GpcStatus.NONE, - conflicts_with_gpc: gpcStatus === GpcStatus.OVERRIDDEN, - }; - }); - - const executableOptions = consentOptions.map((option) => ({ - data_use: option.fidesDataUseKey, - executable: option.executable ?? false, - })); - - const browserIdentity = inspectForBrowserIdentities(); - - updateConsentRequestPreferencesMutationTrigger({ - id: consentRequestId, - body: { - code: verificationCode, - policy_key: config.consent?.page.policy_key, - consent, - executable_options: executableOptions, - browser_identity: browserIdentity, - }, - }); - }, [ - config, - consentContext, - consentOptions, - consentRequestId, - fidesKeyToConsent, - updateConsentRequestPreferencesMutationTrigger, - verificationCode, - ]); - - /** - * Handle consent update result. - */ - useEffect(() => { - if (updateConsentRequestPreferencesMutationResult.isError) { - toastError({ - title: "An error occurred while saving user consent preferences", - error: updateConsentRequestPreferencesMutationResult.error, - }); - return; - } - - if (updateConsentRequestPreferencesMutationResult.isSuccess) { - storeConsentPreferences( - updateConsentRequestPreferencesMutationResult.data - ); - toast({ - title: "Your consent preferences have been saved", - ...SuccessToastOptions, - }); - redirectToIndex(); - } - }, [ - updateConsentRequestPreferencesMutationResult, - storeConsentPreferences, - toastError, - toast, - redirectToIndex, - ]); - - const items = useMemo( - () => - consentOptions.map((option) => { - const defaultValue = resolveConsentValue( - option.default, - consentContext - ); - const value = fidesKeyToConsent[option.fidesDataUseKey] ?? defaultValue; - const gpcStatus = getGpcStatus({ - value, - consentOption: option, - consentContext, - }); - - return { - option, - value, - gpcStatus, - }; - }), - [consentContext, consentOptions, fidesKeyToConsent] - ); - return ( - - {config.consent?.page.title} - - - {config.consent?.page.description_subtext?.map((paragraph, index) => ( - - {paragraph} - - ))} + + - {consentContext.globalPrivacyControl ? : null} - - - {items.map((item, index) => ( - - {index > 0 ? : null} - - - ))} - - - - - - + ); diff --git a/clients/privacy-center/pages/index.tsx b/clients/privacy-center/pages/index.tsx index 88160be927..1bf0bed675 100644 --- a/clients/privacy-center/pages/index.tsx +++ b/clients/privacy-center/pages/index.tsx @@ -13,7 +13,7 @@ import { } from "~/components/modals/consent-request-modal/ConsentRequestModal"; import { useGetIdVerificationConfigQuery } from "~/features/id-verification"; import PrivacyCard from "~/components/PrivacyCard"; -import ConsentCard from "~/components/ConsentCard"; +import ConsentCard from "~/components/consent/ConsentCard"; import { useConfig } from "~/features/common/config.slice"; const Home: NextPage = () => { diff --git a/clients/privacy-center/types/api/index.ts b/clients/privacy-center/types/api/index.ts index 9367b1a8e2..5b6bd960b0 100644 --- a/clients/privacy-center/types/api/index.ts +++ b/clients/privacy-center/types/api/index.ts @@ -2,9 +2,25 @@ /* tslint:disable */ /* eslint-disable */ +export { ComponentType } from "./models/ComponentType"; export type { Consent } from "./models/Consent"; +export { ConsentMechanism } from "./models/ConsentMechanism"; +export { ConsentMethod } from "./models/ConsentMethod"; +export type { ConsentOptionCreate } from "./models/ConsentOptionCreate"; export type { ConsentPreferences } from "./models/ConsentPreferences"; export type { ConsentPreferencesWithVerificationCode } from "./models/ConsentPreferencesWithVerificationCode"; export type { ConsentWithExecutableStatus } from "./models/ConsentWithExecutableStatus"; +export type { CurrentPrivacyPreferenceSchema } from "./models/CurrentPrivacyPreferenceSchema"; +export { DeliveryMechanism } from "./models/DeliveryMechanism"; +export { EnforcementLevel } from "./models/EnforcementLevel"; +export type { ExperienceConfigResponse } from "./models/ExperienceConfigResponse"; export type { Identity } from "./models/Identity"; export type { IdentityVerificationConfigResponse } from "./models/IdentityVerificationConfigResponse"; +export type { Page_PrivacyExperienceResponse_ } from "./models/Page_PrivacyExperienceResponse_"; +export type { PrivacyExperienceResponse } from "./models/PrivacyExperienceResponse"; +export type { PrivacyNoticeHistorySchema } from "./models/PrivacyNoticeHistorySchema"; +export { PrivacyNoticeRegion } from "./models/PrivacyNoticeRegion"; +export type { PrivacyNoticeResponse } from "./models/PrivacyNoticeResponse"; +export type { PrivacyNoticeResponseWithUserPreferences } from "./models/PrivacyNoticeResponseWithUserPreferences"; +export type { PrivacyPreferencesRequest } from "./models/PrivacyPreferencesRequest"; +export { UserConsentPreference } from "./models/UserConsentPreference"; diff --git a/clients/privacy-center/types/api/models/ComponentType.ts b/clients/privacy-center/types/api/models/ComponentType.ts new file mode 100644 index 0000000000..9a6a1b4c87 --- /dev/null +++ b/clients/privacy-center/types/api/models/ComponentType.ts @@ -0,0 +1,11 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * The component type - not formalized in the db + */ +export enum ComponentType { + OVERLAY = "overlay", + PRIVACY_CENTER = "privacy_center", +} diff --git a/clients/privacy-center/types/api/models/ConsentMechanism.ts b/clients/privacy-center/types/api/models/ConsentMechanism.ts new file mode 100644 index 0000000000..41e6c301ef --- /dev/null +++ b/clients/privacy-center/types/api/models/ConsentMechanism.ts @@ -0,0 +1,12 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * An enumeration. + */ +export enum ConsentMechanism { + OPT_IN = "opt_in", + OPT_OUT = "opt_out", + NOTICE_ONLY = "notice_only", +} diff --git a/clients/privacy-center/types/api/models/ConsentMethod.ts b/clients/privacy-center/types/api/models/ConsentMethod.ts new file mode 100644 index 0000000000..951ebc7a4e --- /dev/null +++ b/clients/privacy-center/types/api/models/ConsentMethod.ts @@ -0,0 +1,12 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * An enumeration. + */ +export enum ConsentMethod { + BUTTON = "button", + GPC = "gpc", + API = "api", +} diff --git a/clients/privacy-center/types/api/models/ConsentOptionCreate.ts b/clients/privacy-center/types/api/models/ConsentOptionCreate.ts new file mode 100644 index 0000000000..d79b860804 --- /dev/null +++ b/clients/privacy-center/types/api/models/ConsentOptionCreate.ts @@ -0,0 +1,13 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { UserConsentPreference } from "./UserConsentPreference"; + +/** + * Schema for saving the user's preference for a given notice + */ +export type ConsentOptionCreate = { + privacy_notice_history_id: string; + preference: UserConsentPreference; +}; diff --git a/clients/privacy-center/types/api/models/CurrentPrivacyPreferenceSchema.ts b/clients/privacy-center/types/api/models/CurrentPrivacyPreferenceSchema.ts new file mode 100644 index 0000000000..9bffa55fd3 --- /dev/null +++ b/clients/privacy-center/types/api/models/CurrentPrivacyPreferenceSchema.ts @@ -0,0 +1,18 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { PrivacyNoticeHistorySchema } from "./PrivacyNoticeHistorySchema"; +import type { UserConsentPreference } from "./UserConsentPreference"; + +/** + * Schema to represent the latest saved preference for a given privacy notice + * Note that we return the privacy notice *history* record here though which has the + * contents of the notice the user consented to at the time. + */ +export type CurrentPrivacyPreferenceSchema = { + id: string; + preference: UserConsentPreference; + privacy_notice_history: PrivacyNoticeHistorySchema; + privacy_preference_history_id: string; +}; diff --git a/clients/privacy-center/types/api/models/DeliveryMechanism.ts b/clients/privacy-center/types/api/models/DeliveryMechanism.ts new file mode 100644 index 0000000000..99b24cab60 --- /dev/null +++ b/clients/privacy-center/types/api/models/DeliveryMechanism.ts @@ -0,0 +1,11 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * The delivery mechanism - not formalized in the db + */ +export enum DeliveryMechanism { + BANNER = "banner", + LINK = "link", +} diff --git a/clients/privacy-center/types/api/models/EnforcementLevel.ts b/clients/privacy-center/types/api/models/EnforcementLevel.ts new file mode 100644 index 0000000000..74cdcb0fbb --- /dev/null +++ b/clients/privacy-center/types/api/models/EnforcementLevel.ts @@ -0,0 +1,12 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * Enum is not formalized in the DB because it may be subject to frequent change + */ +export enum EnforcementLevel { + FRONTEND = "frontend", + SYSTEM_WIDE = "system_wide", + NOT_APPLICABLE = "not_applicable", +} diff --git a/clients/privacy-center/types/api/models/ExperienceConfigResponse.ts b/clients/privacy-center/types/api/models/ExperienceConfigResponse.ts new file mode 100644 index 0000000000..532b71881f --- /dev/null +++ b/clients/privacy-center/types/api/models/ExperienceConfigResponse.ts @@ -0,0 +1,31 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ComponentType } from "./ComponentType"; +import type { DeliveryMechanism } from "./DeliveryMechanism"; +import type { PrivacyNoticeRegion } from "./PrivacyNoticeRegion"; + +/** + * An API representation of ExperienceConfig used for response payloads + */ +export type ExperienceConfigResponse = { + acknowledgement_button_label?: string; + banner_title?: string; + banner_description?: string; + component?: ComponentType; + component_title?: string; + component_description?: string; + confirmation_button_label?: string; + delivery_mechanism?: DeliveryMechanism; + disabled?: boolean; + is_default?: boolean; + link_label?: string; + reject_button_label?: string; + id: string; + experience_config_history_id: string; + version: number; + created_at: string; + updated_at: string; + regions: Array; +}; diff --git a/clients/privacy-center/types/api/models/Identity.ts b/clients/privacy-center/types/api/models/Identity.ts index 8bb2ae6dd1..09a22a0f1e 100644 --- a/clients/privacy-center/types/api/models/Identity.ts +++ b/clients/privacy-center/types/api/models/Identity.ts @@ -10,4 +10,5 @@ export type Identity = { email?: string; ga_client_id?: string; ljt_readerID?: string; + fides_user_device_id?: string; }; diff --git a/clients/privacy-center/types/api/models/Page_PrivacyExperienceResponse_.ts b/clients/privacy-center/types/api/models/Page_PrivacyExperienceResponse_.ts new file mode 100644 index 0000000000..42fdad2450 --- /dev/null +++ b/clients/privacy-center/types/api/models/Page_PrivacyExperienceResponse_.ts @@ -0,0 +1,12 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { PrivacyExperienceResponse } from "./PrivacyExperienceResponse"; + +export type Page_PrivacyExperienceResponse_ = { + items: Array; + total: number; + page: number; + size: number; +}; diff --git a/clients/privacy-center/types/api/models/PrivacyExperienceResponse.ts b/clients/privacy-center/types/api/models/PrivacyExperienceResponse.ts new file mode 100644 index 0000000000..b6890df7dc --- /dev/null +++ b/clients/privacy-center/types/api/models/PrivacyExperienceResponse.ts @@ -0,0 +1,26 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ComponentType } from "./ComponentType"; +import type { DeliveryMechanism } from "./DeliveryMechanism"; +import type { ExperienceConfigResponse } from "./ExperienceConfigResponse"; +import type { PrivacyNoticeRegion } from "./PrivacyNoticeRegion"; +import type { PrivacyNoticeResponseWithUserPreferences } from "./PrivacyNoticeResponseWithUserPreferences"; + +/** + * An API representation of a PrivacyExperience used for response payloads + */ +export type PrivacyExperienceResponse = { + disabled?: boolean; + component?: ComponentType; + delivery_mechanism?: DeliveryMechanism; + region: PrivacyNoticeRegion; + experience_config?: ExperienceConfigResponse; + id: string; + created_at: string; + updated_at: string; + version: number; + privacy_experience_history_id: string; + privacy_notices?: Array; +}; diff --git a/clients/privacy-center/types/api/models/PrivacyNoticeHistorySchema.ts b/clients/privacy-center/types/api/models/PrivacyNoticeHistorySchema.ts new file mode 100644 index 0000000000..35770cda23 --- /dev/null +++ b/clients/privacy-center/types/api/models/PrivacyNoticeHistorySchema.ts @@ -0,0 +1,30 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ConsentMechanism } from "./ConsentMechanism"; +import type { EnforcementLevel } from "./EnforcementLevel"; +import type { PrivacyNoticeRegion } from "./PrivacyNoticeRegion"; + +/** + * An API representation of a PrivacyNoticeHistory used for response payloads + */ +export type PrivacyNoticeHistorySchema = { + name: string; + notice_key?: string; + description?: string; + internal_description?: string; + origin?: string; + regions: Array; + consent_mechanism: ConsentMechanism; + data_uses: Array; + enforcement_level: EnforcementLevel; + disabled?: boolean; + has_gpc_flag?: boolean; + displayed_in_privacy_center?: boolean; + displayed_in_overlay?: boolean; + displayed_in_api?: boolean; + id: string; + version: number; + privacy_notice_id: string; +}; diff --git a/clients/privacy-center/types/api/models/PrivacyNoticeRegion.ts b/clients/privacy-center/types/api/models/PrivacyNoticeRegion.ts new file mode 100644 index 0000000000..7acdd0b6ea --- /dev/null +++ b/clients/privacy-center/types/api/models/PrivacyNoticeRegion.ts @@ -0,0 +1,86 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * Enum is not formalized in the DB because it is subject to frequent change + */ +export enum PrivacyNoticeRegion { + US_AL = "us_al", + US_AK = "us_ak", + US_AZ = "us_az", + US_AR = "us_ar", + US_CA = "us_ca", + US_CO = "us_co", + US_CT = "us_ct", + US_DE = "us_de", + US_FL = "us_fl", + US_GA = "us_ga", + US_HI = "us_hi", + US_ID = "us_id", + US_IL = "us_il", + US_IN = "us_in", + US_IA = "us_ia", + US_KS = "us_ks", + US_KY = "us_ky", + US_LA = "us_la", + US_ME = "us_me", + US_MD = "us_md", + US_MA = "us_ma", + US_MI = "us_mi", + US_MN = "us_mn", + US_MS = "us_ms", + US_MO = "us_mo", + US_MT = "us_mt", + US_NE = "us_ne", + US_NV = "us_nv", + US_NH = "us_nh", + US_NJ = "us_nj", + US_NM = "us_nm", + US_NY = "us_ny", + US_NC = "us_nc", + US_ND = "us_nd", + US_OH = "us_oh", + US_OK = "us_ok", + US_OR = "us_or", + US_PA = "us_pa", + US_RI = "us_ri", + US_SC = "us_sc", + US_SD = "us_sd", + US_TN = "us_tn", + US_TX = "us_tx", + US_UT = "us_ut", + US_VT = "us_vt", + US_VA = "us_va", + US_WA = "us_wa", + US_WV = "us_wv", + US_WI = "us_wi", + US_WY = "us_wy", + EU_BE = "eu_be", + EU_BG = "eu_bg", + EU_CZ = "eu_cz", + EU_DK = "eu_dk", + EU_DE = "eu_de", + EU_EE = "eu_ee", + EU_IE = "eu_ie", + EU_EL = "eu_el", + EU_ES = "eu_es", + EU_FR = "eu_fr", + EU_HR = "eu_hr", + EU_IT = "eu_it", + EU_CY = "eu_cy", + EU_LV = "eu_lv", + EU_LT = "eu_lt", + EU_LU = "eu_lu", + EU_HU = "eu_hu", + EU_MT = "eu_mt", + EU_NL = "eu_nl", + EU_AT = "eu_at", + EU_PL = "eu_pl", + EU_PT = "eu_pt", + EU_RO = "eu_ro", + EU_SI = "eu_si", + EU_SK = "eu_sk", + EU_FI = "eu_fi", + EU_SE = "eu_se", +} diff --git a/clients/privacy-center/types/api/models/PrivacyNoticeResponse.ts b/clients/privacy-center/types/api/models/PrivacyNoticeResponse.ts new file mode 100644 index 0000000000..8f33d8b38b --- /dev/null +++ b/clients/privacy-center/types/api/models/PrivacyNoticeResponse.ts @@ -0,0 +1,31 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ConsentMechanism } from "./ConsentMechanism"; +import type { EnforcementLevel } from "./EnforcementLevel"; +import type { PrivacyNoticeRegion } from "./PrivacyNoticeRegion"; + +/** + * An API representation of a PrivacyNotice used for response payloads + */ +export type PrivacyNoticeResponse = { + name?: string; + description?: string; + internal_description?: string; + origin?: string; + regions?: Array; + consent_mechanism?: ConsentMechanism; + data_uses?: Array; + enforcement_level?: EnforcementLevel; + disabled?: boolean; + has_gpc_flag?: boolean; + displayed_in_privacy_center?: boolean; + displayed_in_overlay?: boolean; + displayed_in_api?: boolean; + id: string; + created_at: string; + updated_at: string; + version: number; + privacy_notice_history_id: string; +}; diff --git a/clients/privacy-center/types/api/models/PrivacyNoticeResponseWithUserPreferences.ts b/clients/privacy-center/types/api/models/PrivacyNoticeResponseWithUserPreferences.ts new file mode 100644 index 0000000000..82f78dc9d0 --- /dev/null +++ b/clients/privacy-center/types/api/models/PrivacyNoticeResponseWithUserPreferences.ts @@ -0,0 +1,37 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ConsentMechanism } from "./ConsentMechanism"; +import type { EnforcementLevel } from "./EnforcementLevel"; +import type { PrivacyNoticeRegion } from "./PrivacyNoticeRegion"; +import type { UserConsentPreference } from "./UserConsentPreference"; + +/** + * If retrieving notices for a given user, also return the default preferences for that notice + * and any saved preferences. + */ +export type PrivacyNoticeResponseWithUserPreferences = { + name?: string; + notice_key?: string; + description?: string; + internal_description?: string; + origin?: string; + regions?: Array; + consent_mechanism?: ConsentMechanism; + data_uses?: Array; + enforcement_level?: EnforcementLevel; + disabled?: boolean; + has_gpc_flag?: boolean; + displayed_in_privacy_center?: boolean; + displayed_in_overlay?: boolean; + displayed_in_api?: boolean; + id: string; + created_at: string; + updated_at: string; + version: number; + privacy_notice_history_id: string; + default_preference: UserConsentPreference; + current_preference?: UserConsentPreference; + outdated_preference?: UserConsentPreference; +}; diff --git a/clients/privacy-center/types/api/models/PrivacyPreferencesRequest.ts b/clients/privacy-center/types/api/models/PrivacyPreferencesRequest.ts new file mode 100644 index 0000000000..a907e20baa --- /dev/null +++ b/clients/privacy-center/types/api/models/PrivacyPreferencesRequest.ts @@ -0,0 +1,21 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ConsentMethod } from "./ConsentMethod"; +import type { ConsentOptionCreate } from "./ConsentOptionCreate"; +import type { Identity } from "./Identity"; +import type { PrivacyNoticeRegion } from "./PrivacyNoticeRegion"; + +/** + * Request body for creating PrivacyPreferences. + */ +export type PrivacyPreferencesRequest = { + browser_identity: Identity; + code?: string; + preferences: Array; + policy_key?: string; + privacy_experience_history_id?: string; + user_geography?: PrivacyNoticeRegion; + method?: ConsentMethod; +}; diff --git a/clients/privacy-center/types/api/models/UserConsentPreference.ts b/clients/privacy-center/types/api/models/UserConsentPreference.ts new file mode 100644 index 0000000000..7e22ce512d --- /dev/null +++ b/clients/privacy-center/types/api/models/UserConsentPreference.ts @@ -0,0 +1,12 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * An enumeration. + */ +export enum UserConsentPreference { + OPT_IN = "opt_in", + OPT_OUT = "opt_out", + ACKNOWLEDGE = "acknowledge", +} From ca140f506f2ce35e535c1c55524e6e094b5b04eb Mon Sep 17 00:00:00 2001 From: Catherine Smith Date: Wed, 31 May 2023 14:29:58 -0400 Subject: [PATCH 02/24] 3159 Add calls to Fides API from fides-js (#3361) --- CHANGELOG.md | 1 + clients/fides-js/__tests__/lib/cookie.test.ts | 12 +- .../fides-js/src/components/ConsentModal.tsx | 16 +- .../fides-js/src/components/NoticeToggles.tsx | 24 +- clients/fides-js/src/components/Overlay.tsx | 82 +++- clients/fides-js/src/components/Toggle.tsx | 2 +- clients/fides-js/src/fides.ts | 213 ++++++---- clients/fides-js/src/lib/consent-config.ts | 11 - clients/fides-js/src/lib/consent-types.ts | 75 +++- clients/fides-js/src/lib/consent-utils.ts | 82 +++- clients/fides-js/src/lib/consent-value.ts | 44 +- clients/fides-js/src/lib/consent.tsx | 8 +- clients/fides-js/src/lib/cookie.ts | 78 +++- clients/fides-js/src/lib/preferences.ts | 68 ++- .../src/services/external/geolocation.ts | 17 +- clients/fides-js/src/services/fides/api.ts | 104 ++++- .../privacy-center/app/server-environment.ts | 11 +- .../consent/ConfigDrivenConsent.tsx | 9 +- .../cypress/e2e/consent-banner.cy.ts | 396 ++++++++++++++---- .../privacy-center/cypress/e2e/consent.cy.ts | 131 +++--- .../fixtures/consent/privacy-experience.json | 82 ++++ .../fixtures/consent/test_banner_options.json | 12 +- .../cypress/support/commands.ts | 25 ++ .../features/consent/helpers.ts | 7 +- clients/privacy-center/pages/api/fides-js.ts | 1 + .../public/fides-js-components-demo.html | 69 +-- .../privacy-center/public/fides-js-demo.html | 63 +-- 27 files changed, 1164 insertions(+), 479 deletions(-) delete mode 100644 clients/fides-js/src/lib/consent-config.ts create mode 100644 clients/privacy-center/cypress/fixtures/consent/privacy-experience.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 7403ed66e5..8eb69e83a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,7 @@ The types of changes are: - Add ability for `fides-js` to fetch its own geolocation [#3356](https://github.com/ethyca/fides/pull/3356) - Add ability to select different locations in the "Cookie House" sample app [#3362](https://github.com/ethyca/fides/pull/3362) - Added optional logging of resource changes on the server [#3331](https://github.com/ethyca/fides/pull/3331) +- Add ability for `fides-js` to make API calls to Fides [#3361](https://github.com/ethyca/fides/pull/3361) ### Fixed diff --git a/clients/fides-js/__tests__/lib/cookie.test.ts b/clients/fides-js/__tests__/lib/cookie.test.ts index eb9755ee30..d54be77e43 100644 --- a/clients/fides-js/__tests__/lib/cookie.test.ts +++ b/clients/fides-js/__tests__/lib/cookie.test.ts @@ -3,12 +3,12 @@ import { CookieKeyConsent, FidesCookie, getOrMakeFidesCookie, - makeConsentDefaults, + makeConsentDefaultsLegacy, makeFidesCookie, saveFidesCookie, } from "../../src/lib/cookie"; -import type { ConsentConfig } from "../../src/lib/consent-config"; import type { ConsentContext } from "../../src/lib/consent-context"; +import { LegacyConsentConfig } from "~/lib/consent-types"; // Setup mock date const MOCK_DATE = "2023-01-01T12:00:00.000Z"; @@ -171,8 +171,8 @@ describe("saveFidesCookie", () => { }); }); -describe("makeConsentDefaults", () => { - const config: ConsentConfig = { +describe("makeConsentDefaultsLegacy", () => { + const config: LegacyConsentConfig = { options: [ { cookieKeys: ["default_undefined"], @@ -205,7 +205,7 @@ describe("makeConsentDefaults", () => { const context: ConsentContext = {}; it("returns the default consent values by key", () => { - expect(makeConsentDefaults({ config, context })).toEqual({ + expect(makeConsentDefaultsLegacy(config, context, false)).toEqual({ default_true: true, default_false: false, default_true_with_gpc_false: true, @@ -220,7 +220,7 @@ describe("makeConsentDefaults", () => { }; it("returns the default consent values by key", () => { - expect(makeConsentDefaults({ config, context })).toEqual({ + expect(makeConsentDefaultsLegacy(config, context, false)).toEqual({ default_true: true, default_false: false, default_true_with_gpc_false: false, diff --git a/clients/fides-js/src/components/ConsentModal.tsx b/clients/fides-js/src/components/ConsentModal.tsx index 43fed7d04d..65f138db84 100644 --- a/clients/fides-js/src/components/ConsentModal.tsx +++ b/clients/fides-js/src/components/ConsentModal.tsx @@ -26,11 +26,11 @@ const ConsentModal = ({ experience: ExperienceConfig; notices: PrivacyNotice[]; onClose: () => void; - onSave: (enabledNoticeIds: Array) => void; + onSave: (enabledNoticeKeys: Array) => void; onAcceptAll: () => void; onRejectAll: () => void; }) => { - const initialEnabledNoticeIds = useMemo( + const initialEnabledNoticeKeys = useMemo( () => Object.keys(window.Fides.consent).filter( (key) => window.Fides.consent[key] @@ -38,12 +38,12 @@ const ConsentModal = ({ [] ); - const [enabledNoticeIds, setEnabledNoticeIds] = useState< - Array - >(initialEnabledNoticeIds); + const [enabledNoticeKeys, setEnabledNoticeKeys] = useState< + Array + >(initialEnabledNoticeKeys); const handleSave = () => { - onSave(enabledNoticeIds); + onSave(enabledNoticeKeys); onClose(); }; @@ -75,8 +75,8 @@ const ConsentModal = ({
diff --git a/clients/fides-js/src/components/NoticeToggles.tsx b/clients/fides-js/src/components/NoticeToggles.tsx index 6679734256..8cd474bc8d 100644 --- a/clients/fides-js/src/components/NoticeToggles.tsx +++ b/clients/fides-js/src/components/NoticeToggles.tsx @@ -13,7 +13,7 @@ const NoticeToggle = ({ }: { notice: PrivacyNotice; checked: boolean; - onToggle: (noticeId: PrivacyNotice["id"]) => void; + onToggle: (noticeKey: PrivacyNotice["notice_key"]) => void; }) => { const { isOpen, @@ -21,7 +21,7 @@ const NoticeToggle = ({ getDisclosureProps, onToggle: toggleDescription, } = useDisclosure({ - id: notice.id, + id: notice.notice_key, }); const handleKeyDown = (event: KeyboardEvent) => { @@ -36,7 +36,7 @@ const NoticeToggle = ({ isOpen ? "notice-toggle notice-toggle-expanded" : "notice-toggle" } > -
+
@@ -68,28 +68,28 @@ const NoticeToggle = ({ */ const NoticeToggles = ({ notices, - enabledNoticeIds, + enabledNoticeKeys, onChange, }: { notices: PrivacyNotice[]; - enabledNoticeIds: Array; - onChange: (ids: Array) => void; + enabledNoticeKeys: Array; + onChange: (keys: Array) => void; }) => { - const handleToggle = (noticeId: PrivacyNotice["id"]) => { + const handleToggle = (noticeKey: PrivacyNotice["notice_key"]) => { // Add the notice to list of enabled notices - if (enabledNoticeIds.indexOf(noticeId) === -1) { - onChange([...enabledNoticeIds, noticeId]); + if (enabledNoticeKeys.indexOf(noticeKey) === -1) { + onChange([...enabledNoticeKeys, noticeKey]); } // Remove the notice from the list of enabled notices else { - onChange(enabledNoticeIds.filter((n) => n !== noticeId)); + onChange(enabledNoticeKeys.filter((n) => n !== noticeKey)); } }; return (
{notices.map((notice, idx) => { - const checked = enabledNoticeIds.indexOf(notice.id) !== -1; + const checked = enabledNoticeKeys.indexOf(notice.notice_key) !== -1; const isLast = idx === notices.length - 1; return (
diff --git a/clients/fides-js/src/components/Overlay.tsx b/clients/fides-js/src/components/Overlay.tsx index 87fa291f53..79b106c454 100644 --- a/clients/fides-js/src/components/Overlay.tsx +++ b/clients/fides-js/src/components/Overlay.tsx @@ -1,25 +1,32 @@ -import { FunctionComponent, h } from "preact"; +import { h, FunctionComponent } from "preact"; import { useState } from "preact/hooks"; import { - PrivacyExperience, + ConsentMethod, FidesOptions, + PrivacyExperience, PrivacyNotice, - UserGeolocation, + SaveConsentPreference, } from "../lib/consent-types"; import ConsentBanner from "./ConsentBanner"; -import { CookieKeyConsent } from "../lib/cookie"; import ConsentModal from "./ConsentModal"; import { updateConsentPreferences } from "../lib/preferences"; +import { transformConsentToFidesUserPreference } from "../lib/consent-utils"; +import { FidesCookie } from "../lib/cookie"; export interface OverlayProps { - consentDefaults: CookieKeyConsent; options: FidesOptions; experience: PrivacyExperience; - geolocation?: UserGeolocation; + cookie: FidesCookie; + fidesRegionString: string; } -const Overlay: FunctionComponent = ({ experience }) => { +const Overlay: FunctionComponent = ({ + experience, + options, + fidesRegionString, + cookie, +}) => { const [modalIsOpen, setModalIsOpen] = useState(false); if (!experience.experience_config) { @@ -29,23 +36,70 @@ const Overlay: FunctionComponent = ({ experience }) => { const privacyNotices = experience.privacy_notices ?? []; const onAcceptAll = () => { - const allNoticeIds = privacyNotices.map((notice) => notice.id); + const consentPreferencesToSave = new Array(); + privacyNotices.forEach((notice) => { + consentPreferencesToSave.push( + new SaveConsentPreference( + notice.notice_key, + notice.privacy_notice_history_id, + transformConsentToFidesUserPreference(true, notice.consent_mechanism) + ) + ); + }); updateConsentPreferences({ - privacyNotices, - enabledPrivacyNoticeIds: allNoticeIds, + consentPreferencesToSave, + experienceHistoryId: experience.privacy_experience_history_id, + fidesApiUrl: options.fidesApiUrl, + consentMethod: ConsentMethod.button, + userLocationString: fidesRegionString, + cookie, }); }; const onRejectAll = () => { - updateConsentPreferences({ privacyNotices, enabledPrivacyNoticeIds: [] }); + const consentPreferencesToSave = new Array(); + privacyNotices.forEach((notice) => { + consentPreferencesToSave.push( + new SaveConsentPreference( + notice.notice_key, + notice.privacy_notice_history_id, + transformConsentToFidesUserPreference(false, notice.consent_mechanism) + ) + ); + }); + updateConsentPreferences({ + consentPreferencesToSave, + experienceHistoryId: experience.privacy_experience_history_id, + fidesApiUrl: options.fidesApiUrl, + consentMethod: ConsentMethod.button, + userLocationString: fidesRegionString, + cookie, + }); }; const onSavePreferences = ( - enabledPrivacyNoticeIds: Array + enabledPrivacyNoticeKeys: Array ) => { + const consentPreferencesToSave = new Array(); + privacyNotices.forEach((notice) => { + consentPreferencesToSave.push( + new SaveConsentPreference( + notice.notice_key, + notice.privacy_notice_history_id, + transformConsentToFidesUserPreference( + enabledPrivacyNoticeKeys.includes(notice.notice_key), + notice.consent_mechanism + ) + ) + ); + }); updateConsentPreferences({ - privacyNotices, - enabledPrivacyNoticeIds, + consentPreferencesToSave, + experienceHistoryId: experience.privacy_experience_history_id, + fidesApiUrl: options.fidesApiUrl, + consentMethod: ConsentMethod.button, + userLocationString: fidesRegionString, + cookie, }); }; diff --git a/clients/fides-js/src/components/Toggle.tsx b/clients/fides-js/src/components/Toggle.tsx index 826ac058db..99268a7399 100644 --- a/clients/fides-js/src/components/Toggle.tsx +++ b/clients/fides-js/src/components/Toggle.tsx @@ -10,7 +10,7 @@ const Toggle = ({ name: string; id: string; checked: boolean; - onChange: (noticeId: string) => void; + onChange: (noticeKey: string) => void; }) => { const labelId = `toggle-${id}`; return ( diff --git a/clients/fides-js/src/fides.ts b/clients/fides-js/src/fides.ts index 2882d9a44f..4cc2e5622a 100644 --- a/clients/fides-js/src/fides.ts +++ b/clients/fides-js/src/fides.ts @@ -53,7 +53,8 @@ import { CookieIdentity, CookieMeta, getOrMakeFidesCookie, - makeConsentDefaults, + makeConsentDefaultsLegacy, + buildCookieConsentForExperiences, } from "./lib/cookie"; import { PrivacyExperience, @@ -69,6 +70,7 @@ import { } from "./lib/consent-utils"; import { fetchExperience } from "./services/fides/api"; import { getGeolocation } from "./services/external/geolocation"; +import { OverlayProps } from "~/components/Overlay"; export type Fides = { consent: CookieKeyConsent; @@ -94,22 +96,69 @@ declare global { // eslint-disable-next-line no-underscore-dangle,@typescript-eslint/naming-convention let _Fides: Fides; +const retrieveEffectiveRegionString = async ( + geolocation: UserGeolocation | undefined, + options: FidesOptions +) => { + // Prefer the provided geolocation if available and valid; otherwise, fallback to automatically + // geolocating the user by calling the geolocation API + const fidesRegionString = constructFidesRegionString(geolocation); + if (!fidesRegionString) { + // we always need a region str so that we can PATCH privacy preferences to Fides Api + return constructFidesRegionString( + // Call the geolocation API + await getGeolocation( + options.isGeolocationEnabled, + options.geolocationApiUrl, + options.debug + ) + ); + } + return fidesRegionString; +}; + /** - * Determines effective geolocation + * Determines whether experience is valid and relevant notices exist within the experience */ -const retrieveEffectiveGeolocation = async ( +const experienceIsValid = ( + effectiveExperience: PrivacyExperience | undefined | null, options: FidesOptions -): Promise => { - // If geolocation is not enabled, return undefined - if (!options.isGeolocationEnabled) { +): boolean => { + if (!effectiveExperience) { + debugLog( + options.debug, + `No relevant experience found. Skipping overlay initialization.` + ); + return false; + } + if ( + !( + effectiveExperience.privacy_notices && + effectiveExperience.privacy_notices.length >= 0 + ) + ) { + debugLog( + options.debug, + `No relevant notices in the privacy experience. Skipping overlay initialization.`, + effectiveExperience + ); + return false; + } + if (effectiveExperience.component !== ComponentType.OVERLAY) { + debugLog( + options.debug, + "No experience found with overlay component. Skipping overlay initialization." + ); + return false; + } + if (!effectiveExperience.experience_config) { debugLog( options.debug, - `User location is required but could not be retrieved because geolocation is disabled.` + "No experience config found with for experience. Skipping overlay initialization." ); - return undefined; + return false; } - // Call the geolocation API - return getGeolocation(options.geolocationApiUrl, options.debug); + return true; }; /** @@ -121,112 +170,92 @@ const init = async ({ geolocation, options, }: FidesConfig) => { - // Configure the default consent values + // Configure the default legacy consent values const context = getConsentContext(); - const consentDefaults = makeConsentDefaults({ - config: consent, + const consentDefaults = makeConsentDefaultsLegacy( + consent, context, - }); + options.debug + ); // Load any existing user preferences from the browser cookie - const cookie = getOrMakeFidesCookie(consentDefaults); - - // Initialize the window.Fides object - _Fides.consent = cookie.consent; - _Fides.fides_meta = cookie.fides_meta; - _Fides.identity = cookie.identity; - _Fides.experience = experience; - _Fides.geolocation = geolocation; - _Fides.options = options; - _Fides.initialized = true; + const cookie = getOrMakeFidesCookie(consentDefaults, options.debug); - // TODO: generate device id if it doesn't exist + let shouldInitOverlay: boolean = !options.isOverlayDisabled; - debugLog( - options.debug, - "Validating Fides consent overlay options...", - options - ); if (!validateOptions(options)) { - debugLog(options.debug, "Invalid overlay options", options); - return; + debugLog( + options.debug, + "Invalid overlay options. Skipping overlay initialization.", + options + ); + shouldInitOverlay = false; } - let effectiveGeolocation = geolocation; - let effectiveExperience = experience; + const fidesRegionString = await retrieveEffectiveRegionString( + geolocation, + options + ); + let effectiveExperience: PrivacyExperience | undefined | null = experience; - if (!effectiveExperience || !constructFidesRegionString(geolocation)) { - // If experience is not provided, we need to retrieve it via the Fides API. - // In order to retrieve it, we first need a valid geolocation, which is either provided - // OR can be obtained via the Fides API - effectiveGeolocation = await retrieveEffectiveGeolocation(options); - const userLocationString = constructFidesRegionString(effectiveGeolocation); - if (!userLocationString) { - debugLog( - options.debug, - `User location could not be constructed from location params`, - effectiveGeolocation - ); - return; - } + if (!fidesRegionString) { + debugLog( + options.debug, + `User location could not be obtained. Skipping overlay initialization.` + ); + shouldInitOverlay = false; + } else if (!effectiveExperience) { effectiveExperience = await fetchExperience( - userLocationString, + fidesRegionString, + options.fidesApiUrl, + cookie.identity.fides_user_device_id, options.debug ); - if (!effectiveExperience) { - debugLog(options.debug, `No relevant experience found.`); - return; - } } - if ( - !effectiveExperience.privacy_notices || - effectiveExperience.privacy_notices.length === 0 - ) { - debugLog( - options.debug, - `No relevant notices in the privacy experience.`, - effectiveExperience + if (effectiveExperience && experienceIsValid(effectiveExperience, options)) { + // Overwrite cookie consent with experience-based consent values + cookie.consent = buildCookieConsentForExperiences( + effectiveExperience, + context, + options.debug ); - return; + + if (shouldInitOverlay) { + // Check if there are any notices within the experience that do not have a user preference + const noticesWithNoUserPreferenceExist: boolean = Boolean( + effectiveExperience?.privacy_notices?.some( + (notice) => notice.current_preference == null + ) + ); + if (noticesWithNoUserPreferenceExist) { + await initOverlay({ + experience: effectiveExperience, + fidesRegionString, + cookie, + options, + }).catch(() => {}); + } + } } + // Initialize the window.Fides object + _Fides.consent = cookie.consent; + _Fides.fides_meta = cookie.fides_meta; + _Fides.identity = cookie.identity; + _Fides.experience = experience; + _Fides.geolocation = geolocation; + _Fides.options = options; + _Fides.initialized = true; + if (getConsentContext().globalPrivacyControl) { - effectiveExperience.privacy_notices.forEach((notice) => { + effectiveExperience?.privacy_notices?.forEach((notice) => { if (notice.has_gpc_flag) { // todo- write cookie with user preference // todo- send consent request downstream automatically with saveUserPreference() } }); } - - if (options.isOverlayDisabled) { - debugLog( - options.debug, - "Fides consent overlay is disabled, skipping overlay initialization!" - ); - return; - } - if (effectiveExperience.component !== ComponentType.OVERLAY) { - debugLog( - options.debug, - "No experience found with overlay component, skipping overlay initialization!" - ); - return; - } - if (!effectiveExperience.experience_config) { - debugLog( - options.debug, - "No experience config found with for experience, skipping overlay initialization!" - ); - return; - } - await initOverlay({ - consentDefaults, - experience: effectiveExperience, - geolocation: effectiveGeolocation, - options, - }).catch(() => {}); }; // The global Fides object; this is bound to window.Fides if available @@ -241,6 +270,7 @@ _Fides = { geolocationApiUrl: "", overlayParentId: null, privacyCenterUrl: "", + fidesApiUrl: "", }, fides_meta: {}, identity: {}, @@ -259,7 +289,6 @@ if (typeof window !== "undefined") { // TODO: pretty sure we need ./services/* too? export * from "./lib/consent"; export * from "./components"; -export * from "./lib/consent-config"; export * from "./lib/consent-context"; export * from "./lib/consent-types"; export * from "./lib/consent-links"; diff --git a/clients/fides-js/src/lib/consent-config.ts b/clients/fides-js/src/lib/consent-config.ts deleted file mode 100644 index 71fa7598fb..0000000000 --- a/clients/fides-js/src/lib/consent-config.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ConsentValue } from "./consent-value"; - -export type ConsentOption = { - cookieKeys: string[]; - default?: ConsentValue; - fidesDataUseKey: string; -}; - -export type ConsentConfig = { - options: ConsentOption[]; -}; diff --git a/clients/fides-js/src/lib/consent-types.ts b/clients/fides-js/src/lib/consent-types.ts index 108cc49f55..a1bdb80668 100644 --- a/clients/fides-js/src/lib/consent-types.ts +++ b/clients/fides-js/src/lib/consent-types.ts @@ -1,10 +1,8 @@ -import { ConsentConfig } from "./consent-config"; - export const FIDES_MODAL_LINK = "fides-consent-modal-link"; export interface FidesConfig { // Set the consent defaults from a "legacy" Privacy Center config.json. - consent?: ConsentConfig; + consent?: LegacyConsentConfig; // 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. @@ -33,8 +31,29 @@ export type FidesOptions = { // URL for the Privacy Center, used to customize consent preferences. Required. privacyCenterUrl: string; + + // URL for the Fides API, used to fetch and save consent preferences. Required. + fidesApiUrl: string; }; +export class SaveConsentPreference { + consentPreference: UserConsentPreference; + + noticeHistoryId: string; + + noticeKey: string; + + constructor( + noticeKey: string, + noticeHistoryId: string, + consentPreference: UserConsentPreference + ) { + this.noticeKey = noticeKey; + this.noticeHistoryId = noticeHistoryId; + this.consentPreference = consentPreference; + } +} + export type PrivacyExperience = { disabled?: boolean; component?: ComponentType; @@ -90,8 +109,9 @@ export type PrivacyNotice = { version: number; privacy_notice_history_id: string; default_preference: UserConsentPreference; - current_preference?: UserConsentPreference; - outdated_preference?: UserConsentPreference; + current_preference?: UserConsentPreference | null; + outdated_preference?: UserConsentPreference | null; + notice_key: string; }; export enum EnforcementLevel { @@ -141,16 +161,20 @@ export enum ButtonType { TERTIARY = "tertiary", } -export type PrivacyPreferencesCreateWithCode = { - // TODO: update this schema +export enum ConsentMethod { + button = "button", + gpc = "gpc", + individual_notice = "api", +} + +export type PrivacyPreferencesRequest = { browser_identity: Identity; code?: string; preferences: Array; policy_key?: string; // Will use default consent policy if not supplied - request_origin?: RequestOrigin; - url_recorded?: string; - user_agent?: string; + privacy_experience_history_id?: string; user_geography?: string; + method?: ConsentMethod; }; export type ConsentOptionCreate = { @@ -171,3 +195,34 @@ export enum RequestOrigin { overlay = "overlay", api = "api", } + +// ------------------LEGACY TYPES BELOW ------------------- + +export type ConditionalValue = { + value: boolean; + globalPrivacyControl: boolean; +}; + +/** + * A consent value can be a boolean: + * - `true`: consent/opt-in + * - `false`: revoke/opt-out + * + * A consent value can also be context-dependent, which means it will be decided based on + * information about the user's environment (browser). The `ConditionalValue` object maps the + * context conditions to the value that should be used: + * - `value`: The default value if no context applies. + * - `globalPrivacyControl`: The value to use if the user's browser has Global Privacy Control + * enabled. + */ +export type ConsentValue = boolean | ConditionalValue; + +export type ConsentOption = { + cookieKeys: string[]; + default?: ConsentValue; + fidesDataUseKey: string; +}; + +export type LegacyConsentConfig = { + options: ConsentOption[]; +}; diff --git a/clients/fides-js/src/lib/consent-utils.ts b/clients/fides-js/src/lib/consent-utils.ts index 2cf8f0a77f..114c64cc66 100644 --- a/clients/fides-js/src/lib/consent-utils.ts +++ b/clients/fides-js/src/lib/consent-utils.ts @@ -1,5 +1,7 @@ import { + ConsentMechanism, FidesOptions, + UserConsentPreference, UserGeolocation, VALID_ISO_3166_LOCATION_REGEX, } from "./consent-types"; @@ -24,14 +26,14 @@ export const debugLog = ( * Returns null if geolocation cannot be constructed by provided params, e.g. us_ca */ export const constructFidesRegionString = ( - geoLocation?: UserGeolocation, + geoLocation?: UserGeolocation | null, debug: boolean = false ): string | null => { debugLog(debug, "constructing geolocation..."); if (!geoLocation) { debugLog( debug, - "cannot construct user location since geoLocation is undefined" + "cannot construct user location since geoLocation is undefined or null" ); return null; } @@ -45,9 +47,7 @@ export const constructFidesRegionString = ( if (geoLocation.country && geoLocation.region) { return `${geoLocation.country.toLowerCase()}_${geoLocation.region.toLowerCase()}`; } - if (geoLocation.country) { - return geoLocation.country.toLowerCase(); - } + // todo: return geoLocation.country when BE supports filtering by just country debugLog( debug, "cannot construct user location from provided geoLocation params..." @@ -56,32 +56,76 @@ export const constructFidesRegionString = ( }; /** - * Validate the fides global config options + * Convert a user consent preference into true/false + */ +export const transformUserPreferenceToBoolean = ( + preference: UserConsentPreference | undefined +) => { + if (!preference) { + return false; + } + if (preference === UserConsentPreference.OPT_OUT) { + return false; + } + if (preference === UserConsentPreference.OPT_IN) { + return true; + } + return preference === UserConsentPreference.ACKNOWLEDGE; +}; + +/** + * Convert a true/false consent to Fides user consent preference + */ +export const transformConsentToFidesUserPreference = ( + consented: boolean, + consentMechanism?: ConsentMechanism +): UserConsentPreference => { + if (consented) { + if (consentMechanism === ConsentMechanism.NOTICE_ONLY) { + return UserConsentPreference.ACKNOWLEDGE; + } + return UserConsentPreference.OPT_IN; + } + return UserConsentPreference.OPT_OUT; +}; + +/** + * Validate the fides global config options. If invalid, we cannot make API calls to Fides or link to the Privacy Center. */ export const validateOptions = (options: FidesOptions): boolean => { // Check if options is an invalid type - if (options === undefined || typeof options !== "object") { + debugLog( + options.debug, + "Validating Fides consent overlay options...", + options + ); + if (typeof options !== "object") { return false; } // todo- more validation here? + if (!options.fidesApiUrl) { + debugLog(options.debug, "Invalid options: fidesApiUrl is required!"); + return false; + } + if (!options.privacyCenterUrl) { debugLog(options.debug, "Invalid options: privacyCenterUrl is required!"); return false; } - if (options.privacyCenterUrl) { - try { - // eslint-disable-next-line no-new - new URL(options.privacyCenterUrl); - } catch (e) { - debugLog( - options.debug, - "Invalid options: privacyCenterUrl is an invalid URL!", - options.privacyCenterUrl - ); - return false; - } + try { + // eslint-disable-next-line no-new + new URL(options.privacyCenterUrl); + // eslint-disable-next-line no-new + new URL(options.fidesApiUrl); + } catch (e) { + debugLog( + options.debug, + "Invalid options: privacyCenterUrl or fidesApiUrl is an invalid URL!", + options.privacyCenterUrl + ); + return false; } return true; diff --git a/clients/fides-js/src/lib/consent-value.ts b/clients/fides-js/src/lib/consent-value.ts index 2f3aac57ac..1b471d3097 100644 --- a/clients/fides-js/src/lib/consent-value.ts +++ b/clients/fides-js/src/lib/consent-value.ts @@ -1,29 +1,8 @@ import { ConsentContext } from "./consent-context"; +import { ConsentValue, UserConsentPreference } from "./consent-types"; +import { transformUserPreferenceToBoolean } from "./consent-utils"; -export type ConditionalValue = { - value: boolean; - globalPrivacyControl: boolean; -}; - -/** - * A consent value can be a boolean: - * - `true`: consent/opt-in - * - `false`: revoke/opt-out - * - * A consent value can also be context-dependent, which means it will be decided based on - * information about the user's environment (browser). The `ConditionalValue` object maps the - * context conditions to the value that should be used: - * - `value`: The default value if no context applies. - * - `globalPrivacyControl`: The value to use if the user's browser has Global Privacy Control - * enabled. - */ -export type ConsentValue = boolean | ConditionalValue; - -export type ConsentTypeToValue = { - [consentType: string]: ConsentValue; -}; - -export const resolveConsentValue = ( +export const resolveLegacyConsentValue = ( value: ConsentValue | undefined, context: ConsentContext ): boolean => { @@ -41,3 +20,20 @@ export const resolveConsentValue = ( return value.value; }; + +export const resolveConsentValue = ( + value: UserConsentPreference, + context: ConsentContext, + current_preference?: UserConsentPreference | null, + has_gpc_flag?: boolean +): boolean => { + if (current_preference) { + return transformUserPreferenceToBoolean(current_preference); + } + const gpcEnabled = !!has_gpc_flag && context.globalPrivacyControl === true; + if (gpcEnabled) { + return false; + } + + return transformUserPreferenceToBoolean(value); +}; diff --git a/clients/fides-js/src/lib/consent.tsx b/clients/fides-js/src/lib/consent.tsx index ec1ee026ee..5843b0c498 100644 --- a/clients/fides-js/src/lib/consent.tsx +++ b/clients/fides-js/src/lib/consent.tsx @@ -12,9 +12,9 @@ import { showModalLinkAndSetOnClick } from "./consent-links"; * (see the type definition of FidesOptions for what options are available) */ export const initOverlay = async ({ - consentDefaults, experience, - geolocation, + fidesRegionString, + cookie, options, }: OverlayProps): Promise => { debugLog(options.debug, "Initializing Fides consent overlays..."); @@ -50,10 +50,10 @@ export const initOverlay = async ({ // Render the Overlay to the DOM! render( , parentElem ); diff --git a/clients/fides-js/src/lib/cookie.ts b/clients/fides-js/src/lib/cookie.ts index 315bd7359c..fea62bf3d1 100644 --- a/clients/fides-js/src/lib/cookie.ts +++ b/clients/fides-js/src/lib/cookie.ts @@ -1,9 +1,13 @@ import { v4 as uuidv4 } from "uuid"; import { getCookie, setCookie, Types } from "typescript-cookie"; -import { ConsentConfig } from "./consent-config"; import { ConsentContext } from "./consent-context"; -import { resolveConsentValue } from "./consent-value"; +import { + resolveConsentValue, + resolveLegacyConsentValue, +} from "./consent-value"; +import { LegacyConsentConfig, PrivacyExperience } from "./consent-types"; +import { debugLog } from "./consent-utils"; /** * Store the user's consent preferences on the cookie, as key -> boolean pairs, e.g. @@ -98,7 +102,8 @@ export const makeFidesCookie = (consent?: CookieKeyConsent): FidesCookie => { * `saveFidesCookie` with a valid cookie after editing the values. */ export const getOrMakeFidesCookie = ( - defaults?: CookieKeyConsent + defaults?: CookieKeyConsent, + debug: boolean = false ): FidesCookie => { // Create a default cookie and set the configured consent defaults const defaultCookie = makeFidesCookie(defaults); @@ -110,11 +115,16 @@ export const getOrMakeFidesCookie = ( // Check for an existing cookie for this device const cookieString = getCookie(CONSENT_COOKIE_NAME, CODEC); if (!cookieString) { + debugLog( + debug, + `No existing Fides consent cookie found, returning defaults.`, + cookieString + ); return defaultCookie; } try { - // Parse the cookie and check it's format; if it's structured like we + // Parse the cookie and check its format; if it's structured like we // expect, cast it directly. Otherwise, assume it's a previous version of // the cookie, which was strictly the consent key/value preferences let parsedCookie: FidesCookie; @@ -139,10 +149,16 @@ export const getOrMakeFidesCookie = ( ...parsedCookie.consent, }; parsedCookie.consent = updatedConsent; + // since console.log is synchronous, we stringify to accurately read the parsedCookie obj + debugLog( + debug, + `Applied existing consent to data from existing Fides consent cookie.`, + JSON.stringify(parsedCookie) + ); return parsedCookie; } catch (err) { // eslint-disable-next-line no-console - console.error("Unable to read consent cookie: invalid JSON.", err); + debugLog(debug, `Unable to read consent cookie: invalid JSON.`, err); return defaultCookie; } }; @@ -155,7 +171,7 @@ export const getOrMakeFidesCookie = ( * example.com -> example.com * localhost -> localhost * - * NOTE: This won't handled second-level domains like co.uk: + * NOTE: This won't handle second-level domains like co.uk: * privacy.example.co.uk -> co.uk # ERROR * * (see https://github.com/ethyca/fides/issues/2072) @@ -179,9 +195,41 @@ export const saveFidesCookie = (cookie: FidesCookie) => { ); }; +/** + * Builds consent preferences for this session, based on: + * 1) context: browser context, which can automatically override those defaults + * in some cases (e.g. global privacy control => false) + * 2) experience: current experience-based consent configuration. + * + * Returns cookie consent that can then be changed according to the + * user's preferences. + */ +export const buildCookieConsentForExperiences = ( + experience: PrivacyExperience, + context: ConsentContext, + debug: boolean +): CookieKeyConsent => { + const cookieConsent: CookieKeyConsent = {}; + if (!experience.privacy_notices) { + return cookieConsent; + } + experience.privacy_notices.forEach( + ({ notice_key, current_preference, default_preference, has_gpc_flag }) => { + cookieConsent[notice_key] = resolveConsentValue( + default_preference, + context, + current_preference, + has_gpc_flag + ); + } + ); + debugLog(debug, `Returning cookie consent for experiences.`, cookieConsent); + return cookieConsent; +}; + /** * Generate the *default* consent preferences for this session, based on: - * 1) config: current consent configuration, which defines the options and their + * 1) config: current legacy consent configuration, which defines the options and their * default values (e.g. "data_sales" => true) * 2) context: browser context, which can automatically override those defaults * in some cases (e.g. global privacy control => false) @@ -189,20 +237,18 @@ export const saveFidesCookie = (cookie: FidesCookie) => { * Returns the final set of "defaults" that can then be changed according to the * user's preferences. */ -export const makeConsentDefaults = ({ - config, - context, -}: { - config?: ConsentConfig; - context: ConsentContext; -}): CookieKeyConsent => { +export const makeConsentDefaultsLegacy = ( + config: LegacyConsentConfig | undefined, + context: ConsentContext, + debug: boolean +): CookieKeyConsent => { const defaults: CookieKeyConsent = {}; config?.options.forEach(({ cookieKeys, default: current }) => { if (current === undefined) { return; } - const value = resolveConsentValue(current, context); + const value = resolveLegacyConsentValue(current, context); cookieKeys.forEach((cookieKey) => { const previous = defaults[cookieKey]; @@ -214,6 +260,6 @@ export const makeConsentDefaults = ({ defaults[cookieKey] = previous && value; }); }); - + debugLog(debug, `Returning defaults for legacy config.`, defaults); return defaults; }; diff --git a/clients/fides-js/src/lib/preferences.ts b/clients/fides-js/src/lib/preferences.ts index 5d0b1d2004..c7549ccb9b 100644 --- a/clients/fides-js/src/lib/preferences.ts +++ b/clients/fides-js/src/lib/preferences.ts @@ -1,16 +1,11 @@ import { - ConsentMechanism, + ConsentMethod, ConsentOptionCreate, - PrivacyNotice, - PrivacyPreferencesCreateWithCode, - UserConsentPreference, + PrivacyPreferencesRequest, + SaveConsentPreference, } from "./consent-types"; -import { debugLog } from "./consent-utils"; -import { - CookieKeyConsent, - getOrMakeFidesCookie, - saveFidesCookie, -} from "./cookie"; +import { debugLog, transformUserPreferenceToBoolean } from "./consent-utils"; +import { CookieKeyConsent, FidesCookie, saveFidesCookie } from "./cookie"; import { patchUserPreferenceToFidesServer } from "../services/fides/api"; /** @@ -20,52 +15,55 @@ import { patchUserPreferenceToFidesServer } from "../services/fides/api"; * 3. Save preferences to the `fides_consent` cookie in the browser */ export const updateConsentPreferences = ({ - privacyNotices, - enabledPrivacyNoticeIds, + consentPreferencesToSave, + experienceHistoryId, + fidesApiUrl, + consentMethod, + userLocationString, + cookie, debug = false, }: { - privacyNotices: PrivacyNotice[]; - enabledPrivacyNoticeIds: Array; + consentPreferencesToSave: Array; + experienceHistoryId: string; + fidesApiUrl: string; + consentMethod: ConsentMethod; + userLocationString: string; + cookie: FidesCookie; debug?: boolean; }) => { - const cookie = getOrMakeFidesCookie(); - // Derive the CookieKeyConsent object from privacy notices const noticeMap = new Map( - privacyNotices.map((notice) => [ - // DEFER(fides#3281): use notice key - notice.id, - enabledPrivacyNoticeIds.includes(notice.id), + consentPreferencesToSave.map(({ noticeKey, consentPreference }) => [ + noticeKey, + transformUserPreferenceToBoolean(consentPreference), ]) ); const consentCookieKey: CookieKeyConsent = Object.fromEntries(noticeMap); // Derive the Fides user preferences array from privacy notices const fidesUserPreferences: Array = []; - privacyNotices.forEach((notice) => { - let consentPreference; - if (enabledPrivacyNoticeIds.includes(notice.id)) { - if (notice.consent_mechanism === ConsentMechanism.NOTICE_ONLY) { - consentPreference = UserConsentPreference.ACKNOWLEDGE; - } else { - consentPreference = UserConsentPreference.OPT_IN; - } - } else { - consentPreference = UserConsentPreference.OPT_OUT; - } + consentPreferencesToSave.forEach(({ noticeHistoryId, consentPreference }) => { fidesUserPreferences.push({ - privacy_notice_history_id: notice.privacy_notice_history_id, + privacy_notice_history_id: noticeHistoryId, preference: consentPreference, }); }); - // 1. DEFER: Save preferences to Fides API + // 1. Save preferences to Fides API debugLog(debug, "Saving preferences to Fides API"); - const privacyPreferenceCreate: PrivacyPreferencesCreateWithCode = { + const privacyPreferenceCreate: PrivacyPreferencesRequest = { browser_identity: cookie.identity, preferences: fidesUserPreferences, + privacy_experience_history_id: experienceHistoryId, + user_geography: userLocationString, + method: consentMethod, }; - patchUserPreferenceToFidesServer(privacyPreferenceCreate, debug); + patchUserPreferenceToFidesServer( + privacyPreferenceCreate, + fidesApiUrl, + cookie.identity.fides_user_device_id, + debug + ); // 2. Update the window.Fides.consent object debugLog(debug, "Updating window.Fides"); diff --git a/clients/fides-js/src/services/external/geolocation.ts b/clients/fides-js/src/services/external/geolocation.ts index 2ca7f7eef1..d4e9add3ab 100644 --- a/clients/fides-js/src/services/external/geolocation.ts +++ b/clients/fides-js/src/services/external/geolocation.ts @@ -5,17 +5,26 @@ import { debugLog } from "../../lib/consent-utils"; * Fetch the user's geolocation from an external API */ export const getGeolocation = async ( + isGeolocationEnabled?: boolean, geolocationApiUrl?: string, debug: boolean = false -): Promise => { +): Promise => { debugLog(debug, "Running getLocation..."); + if (!isGeolocationEnabled) { + debugLog( + debug, + `User location could not be retrieved because geolocation is disabled.` + ); + return null; + } + if (!geolocationApiUrl) { debugLog( debug, "Location cannot be found due to no configured geoLocationApiUrl." ); - return {}; + return null; } debugLog(debug, `Calling geolocation API: GET ${geolocationApiUrl}...`); @@ -30,7 +39,7 @@ export const getGeolocation = async ( "Error getting location from geolocation API, returning {}. Response:", response ); - return {}; + return null; } try { @@ -47,6 +56,6 @@ export const getGeolocation = async ( "Error parsing response body from geolocation API, returning {}. Response:", response ); - return {}; + return null; } }; diff --git a/clients/fides-js/src/services/fides/api.ts b/clients/fides-js/src/services/fides/api.ts index db2f4e9e75..b219a01db9 100644 --- a/clients/fides-js/src/services/fides/api.ts +++ b/clients/fides-js/src/services/fides/api.ts @@ -1,30 +1,108 @@ import { + ComponentType, PrivacyExperience, - PrivacyPreferencesCreateWithCode, -} from "~/lib/consent-types"; + PrivacyPreferencesRequest, +} from "../../lib/consent-types"; import { debugLog } from "../../lib/consent-utils"; +export enum FidesEndpointPaths { + PRIVACY_EXPERIENCE = "/privacy-experience", + PRIVACY_PREFERENCES = "/privacy-preferences", +} + /** * Fetch the relevant experience based on user location and user device id (if exists). * Fetches both Privacy Center and Overlay components, because GPC needs to work regardless of component */ -export const fetchExperience = ( - userLocationString: String, +export const fetchExperience = async ( + userLocationString: string, + fidesApiUrl: string, + fidesUserDeviceId: string, debug: boolean -): PrivacyExperience | undefined => { - debugLog(debug, "Fetching experience for location...", userLocationString); - // TODO: GET /privacy-experience - return undefined; +): Promise => { + debugLog( + debug, + `Fetching experience for userId: ${fidesUserDeviceId} in location: ${userLocationString}` + ); + const fetchOptions: RequestInit = { + method: "GET", + mode: "cors", + }; + const params = new URLSearchParams({ + show_disabled: "false", + region: userLocationString, + component: ComponentType.OVERLAY, + has_notices: "true", + has_config: "true", + fides_user_device_id: fidesUserDeviceId, + }); + const response = await fetch( + `${fidesApiUrl}${FidesEndpointPaths.PRIVACY_EXPERIENCE}?${params}`, + fetchOptions + ); + if (!response.ok) { + debugLog( + debug, + "Error getting experience from Fides API, returning null. Response:", + response + ); + return null; + } + try { + const body = await response.json(); + const experience = body.items && body.items[0]; + if (!experience) { + debugLog( + debug, + "No relevant experience found from Fides API, returning null. Response:", + body + ); + return null; + } + debugLog( + debug, + "Got experience response from Fides API, returning:", + experience + ); + return experience; + } catch (e) { + debugLog( + debug, + "Error parsing experience response body from Fides API, returning {}. Response:", + response + ); + return null; + } }; /** * Sends user consent preference downstream to Fides */ -export const patchUserPreferenceToFidesServer = ( - preferences: PrivacyPreferencesCreateWithCode, +export const patchUserPreferenceToFidesServer = async ( + preferences: PrivacyPreferencesRequest, + fidesApiUrl: string, + fidesUserDeviceId: string, debug: boolean -): PrivacyExperience | undefined => { +): Promise => { debugLog(debug, "Saving user consent preference...", preferences); - // TODO: PATCH /consent-request/{consent_request_id}/privacy-preferences - return undefined; + const fetchOptions: RequestInit = { + method: "PATCH", + mode: "cors", + body: JSON.stringify(preferences), + headers: { + "Content-Type": "application/json", + }, + }; + const response = await fetch( + `${fidesApiUrl}${FidesEndpointPaths.PRIVACY_PREFERENCES}`, + fetchOptions + ); + if (!response.ok) { + debugLog( + debug, + "Error patching user preference Fides API. Response:", + response + ); + } + return Promise.resolve(); }; diff --git a/clients/privacy-center/app/server-environment.ts b/clients/privacy-center/app/server-environment.ts index 3e955ec371..876708d367 100644 --- a/clients/privacy-center/app/server-environment.ts +++ b/clients/privacy-center/app/server-environment.ts @@ -250,13 +250,16 @@ export const loadPrivacyCenterEnvironment = "file:///app/config/config.css", // Overlay options - DEBUG: process.env.FIDES_PRIVACY_CENTER__DEBUG === "true" || false, + DEBUG: process.env.FIDES_PRIVACY_CENTER__DEBUG + ? process.env.FIDES_PRIVACY_CENTER__DEBUG === "true" + : false, IS_OVERLAY_DISABLED: process.env.FIDES_PRIVACY_CENTER__IS_OVERLAY_DISABLED ? process.env.FIDES_PRIVACY_CENTER__IS_OVERLAY_DISABLED === "true" : true, - IS_GEOLOCATION_ENABLED: - process.env.FIDES_PRIVACY_CENTER__IS_GEOLOCATION_ENABLED === "true" || - false, + IS_GEOLOCATION_ENABLED: process.env + .FIDES_PRIVACY_CENTER__IS_GEOLOCATION_ENABLED + ? process.env.FIDES_PRIVACY_CENTER__IS_GEOLOCATION_ENABLED === "true" + : false, GEOLOCATION_API_URL: process.env.FIDES_PRIVACY_CENTER__GEOLOCATION_API_URL || "", OVERLAY_PARENT_ID: diff --git a/clients/privacy-center/components/consent/ConfigDrivenConsent.tsx b/clients/privacy-center/components/consent/ConfigDrivenConsent.tsx index b5c049e314..f6e5e938d1 100644 --- a/clients/privacy-center/components/consent/ConfigDrivenConsent.tsx +++ b/clients/privacy-center/components/consent/ConfigDrivenConsent.tsx @@ -1,6 +1,6 @@ import { Divider, Stack, useToast } from "@fidesui/react"; import React, { useCallback, useEffect, useMemo } from "react"; -import { getConsentContext, resolveConsentValue } from "fides-js"; +import { getConsentContext, resolveLegacyConsentValue } from "fides-js"; import { useAppDispatch, useAppSelector } from "~/app/hooks"; import { changeConsent, @@ -46,7 +46,10 @@ const ConfigDrivenConsent = ({ */ const saveUserConsentOptions = useCallback(() => { const consent = consentOptions.map((option) => { - const defaultValue = resolveConsentValue(option.default, consentContext); + const defaultValue = resolveLegacyConsentValue( + option.default, + consentContext + ); const value = fidesKeyToConsent[option.fidesDataUseKey] ?? defaultValue; const gpcStatus = getGpcStatus({ value, @@ -139,7 +142,7 @@ const ConfigDrivenConsent = ({ const items = useMemo( () => consentOptions.map((option) => { - const defaultValue = resolveConsentValue( + const defaultValue = resolveLegacyConsentValue( option.default, consentContext ); diff --git a/clients/privacy-center/cypress/e2e/consent-banner.cy.ts b/clients/privacy-center/cypress/e2e/consent-banner.cy.ts index 3d244fc253..6ba2fd0f67 100644 --- a/clients/privacy-center/cypress/e2e/consent-banner.cy.ts +++ b/clients/privacy-center/cypress/e2e/consent-banner.cy.ts @@ -1,15 +1,19 @@ -import { - CONSENT_COOKIE_NAME, +import LegacyConsentConfig, { ComponentType, + CONSENT_COOKIE_NAME, + ConsentMethod, DeliveryMechanism, FidesCookie, } from "fides-js"; import { + ConsentMechanism, + EnforcementLevel, FidesOptions, PrivacyExperience, + UserConsentPreference, UserGeolocation, } from "fides-js/src/lib/consent-types"; -import { ConsentConfig } from "fides-js/src/lib/consent-config"; +import { FidesEndpointPaths } from "fides-js/src/services/fides/api"; enum OVERRIDE { // signals that we should override entire prop with undefined @@ -18,7 +22,7 @@ enum OVERRIDE { export interface FidesConfigTesting { // We don't need all required props to override the default config - consent?: Partial | OVERRIDE; + consent?: Partial | OVERRIDE; experience?: Partial | OVERRIDE; geolocation?: Partial | OVERRIDE; options: Partial | OVERRIDE; @@ -28,12 +32,11 @@ export interface FidesConfigTesting { * Helper function to swap out config * @example stubExperience({experience: {component: ComponentType.PRIVACY_CENTER}}) */ -const stubConfig = ({ - consent, - experience, - geolocation, - options, -}: Partial) => { +const stubConfig = ( + { consent, experience, geolocation, options }: Partial, + mockGeolocationApiResp?: any, + mockExperienceApiResp?: any +) => { cy.fixture("consent/test_banner_options.json").then((config) => { const updatedConfig = { consent: @@ -53,39 +56,106 @@ const stubConfig = ({ ? undefined : Object.assign(config.options, options), }; - if (typeof options !== "string" && options?.geolocationApiUrl) { - cy.intercept("GET", options.geolocationApiUrl, { + // We conditionally stub these APIs because we need the exact API urls, which can change or not even exist + // depending on the specific test case. + if ( + typeof updatedConfig.options !== "string" && + updatedConfig.options?.geolocationApiUrl + ) { + const geoLocationResp = mockGeolocationApiResp || { body: { country: "US", ip: "63.173.339.012:13489", location: "US-CA", region: "CA", }, - }).as("getGeolocation"); + }; + cy.intercept( + "GET", + updatedConfig.options.geolocationApiUrl, + geoLocationResp + ).as("getGeolocation"); + } + if ( + typeof updatedConfig.options !== "string" && + updatedConfig.options?.fidesApiUrl + ) { + const experienceResp = mockExperienceApiResp || { + fixture: "consent/privacy-experience.json", + }; + cy.intercept( + "GET", + `${updatedConfig.options.fidesApiUrl}${FidesEndpointPaths.PRIVACY_EXPERIENCE}*`, + experienceResp + ).as("getPrivacyExperience"); + cy.intercept( + "PATCH", + `${updatedConfig.options.fidesApiUrl}${FidesEndpointPaths.PRIVACY_PREFERENCES}`, + { + body: {}, + } + ).as("patchPrivacyPreference"); } cy.visitConsentDemo(updatedConfig); }); }; -const PRIVACY_NOTICE_ID_1 = "pri_4bed96d0-b9e3-4596-a807-26b783836374"; -const PRIVACY_NOTICE_ID_2 = "pri_4bed96d0-b9e3-4596-a807-26b783836375"; +const PRIVACY_NOTICE_KEY_1 = "advertising"; +const PRIVACY_NOTICE_KEY_2 = "essential"; describe("Consent banner", () => { describe("when overlay is disabled", () => { - beforeEach(() => { - stubConfig({ - options: { - isOverlayDisabled: true, - }, + describe("when both experience and legacy consent exist", () => { + beforeEach(() => { + stubConfig({ + options: { + isOverlayDisabled: true, + }, + }); + }); + it("sets Fides.consent object with default consent based on privacy notices", () => { + cy.window() + .its("Fides") + .its("consent") + .should("eql", { + [PRIVACY_NOTICE_KEY_1]: false, + [PRIVACY_NOTICE_KEY_2]: false, + }); + }); + it("does not render banner", () => { + cy.get("div#fides-consent-banner").should("not.exist"); + cy.contains("button", "Accept Test").should("not.exist"); + }); + it("does not render modal link", () => { + cy.get("#fides-consent-modal-link").should("not.be.visible"); }); }); - - it("does not render banner", () => { - cy.get("div#fides-consent-banner").should("not.exist"); - cy.contains("button", "Accept Test").should("not.exist"); - }); - it("does not render modal link", () => { - cy.get("#fides-consent-modal-link").should("not.be.visible"); + describe("when only legacy consent exists", () => { + beforeEach(() => { + stubConfig( + { + options: { + isOverlayDisabled: true, + }, + experience: OVERRIDE.EMPTY, + }, + {}, + {} + ); + }); + it("sets Fides.consent object with default consent based on legacy consent", () => { + cy.window().its("Fides").its("consent").should("eql", { + data_sales: true, + tracking: false, + }); + }); + it("does not render banner", () => { + cy.get("div#fides-consent-banner").should("not.exist"); + cy.contains("button", "Accept Test").should("not.exist"); + }); + it("does not render modal link", () => { + cy.get("#fides-consent-modal-link").should("not.be.visible"); + }); }); }); @@ -143,10 +213,10 @@ describe("Consent banner", () => { decodeURIComponent(cookie!.value) ); expect(cookieKeyConsent.consent) - .property(PRIVACY_NOTICE_ID_1) + .property(PRIVACY_NOTICE_KEY_1) .is.eql(true); expect(cookieKeyConsent.consent) - .property(PRIVACY_NOTICE_ID_2) + .property(PRIVACY_NOTICE_KEY_2) .is.eql(true); }); cy.contains("button", "Accept Test").should("not.be.visible"); @@ -161,10 +231,10 @@ describe("Consent banner", () => { decodeURIComponent(cookie!.value) ); expect(cookieKeyConsent.consent) - .property(PRIVACY_NOTICE_ID_1) + .property(PRIVACY_NOTICE_KEY_1) .is.eql(false); expect(cookieKeyConsent.consent) - .property(PRIVACY_NOTICE_ID_2) + .property(PRIVACY_NOTICE_KEY_2) .is.eql(false); }); }); @@ -188,43 +258,70 @@ describe("Consent banner", () => { }); cy.getByTestId("toggle-Essential").click(); - // DEFER: intercept and check the API call once it is hooked up cy.getByTestId("Save-btn").click(); // Modal should close after saving cy.getByTestId("consent-modal").should("not.exist"); + // check that consent was sent to Fides API + let generatedUserDeviceId: string; + cy.wait("@patchPrivacyPreference").then((interception) => { + const { body } = interception.request; + const expected = { + // browser_identity.fides_user_device_id is intentionally left out here + // so we can later assert to be any string + preferences: [ + { + privacy_notice_history_id: + "pri_b09058a7-9f54-4360-8da5-4521e8975d4f", + preference: "opt_in", + }, + { + privacy_notice_history_id: + "pri_b09058a7-9f54-4360-8da5-4521e8975d4e", + preference: "acknowledge", + }, + ], + privacy_experience_history_id: "2342345", + user_geography: "us_ca", + method: ConsentMethod.button, + }; + // uuid is generated automatically if the user has no saved consent cookie + generatedUserDeviceId = body.browser_identity.fides_user_device_id; + expect(generatedUserDeviceId).to.be.a("string"); + expect(body.preferences).to.eql(expected.preferences); + expect(body.privacy_experience_history_id).to.eql( + expected.privacy_experience_history_id + ); + expect(body.user_geography).to.eql(expected.user_geography); + expect(body.method).to.eql(expected.method); + }); + // check that the cookie updated cy.waitUntilCookieExists(CONSENT_COOKIE_NAME).then(() => { cy.getCookie(CONSENT_COOKIE_NAME).then((cookie) => { const cookieKeyConsent: FidesCookie = JSON.parse( decodeURIComponent(cookie!.value) ); + expect(cookieKeyConsent.identity.fides_user_device_id).is.eql( + generatedUserDeviceId + ); expect(cookieKeyConsent.consent) - .property(PRIVACY_NOTICE_ID_1) + .property(PRIVACY_NOTICE_KEY_1) .is.eql(true); expect(cookieKeyConsent.consent) - .property(PRIVACY_NOTICE_ID_2) + .property(PRIVACY_NOTICE_KEY_2) .is.eql(true); }); }); // check that window.Fides.consent updated - cy.window().then((win) => { - expect(win.Fides.consent).to.eql({ - [PRIVACY_NOTICE_ID_1]: true, - [PRIVACY_NOTICE_ID_2]: true, + cy.window() + .its("Fides") + .its("consent") + .should("eql", { + [PRIVACY_NOTICE_KEY_1]: true, + [PRIVACY_NOTICE_KEY_2]: true, }); - }); - - // Upon reload, window.Fides should make the notices enabled - cy.reload(); - cy.contains("button", "Manage preferences").click(); - cy.getByTestId("toggle-Test privacy notice").within(() => { - cy.get("input").should("have.attr", "checked"); - }); - cy.getByTestId("toggle-Essential").within(() => { - cy.get("input").should("have.attr", "checked"); - }); }); }); @@ -242,6 +339,15 @@ describe("Consent banner", () => { consent: legacyNotices, }; cy.setCookie(CONSENT_COOKIE_NAME, JSON.stringify(originalCookie)); + + // we need to visit the page after the cookie exists, so the Fides.consent obj is initialized with the original + // cookie values + stubConfig({ + options: { + isOverlayDisabled: false, + }, + }); + cy.contains("button", "Manage preferences").click(); // Save new preferences @@ -251,9 +357,34 @@ describe("Consent banner", () => { // New privacy notice values only, no legacy ones const expectedConsent = { - [PRIVACY_NOTICE_ID_1]: true, - [PRIVACY_NOTICE_ID_2]: true, + [PRIVACY_NOTICE_KEY_1]: true, + [PRIVACY_NOTICE_KEY_2]: true, }; + + // check that consent was sent to Fides API + cy.wait("@patchPrivacyPreference").then((interception) => { + const { body } = interception.request; + const expected = { + browser_identity: { fides_user_device_id: uuid }, + preferences: [ + { + privacy_notice_history_id: + "pri_b09058a7-9f54-4360-8da5-4521e8975d4f", + preference: "opt_in", + }, + { + privacy_notice_history_id: + "pri_b09058a7-9f54-4360-8da5-4521e8975d4e", + preference: "acknowledge", + }, + ], + privacy_experience_history_id: "2342345", + user_geography: "us_ca", + method: ConsentMethod.button, + }; + expect(body).to.eql(expected); + }); + // check that the cookie updated cy.waitUntilCookieExists(CONSENT_COOKIE_NAME).then(() => { cy.getCookie(CONSENT_COOKIE_NAME).then((cookie) => { @@ -265,14 +396,7 @@ describe("Consent banner", () => { }); // check that window.Fides.consent updated - cy.window().then((win) => { - expect(win.Fides.consent).to.eql(expectedConsent); - }); - }); - - it.skip("should save the consent request to the Fides API", () => { - // TODO: add tests for saving to API (ie PATCH /api/v1/consent-request/{id}/preferences...) - expect(false).is.eql(true); + cy.window().its("Fides").its("consent").should("eql", expectedConsent); }); it.skip("should support option to display at top or bottom of page", () => { @@ -349,9 +473,48 @@ describe("Consent banner", () => { }); }); - it.skip("renders the banner", () => { - // TODO: add when we are able to retrieve experience via API from fides.js - expect(false).is.eql(true); + it("fetches experience and renders the banner", () => { + cy.wait("@getPrivacyExperience").then((interception) => { + expect(interception.request.query.region).to.eq("us_ca"); + }); + cy.get("div#fides-consent-banner").should("exist"); + cy.contains("button", "Accept Test").should("exist"); + cy.get("div#fides-consent-banner.fides-consent-banner").within(() => { + cy.get( + "div#fides-consent-banner-description.fides-consent-banner-description" + ).contains( + "Config from mocked Fides API is overriding this banner description." + ); + }); + }); + it("does not render modal link", () => { + cy.get("#fides-consent-modal-link").should("not.be.visible"); + }); + }); + + describe("when experience is provided, and geolocation is not provided", () => { + beforeEach(() => { + stubConfig({ + geolocation: OVERRIDE.EMPTY, + options: { + isGeolocationEnabled: true, + geolocationApiUrl: "https://some-geolocation-url.com", + }, + }); + }); + + it("fetches geolocation and renders the banner", () => { + // we still need geolocation because it is needed to save consent preference + cy.wait("@getGeolocation"); + cy.get("div#fides-consent-banner").should("exist"); + cy.contains("button", "Accept Test").should("exist"); + cy.get("div#fides-consent-banner.fides-consent-banner").within(() => { + cy.get( + "div#fides-consent-banner-description.fides-consent-banner-description" + ).contains( + "This test website is overriding the banner description label." + ); + }); }); it("does not render modal link", () => { cy.get("#fides-consent-modal-link").should("not.be.visible"); @@ -372,10 +535,20 @@ describe("Consent banner", () => { }); }); - it("renders the banner", () => { + it("fetches geolocation and experience renders the banner", () => { cy.wait("@getGeolocation"); - // TODO: add assertion for fetching experience - // TODO: add assertion for rendering banner + cy.wait("@getPrivacyExperience").then((interception) => { + expect(interception.request.query.region).to.eq("us_ca"); + }); + cy.get("div#fides-consent-banner").should("exist"); + cy.contains("button", "Accept Test").should("exist"); + cy.get("div#fides-consent-banner.fides-consent-banner").within(() => { + cy.get( + "div#fides-consent-banner-description.fides-consent-banner-description" + ).contains( + "Config from mocked Fides API is overriding this banner description." + ); + }); }); it.skip("hides the modal link", () => { // TODO: add when we have link binding working @@ -385,24 +558,26 @@ describe("Consent banner", () => { describe("when geolocation is not successful", () => { beforeEach(() => { - const geoLocationUrl = "https://some-geolocation-api.com"; // mock failed geolocation api call - cy.intercept("GET", geoLocationUrl, { - body: undefined, - }).as("getGeolocation"); - stubConfig({ - experience: OVERRIDE.EMPTY, - geolocation: OVERRIDE.EMPTY, - options: { - isGeolocationEnabled: true, - geolocationApiUrl: geoLocationUrl, + const mockFailedGeolocationCall = { + body: {}, + }; + stubConfig( + { + experience: OVERRIDE.EMPTY, + geolocation: OVERRIDE.EMPTY, + options: { + isGeolocationEnabled: true, + geolocationApiUrl: "https://some-geolocation-api.com", + }, }, - }); + mockFailedGeolocationCall + ); }); - it.skip("does not render", () => { + it("does not render banner", () => { cy.wait("@getGeolocation"); - // TODO: add assertion for fetching experience - // TODO: add assertion for banner not rendering after we implement experience api call + cy.get("div#fides-consent-banner").should("not.exist"); + cy.contains("button", "Accept Test").should("not.exist"); }); it.skip("hides the modal link", () => { // TODO: add when we have link binding working @@ -418,6 +593,8 @@ describe("Consent banner", () => { experience: OVERRIDE.EMPTY, geolocation: { country: "US", + location: "", + region: "", }, options: { isGeolocationEnabled: true, @@ -426,9 +603,20 @@ describe("Consent banner", () => { }); }); - it("fetches geolocation from API and renders the banner", () => { + it("fetches geolocation and experience and renders the banner", () => { cy.wait("@getGeolocation"); - // todo: add banner render assertion when we have experience API call + cy.wait("@getPrivacyExperience").then((interception) => { + expect(interception.request.query.region).to.eq("us_ca"); + }); + cy.get("div#fides-consent-banner").should("exist"); + cy.contains("button", "Accept Test").should("exist"); + cy.get("div#fides-consent-banner.fides-consent-banner").within(() => { + cy.get( + "div#fides-consent-banner-description.fides-consent-banner-description" + ).contains( + "Config from mocked Fides API is overriding this banner description." + ); + }); }); it("does not render modal link", () => { @@ -476,6 +664,52 @@ describe("Consent banner", () => { }); }); + describe("when all notices have current user preference set", () => { + beforeEach(() => { + stubConfig({ + experience: { + privacy_notices: [ + { + name: "Test privacy notice", + disabled: false, + origin: "12435134", + description: "a test sample privacy notice configuration", + internal_description: + "a test sample privacy notice configuration for internal use", + regions: ["us_ca"], + consent_mechanism: ConsentMechanism.OPT_IN, + default_preference: UserConsentPreference.OPT_IN, + current_preference: UserConsentPreference.OPT_IN, + outdated_preference: null, + has_gpc_flag: true, + data_uses: ["advertising", "third_party_sharing"], + enforcement_level: EnforcementLevel.SYSTEM_WIDE, + displayed_in_overlay: true, + displayed_in_api: true, + displayed_in_privacy_center: false, + id: "pri_4bed96d0-b9e3-4596-a807-26b783836374", + created_at: "2023-04-24T21:29:08.870351+00:00", + updated_at: "2023-04-24T21:29:08.870351+00:00", + version: 1.0, + privacy_notice_history_id: + "pri_b09058a7-9f54-4360-8da5-4521e8975d4f", + notice_key: "advertising", + }, + ], + }, + }); + }); + + it("does not render banner", () => { + cy.get("div#fides-consent-banner").should("not.exist"); + cy.contains("button", "Accept Test").should("not.exist"); + }); + + it("does not render modal link", () => { + cy.get("#fides-consent-modal-link").should("not.be.visible"); + }); + }); + describe("when experience delivery mechanism is link", () => { beforeEach(() => { stubConfig({ diff --git a/clients/privacy-center/cypress/e2e/consent.cy.ts b/clients/privacy-center/cypress/e2e/consent.cy.ts index c5d3dd6466..08da811917 100644 --- a/clients/privacy-center/cypress/e2e/consent.cy.ts +++ b/clients/privacy-center/cypress/e2e/consent.cy.ts @@ -245,38 +245,41 @@ describe("Consent settings", () => { cy.visit("/fides-js-demo.html"); cy.get("#consent-json"); - cy.window().then((win) => { - // Now all of the cookie keys should be populated. - expect(win).to.have.nested.property("Fides.consent").that.eql({ - data_sales: false, - tracking: false, - analytics: true, - gpc_test: true, - }); + cy.waitUntilFidesInitialized().then(() => { + cy.window({ timeout: 1000 }).should("have.property", "dataLayer"); + cy.window().then((win) => { + // Now all of the cookie keys should be populated. + expect(win).to.have.nested.property("Fides.consent").that.eql({ + data_sales: false, + tracking: false, + analytics: true, + gpc_test: true, + }); - // GTM configuration - expect(win) - .to.have.nested.property("dataLayer") - .that.eql([ - { - Fides: { - consent: { - data_sales: false, - tracking: false, - analytics: true, - gpc_test: true, + // GTM configuration + expect(win) + .to.have.nested.property("dataLayer") + .that.eql([ + { + Fides: { + consent: { + data_sales: false, + tracking: false, + analytics: true, + gpc_test: true, + }, }, }, - }, - ]); - - // Meta Pixel configuration - expect(win) - .to.have.nested.property("fbq.queue") - .that.eql([ - ["consent", "revoke"], - ["dataProcessingOptions", ["LDU"], 1, 1000], - ]); + ]); + + // Meta Pixel configuration + expect(win) + .to.have.nested.property("fbq.queue") + .that.eql([ + ["consent", "revoke"], + ["dataProcessingOptions", ["LDU"], 1, 1000], + ]); + }); }); }); @@ -339,36 +342,39 @@ describe("Consent settings", () => { it("reflects the defaults from config.json", () => { cy.visit("/fides-js-demo.html"); cy.get("#consent-json"); - cy.window().then((win) => { - // Before visiting the privacy center the consent object only has the default choices. - expect(win).to.have.nested.property("Fides.consent").that.eql({ - data_sales: true, - tracking: true, - analytics: true, - }); + cy.waitUntilFidesInitialized().then(() => { + cy.window({ timeout: 1000 }).should("have.property", "dataLayer"); + cy.window().then((win) => { + // Before visiting the privacy center the consent object only has the default choices. + expect(win).to.have.nested.property("Fides.consent").that.eql({ + data_sales: true, + tracking: true, + analytics: true, + }); - // GTM configuration - expect(win) - .to.have.nested.property("dataLayer") - .that.eql([ - { - Fides: { - consent: { - data_sales: true, - tracking: true, - analytics: true, + // GTM configuration + expect(win) + .to.have.nested.property("dataLayer") + .that.eql([ + { + Fides: { + consent: { + data_sales: true, + tracking: true, + analytics: true, + }, }, }, - }, - ]); - - // Meta Pixel configuration - expect(win) - .to.have.nested.property("fbq.queue") - .that.eql([ - ["consent", "grant"], - ["dataProcessingOptions", []], - ]); + ]); + + // Meta Pixel configuration + expect(win) + .to.have.nested.property("fbq.queue") + .that.eql([ + ["consent", "grant"], + ["dataProcessingOptions", []], + ]); + }); }); }); @@ -376,11 +382,14 @@ describe("Consent settings", () => { it("uses the globalPrivacyControl default", () => { cy.visit("/fides-js-demo.html?globalPrivacyControl=true"); cy.get("#consent-json"); - cy.window().then((win) => { - expect(win).to.have.nested.property("Fides.consent").that.eql({ - data_sales: false, - tracking: false, - analytics: true, + cy.waitUntilFidesInitialized().then(() => { + cy.window({ timeout: 1000 }).should("have.property", "dataLayer"); + cy.window().then((win) => { + expect(win).to.have.nested.property("Fides.consent").that.eql({ + data_sales: false, + tracking: false, + analytics: true, + }); }); }); }); diff --git a/clients/privacy-center/cypress/fixtures/consent/privacy-experience.json b/clients/privacy-center/cypress/fixtures/consent/privacy-experience.json new file mode 100644 index 0000000000..f2dad055d6 --- /dev/null +++ b/clients/privacy-center/cypress/fixtures/consent/privacy-experience.json @@ -0,0 +1,82 @@ +{ + "items": [ + { + "id": "132345243", + "created_at": "2023-04-24T21:29:08.870351+00:00", + "updated_at": "2023-04-24T21:29:08.870351+00:00", + "version": "1.0", + "component": "overlay", + "disabled": false, + "delivery_mechanism": "banner", + "region": "us_ca", + "privacy_experience_history_id": "2342345", + "experience_config": { + "component": "overlay", + "delivery_mechanism": "banner", + "disabled": false, + "component_title": "Manage your consent", + "component_description": "On this page you can opt in and out of these data uses cases", + "banner_title": "Manage your consent", + "banner_description": "Config from mocked Fides API is overriding this banner description.", + "confirmation_button_label": "Accept Test", + "acknowledgement_button_label": "OK", + "reject_button_label": "Reject Test", + "version": 1.0, + "created_at": "2023-04-24T21:29:08.870351+00:00", + "updated_at": "2023-04-24T21:29:08.870351+00:00", + "experience_config_history_id": "2345324", + "regions": ["us_ca"], + "id": "2348571y34", + "is_default": false, + "link_label": "Manage Consent" + }, + "privacy_notices": [ + { + "name": "Test privacy notice", + "description": "a test sample privacy notice configuration", + "internal_description": "a test sample privacy notice configuration for internal use", + "regions": ["us_ca"], + "consent_mechanism": "opt_in", + "default_preference": "opt_out", + "current_preference": null, + "outdated_preference": null, + "has_gpc_flag": true, + "disabled": false, + "origin": "12435134", + "data_uses": ["advertising", "third_party_sharing"], + "enforcement_level": "system_wide", + "displayed_in_overlay": true, + "displayed_in_api": true, + "displayed_in_privacy_center": false, + "id": "pri_4bed96d0-b9e3-4596-a807-26b783836374", + "created_at": "2023-04-24T21:29:08.870351+00:00", + "updated_at": "2023-04-24T21:29:08.870351+00:00", + "version": 1.0, + "privacy_notice_history_id": "pri_b09058a7-9f54-4360-8da5-4521e8975d4f", + "notice_key": "advertising" + }, + { + "name": "Essential", + "description": "Notify the user about data processing activities that are essential to your services functionality. Typically consent is not required for this.", + "regions": ["us_ca"], + "consent_mechanism": "notice_only", + "has_gpc_flag": true, + "data_uses": ["provide.service"], + "enforcement_level": "system_wide", + "displayed_in_overlay": true, + "displayed_in_api": true, + "displayed_in_privacy_center": false, + "id": "pri_4bed96d0-b9e3-4596-a807-26b783836375", + "created_at": "2023-04-24T21:29:08.870351+00:00", + "updated_at": "2023-04-24T21:29:08.870351+00:00", + "version": 1.0, + "privacy_notice_history_id": "pri_b09058a7-9f54-4360-8da5-4521e8975d4e", + "notice_key": "essential" + } + ] + } + ], + "total": 1, + "page": 1, + "size": 10 +} diff --git a/clients/privacy-center/cypress/fixtures/consent/test_banner_options.json b/clients/privacy-center/cypress/fixtures/consent/test_banner_options.json index d35646d156..be03719b01 100644 --- a/clients/privacy-center/cypress/fixtures/consent/test_banner_options.json +++ b/clients/privacy-center/cypress/fixtures/consent/test_banner_options.json @@ -65,13 +65,17 @@ "created_at": "2023-04-24T21:29:08.870351+00:00", "updated_at": "2023-04-24T21:29:08.870351+00:00", "version": 1.0, - "privacy_notice_history_id": "pri_b09058a7-9f54-4360-8da5-4521e8975d4f" + "privacy_notice_history_id": "pri_b09058a7-9f54-4360-8da5-4521e8975d4f", + "notice_key": "advertising" }, { "name": "Essential", "description": "Notify the user about data processing activities that are essential to your services functionality. Typically consent is not required for this.", "regions": ["us_ca"], "consent_mechanism": "notice_only", + "default_preference": "opt_out", + "current_preference": null, + "outdated_preference": null, "has_gpc_flag": true, "data_uses": ["provide.service"], "enforcement_level": "system_wide", @@ -82,7 +86,8 @@ "created_at": "2023-04-24T21:29:08.870351+00:00", "updated_at": "2023-04-24T21:29:08.870351+00:00", "version": 1.0, - "privacy_notice_history_id": "pri_b09058a7-9f54-4360-8da5-4521e8975d4e" + "privacy_notice_history_id": "pri_b09058a7-9f54-4360-8da5-4521e8975d4e", + "notice_key": "essential" } ] }, @@ -96,6 +101,7 @@ "isOverlayDisabled": false, "isGeolocationEnabled": false, "geolocationApiUrl": "", - "privacyCenterUrl": "http://localhost:3000" + "privacyCenterUrl": "http://localhost:3000", + "fidesApiUrl": "http://localhost:8080/api/v1" } } diff --git a/clients/privacy-center/cypress/support/commands.ts b/clients/privacy-center/cypress/support/commands.ts index 239b2d10c1..a9dfc846e5 100644 --- a/clients/privacy-center/cypress/support/commands.ts +++ b/clients/privacy-center/cypress/support/commands.ts @@ -28,6 +28,18 @@ Cypress.Commands.add("waitUntilCookieExists", (cookieName: string, ...args) => { ); }); +Cypress.Commands.add("waitUntilFidesInitialized", (...args) => { + cy.waitUntil( + () => + cy + .window() + .its("Fides") + .its("initialized") + .then(() => true), + ...args + ); +}); + Cypress.Commands.add("loadConfigFixture", (fixtureName: string, ...args) => { cy.getByTestId("logo"); cy.fixture(fixtureName, ...args).then((config) => { @@ -106,6 +118,19 @@ declare global { Cypress.Shadow > ): Chainable; + /** + * Custom command to wait until Fides consent script is fully initialized. + * + * @example cy.waitUntilFidesInitialized(); + */ + waitUntilFidesInitialized( + options?: Partial< + Cypress.Loggable & + Cypress.Timeoutable & + Cypress.Withinable & + Cypress.Shadow + > + ): Chainable; /** * Custom command to load a Privacy Center configuration JSON file from a fixture. * Note that because it is injected into the Redux state, any subsequent page-load resets that with the original diff --git a/clients/privacy-center/features/consent/helpers.ts b/clients/privacy-center/features/consent/helpers.ts index 7ead36d8b3..13e37f6aa8 100644 --- a/clients/privacy-center/features/consent/helpers.ts +++ b/clients/privacy-center/features/consent/helpers.ts @@ -1,7 +1,7 @@ import { ConsentContext, CookieKeyConsent, - resolveConsentValue, + resolveLegacyConsentValue, } from "fides-js"; import { @@ -64,7 +64,10 @@ export const makeCookieKeyConsent = ({ }): CookieKeyConsent => { const cookieKeyConsent: CookieKeyConsent = {}; consentOptions.forEach((option) => { - const defaultValue = resolveConsentValue(option.default, consentContext); + const defaultValue = resolveLegacyConsentValue( + option.default, + consentContext + ); const value = fidesKeyToConsent[option.fidesDataUseKey] ?? defaultValue; option.cookieKeys?.forEach((cookieKey) => { diff --git a/clients/privacy-center/pages/api/fides-js.ts b/clients/privacy-center/pages/api/fides-js.ts index 3349861a68..8d8d35b7e3 100644 --- a/clients/privacy-center/pages/api/fides-js.ts +++ b/clients/privacy-center/pages/api/fides-js.ts @@ -79,6 +79,7 @@ export default async function handler( isOverlayDisabled: environment.settings.IS_OVERLAY_DISABLED, overlayParentId: environment.settings.OVERLAY_PARENT_ID, privacyCenterUrl: environment.settings.PRIVACY_CENTER_URL, + fidesApiUrl: environment.settings.FIDES_API_URL, }, geolocation, }; diff --git a/clients/privacy-center/public/fides-js-components-demo.html b/clients/privacy-center/public/fides-js-components-demo.html index 67dba5b447..d06d08f1b4 100644 --- a/clients/privacy-center/public/fides-js-components-demo.html +++ b/clients/privacy-center/public/fides-js-components-demo.html @@ -81,6 +81,7 @@ version: 1.0, privacy_notice_history_id: "pri_b09058a7-9f54-4360-8da5-4521e8975d4f", + notice_key: "advertising", }, { name: "Essential", @@ -88,6 +89,9 @@ "Notify the user about data processing activities that are essential to your services functionality. Typically consent is not required for this.", regions: ["us_ca"], consent_mechanism: "notice_only", + default_preference: "opt_out", + current_preference: null, + outdated_preference: null, has_gpc_flag: true, data_uses: ["provide.service"], enforcement_level: "system_wide", @@ -100,6 +104,7 @@ version: 1.0, privacy_notice_history_id: "pri_b09058a7-9f54-4360-8da5-4521e8975d4e", + notice_key: "essential", }, ], }, @@ -115,6 +120,7 @@ geolocationApiUrl: "", overlayParentId: null, privacyCenterUrl: "http://localhost:3000", + fidesApiUrl: "http://localhost:8080/api/v1", }, }; window.Fides.init(fidesConfig); @@ -171,34 +177,41 @@

Consent Options

return; } - // Pretty-print the fides consent object and add it to the page. - document.getElementById("consent-json").textContent = JSON.stringify( - Fides.consent, - null, - 2 - ); - - // Pretty-print the fides experience config object and add it to the page. - document.getElementById("consent-experience").textContent = - JSON.stringify(Fides.experience, null, 2); - - // Pretty-print the fides geolocation object and add it to the page. - document.getElementById("consent-geolocation").textContent = - JSON.stringify(Fides.geolocation, null, 2); - - // Pretty-print the fides options object and add it to the page. - document.getElementById("consent-options").textContent = JSON.stringify( - Fides.options, - null, - 2 - ); - - // Test behavior of integrations that can be configured without/before their platform scripts. - Fides.gtm(); - Fides.meta({ - consent: Fides.consent.tracking, - dataUse: Fides.consent.data_sales, - }); + // DEFER: Ensure we only render the consent config to the DOM once Fides is fully initialized + // https://github.com/ethyca/fides/issues/3350 + let timerId = setInterval(() => { + if (window.Fides?.initialized) { + console.log("Fides has been initialized!"); + // Pretty-print the fides consent object and add it to the page. + document.getElementById("consent-json").textContent = JSON.stringify( + Fides.consent, + null, + 2 + ); + + // Pretty-print the fides experience config object and add it to the page. + document.getElementById("consent-experience").textContent = + JSON.stringify(Fides.experience, null, 2); + + // Pretty-print the fides geolocation object and add it to the page. + document.getElementById("consent-geolocation").textContent = + JSON.stringify(Fides.geolocation, null, 2); + + // Pretty-print the fides options object and add it to the page. + document.getElementById("consent-options").textContent = + JSON.stringify(Fides.options, null, 2); + + // Test behavior of integrations that can be configured without/before their platform scripts. + Fides.gtm(); + Fides.meta({ + consent: Fides.consent.tracking, + dataUse: Fides.consent.data_sales, + }); + clearInterval(timerId); + } else { + console.log("Fides not yet initialized, waiting. . ."); + } + }, 100); })(); diff --git a/clients/privacy-center/public/fides-js-demo.html b/clients/privacy-center/public/fides-js-demo.html index b7b83d9fb9..a6fb202fe5 100644 --- a/clients/privacy-center/public/fides-js-demo.html +++ b/clients/privacy-center/public/fides-js-demo.html @@ -51,34 +51,41 @@

Consent Options

return; } - // Pretty-print the fides consent object and add it to the page. - document.getElementById("consent-json").textContent = JSON.stringify( - Fides.consent, - null, - 2 - ); - - // Pretty-print the fides experience config object and add it to the page. - document.getElementById("consent-experience").textContent = - JSON.stringify(Fides.experience, null, 2); - - // Pretty-print the fides geolocation object and add it to the page. - document.getElementById("consent-geolocation").textContent = - JSON.stringify(Fides.geolocation, null, 2); - - // Pretty-print the fides options object and add it to the page. - document.getElementById("consent-options").textContent = JSON.stringify( - Fides.options, - null, - 2 - ); - - // Test behavior of integrations that can be configured without/before their platform scripts. - Fides.gtm(); - Fides.meta({ - consent: Fides.consent.tracking, - dataUse: Fides.consent.data_sales, - }); + // DEFER: Ensure we only render the consent config to the DOM once Fides is fully initialized + // https://github.com/ethyca/fides/issues/3350 + let timerId = setInterval(() => { + if (window.Fides?.initialized) { + console.log("Fides has been initialized!"); + // Pretty-print the fides consent object and add it to the page. + document.getElementById("consent-json").textContent = JSON.stringify( + Fides.consent, + null, + 2 + ); + + // Pretty-print the fides experience config object and add it to the page. + document.getElementById("consent-experience").textContent = + JSON.stringify(Fides.experience, null, 2); + + // Pretty-print the fides geolocation object and add it to the page. + document.getElementById("consent-geolocation").textContent = + JSON.stringify(Fides.geolocation, null, 2); + + // Pretty-print the fides options object and add it to the page. + document.getElementById("consent-options").textContent = + JSON.stringify(Fides.options, null, 2); + + // Test behavior of integrations that can be configured without/before their platform scripts. + Fides.gtm(); + Fides.meta({ + consent: Fides.consent.tracking, + dataUse: Fides.consent.data_sales, + }); + clearInterval(timerId); + } else { + console.log("Fides not yet initialized, waiting. . ."); + } + }, 100); })(); From c3b9e6164f93d8eb9920731e5954b93e296c7fcf Mon Sep 17 00:00:00 2001 From: Catherine Smith Date: Fri, 2 Jun 2023 11:52:56 -0400 Subject: [PATCH 03/24] 3424 styling for banner (#3430) --- .../fides-js/src/components/ConsentBanner.tsx | 58 ++++++++++--------- clients/fides-js/src/lib/overlay.module.css | 38 +++++++++--- .../cypress/e2e/consent-banner.cy.ts | 4 +- .../public/fides-js-components-demo.html | 2 +- 4 files changed, 64 insertions(+), 38 deletions(-) diff --git a/clients/fides-js/src/components/ConsentBanner.tsx b/clients/fides-js/src/components/ConsentBanner.tsx index d3cca6ccc6..7ee860e8f3 100644 --- a/clients/fides-js/src/components/ConsentBanner.tsx +++ b/clients/fides-js/src/components/ConsentBanner.tsx @@ -55,7 +55,7 @@ const ConsentBanner: FunctionComponent = ({ isShown ? "" : "fides-consent-banner-hidden" } `} > -
+ -
); diff --git a/clients/fides-js/src/lib/overlay.module.css b/clients/fides-js/src/lib/overlay.module.css index f2e25b72e2..2f18091054 100644 --- a/clients/fides-js/src/lib/overlay.module.css +++ b/clients/fides-js/src/lib/overlay.module.css @@ -6,6 +6,7 @@ --fides-consent-overlay-primary-color: #8243f2; --fides-consent-overlay-background-color: #f7fafc; --fides-consent-overlay-font-color: #4a5568; + --fides-consent-overlay-font-color-dark: #2d3748; --fides-consent-overlay-hover-color: #edf2f7; /* Buttons */ --fides-consent-overlay-primary-button-background-color: var( @@ -31,6 +32,9 @@ --fides-consent-overlay-body-font-color: var( --fides-consent-overlay-font-color ); + --fides-consent-overlay-link-font-color: var( + --fides-consent-overlay-font-color-dark + ); /* Switches */ --fides-consent-overlay-primary-active-color: var( --fides-consent-overlay-primary-color @@ -46,8 +50,9 @@ /* Everything else */ --fides-consent-overlay-font-family: "inherit"; - --fides-consent-overlay-font-size: 16px; - --fides-consent-overlay-font-size-title: 18px; + --fides-consent-overlay-font-size-body: 12px; + --fides-consent-overlay-font-size-title: 16px; + --fides-consent-overlay-font-size-buttons: 14px; --fides-consent-overlay-padding: 0.75em 2em 1em; --fides-consent-overlay-button-border-radius: 4px; --fides-consent-overlay-button-padding: 0.5em 1em; @@ -60,7 +65,7 @@ div#fides-consent-banner { font-family: var(--fides-consent-overlay-font-family); - font-size: var(--fides-consent-overlay-font-size); + font-size: var(--fides-consent-overlay-font-size-body); background: var(--fides-consent-overlay-background-color); color: var(--fides-consent-overlay-body-font-color); box-sizing: border-box; @@ -76,6 +81,10 @@ div#fides-consent-banner { transition: transform 1s; } +div#fides-consent-banner-inner { + width: 100%; +} + div#fides-consent-banner.fides-consent-banner-bottom { position: fixed; z-index: 1; @@ -104,6 +113,7 @@ div#fides-consent-banner.fides-consent-banner-top.fides-consent-banner-hidden { div#fides-consent-banner-title { font-size: var(--fides-consent-overlay-font-size-title); + font-weight: 600; margin-top: 0.5em; margin-right: 2em; min-width: 33%; @@ -119,9 +129,12 @@ div#fides-consent-banner-description { } div#fides-consent-banner-buttons { - display: flex; - flex-direction: row; - flex-wrap: wrap; + margin-top: 0.5em; + font-size: var(--fides-consent-overlay-font-size-buttons); +} + +span.fides-consent-banner-buttons-right { + float: right; } button.fides-consent-banner-button { @@ -136,7 +149,6 @@ button.fides-consent-banner-button { background: var(--fides-consent-overlay-primary-button-background-color); color: var(--fides-consent-overlay-primary-button-text-color); border: 1px solid; - border-radius: var(--fides-consent-overlay-button-border-radius); font-family: inherit; font-size: 100%; @@ -174,10 +186,20 @@ button.fides-consent-banner-button.fides-consent-banner-button-secondary:hover { ); } +button.fides-consent-banner-button.fides-consent-banner-button-tertiary { + background: none; + border: none; + padding: 0; + color: var(--fides-consent-overlay-link-font-color); + text-decoration: underline; + cursor: pointer; + line-height: 2em; +} + /** Modal */ div#fides-consent-modal { font-family: var(--fides-consent-overlay-font-family); - font-size: var(--fides-consent-overlay-font-size); + font-size: var(--fides-consent-overlay-font-size-body); color: var(--fides-consent-overlay-body-font-color); box-sizing: border-box; padding: var(--fides-consent-overlay-padding); diff --git a/clients/privacy-center/cypress/e2e/consent-banner.cy.ts b/clients/privacy-center/cypress/e2e/consent-banner.cy.ts index 6ba2fd0f67..4288dc2f17 100644 --- a/clients/privacy-center/cypress/e2e/consent-banner.cy.ts +++ b/clients/privacy-center/cypress/e2e/consent-banner.cy.ts @@ -180,7 +180,7 @@ describe("Consent banner", () => { "div#fides-consent-banner-buttons.fides-consent-banner-buttons" ).within(() => { cy.get( - "button#fides-consent-banner-button-secondary.fides-consent-banner-button.fides-consent-banner-button-secondary" + "button#fides-consent-banner-button-tertiary.fides-consent-banner-button.fides-consent-banner-button-tertiary" ).contains("Manage preferences"); cy.get( "button#fides-consent-banner-button-primary.fides-consent-banner-button.fides-consent-banner-button-primary" @@ -191,7 +191,7 @@ describe("Consent banner", () => { // Order matters - it should always be secondary, then primary! cy.get("button") .eq(0) - .should("have.id", "fides-consent-banner-button-secondary"); + .should("have.id", "fides-consent-banner-button-tertiary"); cy.get("button") .eq(1) .should("have.id", "fides-consent-banner-button-primary"); diff --git a/clients/privacy-center/public/fides-js-components-demo.html b/clients/privacy-center/public/fides-js-components-demo.html index d06d08f1b4..36c06e49f8 100644 --- a/clients/privacy-center/public/fides-js-components-demo.html +++ b/clients/privacy-center/public/fides-js-components-demo.html @@ -43,7 +43,7 @@ "On this page you can opt in and out of these data uses cases", banner_title: "Manage your consent", banner_description: - "This test website is overriding the banner description label.", + "We use cookies and similar methods to recognize visitors and remember their preferences. We also use them to measure ad campaign effectiveness, target ads and analyze site traffic. Learn more about these methods, including how to manage them, by clicking ‘Manage Preferences.’ By clicking ‘accept’ you consent to the of these methods by us and our third parties. By clicking ‘reject’ you decline the use of these methods.", confirmation_button_label: "Accept Test", acknowledgement_button_label: "OK", reject_button_label: "Reject Test", From 2323f1d44f95e09cddb765ff3e166c3b2829640e Mon Sep 17 00:00:00 2001 From: Allison King Date: Fri, 2 Jun 2023 12:53:04 -0400 Subject: [PATCH 04/24] Modal css fixes (#3429) --- .../fides-js/src/components/ConsentBanner.tsx | 1 - .../fides-js/src/components/ConsentModal.tsx | 2 +- clients/fides-js/src/components/Overlay.tsx | 2 + clients/fides-js/src/lib/overlay.module.css | 197 +++++++++--------- 4 files changed, 106 insertions(+), 96 deletions(-) diff --git a/clients/fides-js/src/components/ConsentBanner.tsx b/clients/fides-js/src/components/ConsentBanner.tsx index 7ee860e8f3..943c83d9a2 100644 --- a/clients/fides-js/src/components/ConsentBanner.tsx +++ b/clients/fides-js/src/components/ConsentBanner.tsx @@ -2,7 +2,6 @@ import { h, FunctionComponent } from "preact"; import { useState, useEffect } from "preact/hooks"; import { ButtonType, ExperienceConfig } from "../lib/consent-types"; import Button from "./Button"; -import "../lib/overlay.module.css"; import { useHasMounted } from "../lib/hooks"; interface BannerProps { diff --git a/clients/fides-js/src/components/ConsentModal.tsx b/clients/fides-js/src/components/ConsentModal.tsx index 65f138db84..4772e8716e 100644 --- a/clients/fides-js/src/components/ConsentModal.tsx +++ b/clients/fides-js/src/components/ConsentModal.tsx @@ -69,7 +69,7 @@ const ConsentModal = ({

{experience.component_title}

-

+

{experience.component_description}

diff --git a/clients/fides-js/src/components/Overlay.tsx b/clients/fides-js/src/components/Overlay.tsx index 79b106c454..309f098aa1 100644 --- a/clients/fides-js/src/components/Overlay.tsx +++ b/clients/fides-js/src/components/Overlay.tsx @@ -14,6 +14,8 @@ import { updateConsentPreferences } from "../lib/preferences"; import { transformConsentToFidesUserPreference } from "../lib/consent-utils"; import { FidesCookie } from "../lib/cookie"; +import "../lib/overlay.module.css"; + export interface OverlayProps { options: FidesOptions; experience: PrivacyExperience; diff --git a/clients/fides-js/src/lib/overlay.module.css b/clients/fides-js/src/lib/overlay.module.css index 2f18091054..6bbf4b5373 100644 --- a/clients/fides-js/src/lib/overlay.module.css +++ b/clients/fides-js/src/lib/overlay.module.css @@ -49,23 +49,30 @@ ); /* Everything else */ - --fides-consent-overlay-font-family: "inherit"; - --fides-consent-overlay-font-size-body: 12px; - --fides-consent-overlay-font-size-title: 16px; - --fides-consent-overlay-font-size-buttons: 14px; - --fides-consent-overlay-padding: 0.75em 2em 1em; + --fides-consent-overlay-font-family: Inter, sans-serif; + --fides-consent-overlay-font-size-body: 1em; + --fides-consent-overlay-font-size-title: 1.1em; + --fides-consent-overlay-font-size-buttons: 0.9em; + --fides-consent-overlay-padding: 1.5em; --fides-consent-overlay-button-border-radius: 4px; --fides-consent-overlay-button-padding: 0.5em 1em; --fides-consent-overlay-component-border-radius: 0px; } +div#fides-overlay { + font-family: var(--fides-consent-overlay-font-family); + font-size: var(--fides-consent-overlay-font-size-body); + + /* CSS reset values, adapted from https://www.joshwcomeau.com/css/custom-css-reset/ */ + line-height: calc(1em + 0.5rem); + -webkit-font-smoothing: antialiased; +} + #fides-consent-modal-link { display: none; } div#fides-consent-banner { - font-family: var(--fides-consent-overlay-font-family); - font-size: var(--fides-consent-overlay-font-size-body); background: var(--fides-consent-overlay-background-color); color: var(--fides-consent-overlay-body-font-color); box-sizing: border-box; @@ -151,7 +158,6 @@ button.fides-consent-banner-button { border: 1px solid; font-family: inherit; - font-size: 100%; line-height: 1.15; text-decoration: none; } @@ -215,16 +221,21 @@ div#fides-consent-modal { top: 50%; left: 50%; transform: translate(-50%, -50%); +} - .modal-header { - text-align: center; - font-size: var(--fides-consent-overlay-font-size-title); - color: var(--fides-consent-overlay-title-font-color); - } +div#fides-consent-modal .modal-header { + text-align: center; + font-weight: 600; + font-size: var(--fides-consent-overlay-font-size-title); + color: var(--fides-consent-overlay-title-font-color); +} - .modal-button-group { - display: flex; - } +div#fides-consent-modal .modal-description { + margin: 1em 0 1em 0; +} + +div#fides-consent-modal .modal-button-group { + display: flex; } .modal-overlay { @@ -254,81 +265,81 @@ div#fides-consent-modal { flex-wrap: wrap; position: relative; gap: 1ch; +} - .toggle-input { - position: absolute; - opacity: 0; - width: 100%; - height: 100%; - z-index: 4; - cursor: pointer; - } +.toggle .toggle-input { + position: absolute; + opacity: 0; + width: 100%; + height: 100%; + z-index: 4; + cursor: pointer; +} - .toggle-display { - --offset: 0.1em; - --diameter: 1em; +.toggle .toggle-display { + --offset: 0.1em; + --diameter: 1em; - display: inline-flex; - align-items: center; - justify-content: space-around; + display: inline-flex; + align-items: center; + justify-content: space-around; - width: calc(var(--diameter) * 2 + var(--offset) * 2); - height: calc(var(--diameter) + var(--offset) * 2); - box-sizing: content-box; + width: calc(var(--diameter) * 2 + var(--offset) * 2); + height: calc(var(--diameter) + var(--offset) * 2); + box-sizing: content-box; - position: relative; - border-radius: 100vw; - background-color: var(--fides-consent-overlay-inactive-color); - transition: 250ms; - } + position: relative; + border-radius: 100vw; + background-color: var(--fides-consent-overlay-inactive-color); + transition: 250ms; +} - .toggle-display::before { - content: ""; +.toggle .toggle-display::before { + content: ""; - width: var(--diameter); - height: var(--diameter); - border-radius: 50%; + width: var(--diameter); + height: var(--diameter); + border-radius: 50%; - box-sizing: border-box; + box-sizing: border-box; - position: absolute; - z-index: 3; - top: 50%; - left: var(--offset); - transform: translate(0, -50%); + position: absolute; + z-index: 3; + top: 50%; + left: var(--offset); + transform: translate(0, -50%); - background-color: #fff; - transition: inherit; - } + background-color: #fff; + transition: inherit; +} - /* Checked/unchecked states */ - .toggle-input:checked + .toggle-display { - background-color: var(--fides-consent-overlay-primary-active-color); - } - .toggle-input:checked + .toggle-display::before { - transform: translate(100%, -50%); - } +/* Checked/unchecked states */ +.toggle .toggle-input:checked + .toggle-display { + background-color: var(--fides-consent-overlay-primary-active-color); +} +.toggle .toggle-input:checked + .toggle-display::before { + transform: translate(100%, -50%); +} - /* Disabled state */ - .toggle-input:disabled { - cursor: not-allowed; - } - .toggle-input:disabled + .toggle-display { - background-color: var(--fides-consent-overlay-disabled-color); - } - .toggle-input:disabled:checked + .toggle-display { - background-color: var( - --fides-consent-overlay-primary-active-disabled-color - ); - } +/* Disabled state */ +.toggle .toggle-input:disabled { + cursor: not-allowed; +} +.toggle .toggle-input:disabled + .toggle-display { + background-color: var(--fides-consent-overlay-disabled-color); +} +.toggle .toggle-input:disabled:checked + .toggle-display { + background-color: var(--fides-consent-overlay-primary-active-disabled-color); +} - /* Focus ring when using keyboard */ - .toggle-input:focus + .toggle-display { - outline: 1px auto -webkit-focus-ring-color; - } - .toggle-input:focus:not(:focus-visible) + .toggle-display { - outline: 0; - } +/* Focus ring when using keyboard */ +.toggle .toggle-input:focus + .toggle-display { + /* Firefox only has Highlight, not -webkit-focus-ring-color */ + outline: 1px auto Highlight; + outline: 1px auto -webkit-focus-ring-color; +} +.toggle .toggle-input:focus:not(:focus-visible) + .toggle-display { + outline: 0; } /* Divider */ @@ -348,25 +359,23 @@ div#fides-consent-modal { margin-bottom: 0px; } -.notice-toggle { - .notice-toggle-title { - padding: 0.5em; - display: flex; - justify-content: space-between; - } +.notice-toggle .notice-toggle-title { + padding: 0.5em; + display: flex; + justify-content: space-between; +} - .notice-toggle-trigger { - width: 100%; - } +.notice-toggle .notice-toggle-trigger { + width: 100%; +} - .notice-toggle-title:hover { - cursor: pointer; - background-color: var(--fides-consent-overlay-row-hover-color); - } +.notice-toggle .notice-toggle-title:hover { + cursor: pointer; + background-color: var(--fides-consent-overlay-row-hover-color); +} - .disclosure-visible { - padding-left: 0.5em; - } +.notice-toggle .disclosure-visible { + padding-left: 0.5em; } .notice-toggle-expanded { From e73144d0b9ddcf6679cfc1048b2bbafe90018809 Mon Sep 17 00:00:00 2001 From: Allison King Date: Fri, 2 Jun 2023 15:25:35 -0400 Subject: [PATCH 05/24] Update fides-js and privacy center fields based on refactored experience config (#3442) Co-authored-by: eastandwestwind --- .../fides-js/src/components/ConsentBanner.tsx | 18 ++-- .../fides-js/src/components/ConsentModal.tsx | 19 +++- clients/fides-js/src/lib/consent-types.ts | 38 ++++---- clients/fides-js/src/lib/consent.tsx | 6 +- clients/fides-js/src/lib/overlay.module.css | 9 +- .../consent/ConfigDrivenConsent.tsx | 2 +- .../components/consent/ConsentDescription.tsx | 2 +- .../components/consent/ConsentHeading.tsx | 2 +- .../components/consent/ConsentItem.tsx | 1 - .../consent/NoticeDrivenConsent.tsx | 11 ++- .../components/consent/PrivacyPolicyLink.tsx | 33 +++++++ .../components/consent/SaveCancel.tsx | 11 ++- .../cypress/e2e/consent-banner.cy.ts | 13 ++- .../cypress/fixtures/consent/experience.json | 93 ++++++++++--------- .../fixtures/consent/overlay_experience.json | 65 +++++++++++++ .../fixtures/consent/privacy-experience.json | 82 ---------------- .../fixtures/consent/test_banner_options.json | 31 ++++--- .../public/fides-js-components-demo.html | 29 +++--- clients/privacy-center/types/api/index.ts | 2 +- .../types/api/models/BannerEnabled.ts | 12 +++ .../types/api/models/DeliveryMechanism.ts | 11 --- .../api/models/ExperienceConfigResponse.ts | 57 ++++++++++-- .../api/models/PrivacyExperienceResponse.ts | 7 +- 23 files changed, 317 insertions(+), 237 deletions(-) create mode 100644 clients/privacy-center/components/consent/PrivacyPolicyLink.tsx create mode 100644 clients/privacy-center/cypress/fixtures/consent/overlay_experience.json delete mode 100644 clients/privacy-center/cypress/fixtures/consent/privacy-experience.json create mode 100644 clients/privacy-center/types/api/models/BannerEnabled.ts delete mode 100644 clients/privacy-center/types/api/models/DeliveryMechanism.ts diff --git a/clients/fides-js/src/components/ConsentBanner.tsx b/clients/fides-js/src/components/ConsentBanner.tsx index 943c83d9a2..44d3b97cc8 100644 --- a/clients/fides-js/src/components/ConsentBanner.tsx +++ b/clients/fides-js/src/components/ConsentBanner.tsx @@ -18,17 +18,17 @@ const ConsentBanner: FunctionComponent = ({ onAcceptAll, onRejectAll, waitBeforeShow, - managePreferencesLabel = "Manage preferences", onOpenModal, }) => { const [isShown, setIsShown] = useState(false); const hasMounted = useHasMounted(); const { - banner_title: bannerTitle = "Manage your consent", - banner_description: - bannerDescription = "This website processes your data respectfully, so we require your consent to use cookies.", - confirmation_button_label: confirmationButtonLabel = "Accept All", + title = "Manage your consent", + description = "This website processes your data respectfully, so we require your consent to use cookies.", + accept_button_label: acceptButtonLabel = "Accept All", reject_button_label: rejectButtonLabel = "Reject All", + privacy_preferences_link_label: + privacyPreferencesLabel = "Manage preferences", } = experience; useEffect(() => { @@ -59,13 +59,13 @@ const ConsentBanner: FunctionComponent = ({ id="fides-consent-banner-title" className="fides-consent-banner-title" > - {bannerTitle || ""} + {title}
+ {experience.privacy_policy_link_label && + experience.privacy_policy_url ? ( + + {experience.privacy_policy_link_label} + + ) : null}
diff --git a/clients/fides-js/src/components/fides.css b/clients/fides-js/src/components/fides.css index 06ee44948b..af8a0b9805 100644 --- a/clients/fides-js/src/components/fides.css +++ b/clients/fides-js/src/components/fides.css @@ -47,7 +47,7 @@ --fides-overlay-padding: 1.5em; --fides-overlay-button-border-radius: 4px; --fides-overlay-button-padding: 0.5em 1em; - --fides-overlay-component-border-radius: 0px; + --fides-overlay-component-border-radius: 4px; } div#fides-overlay { @@ -63,53 +63,73 @@ div#fides-overlay { display: none; } +div#fides-banner-container { + position: fixed; + z-index: 1; + width: 100%; + transform: translateY(0%); + transition: transform 1s; + display: flex; + justify-content: center; +} + div#fides-banner { font-size: var(--fides-overlay-font-size-body-small); background: var(--fides-overlay-background-color); color: var(--fides-overlay-body-font-color); box-sizing: border-box; padding: var(--fides-overlay-padding); - border: 1px solid var(--fides-overlay-primary-color); - border-radius: var(--fides-overlay-component-border-radius); display: flex; flex-direction: row; flex-wrap: wrap; justify-content: space-between; align-items: center; - transition: transform 1s; } div#fides-banner-inner { width: 100%; } -div#fides-banner.fides-banner-bottom { - position: fixed; - z-index: 1; - width: 100%; +div#fides-banner-container.fides-banner-bottom { bottom: 0; left: 0; - transform: translateY(0%); } -div#fides-banner.fides-banner-bottom.fides-banner-hidden { +div#fides-banner-container.fides-banner-bottom.fides-banner-hidden { transform: translateY(100%); } -div#fides-banner.fides-banner-top { - position: fixed; - z-index: 1; - width: 100%; +div#fides-banner-container.fides-banner-top { top: 0; left: 0; - transform: translateY(0%); } -div#fides-banner.fides-banner-top.fides-banner-hidden { +div#fides-banner-container.fides-banner-top.fides-banner-hidden { transform: translateY(-100%); } +/* Responsive banner */ +@media screen and (min-width: 48em) { + div#fides-banner { + width: 75%; + border-radius: var(--fides-overlay-component-border-radius); + border: 1px solid var(--fides-overlay-primary-color); + } + div#fides-banner-container.fides-banner-bottom { + bottom: 48px; + } + div#fides-banner-container.fides-banner-top { + top: 48px; + } +} + +@media only screen and (min-width: 80em) { + div#fides-banner { + width: 60%; + } +} + div#fides-banner-title { font-size: var(--fides-overlay-font-size-title); font-weight: 600; diff --git a/clients/privacy-center/cypress/e2e/consent-banner.cy.ts b/clients/privacy-center/cypress/e2e/consent-banner.cy.ts index b3a1995952..81ac9f06eb 100644 --- a/clients/privacy-center/cypress/e2e/consent-banner.cy.ts +++ b/clients/privacy-center/cypress/e2e/consent-banner.cy.ts @@ -170,7 +170,7 @@ describe("Consent banner", () => { }); }); it("should render the expected HTML banner", () => { - cy.get("div#fides-banner.fides-banner").within(() => { + cy.get("div#fides-banner").within(() => { cy.get( "div#fides-banner-description.fides-banner-description" ).contains( diff --git a/clients/privacy-center/public/fides-js-components-demo.html b/clients/privacy-center/public/fides-js-components-demo.html index b099027dd3..8ec2a81df2 100644 --- a/clients/privacy-center/public/fides-js-components-demo.html +++ b/clients/privacy-center/public/fides-js-components-demo.html @@ -2,6 +2,7 @@ fides-js script demo page + From dea614d637ae3c37886310d863fb9ab78e65b9d0 Mon Sep 17 00:00:00 2001 From: Allison King Date: Tue, 6 Jun 2023 14:57:50 -0400 Subject: [PATCH 13/24] Fix banner transform (#3476) --- clients/fides-js/src/components/fides.css | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/clients/fides-js/src/components/fides.css b/clients/fides-js/src/components/fides.css index af8a0b9805..1c719fcadb 100644 --- a/clients/fides-js/src/components/fides.css +++ b/clients/fides-js/src/components/fides.css @@ -48,6 +48,7 @@ --fides-overlay-button-border-radius: 4px; --fides-overlay-button-padding: 0.5em 1em; --fides-overlay-component-border-radius: 4px; + --fides-overlay-banner-offset: 48px; } div#fides-overlay { @@ -97,7 +98,7 @@ div#fides-banner-container.fides-banner-bottom { } div#fides-banner-container.fides-banner-bottom.fides-banner-hidden { - transform: translateY(100%); + transform: translateY(150%); } div#fides-banner-container.fides-banner-top { @@ -106,7 +107,7 @@ div#fides-banner-container.fides-banner-top { } div#fides-banner-container.fides-banner-top.fides-banner-hidden { - transform: translateY(-100%); + transform: translateY(-150%); } /* Responsive banner */ @@ -117,10 +118,10 @@ div#fides-banner-container.fides-banner-top.fides-banner-hidden { border: 1px solid var(--fides-overlay-primary-color); } div#fides-banner-container.fides-banner-bottom { - bottom: 48px; + bottom: var(--fides-overlay-banner-offset); } div#fides-banner-container.fides-banner-top { - top: 48px; + top: var(--fides-overlay-banner-offset); } } From 8274b2a7077b417a3433be71f8a91101d2ad78f0 Mon Sep 17 00:00:00 2001 From: Allison King Date: Tue, 6 Jun 2023 16:20:54 -0400 Subject: [PATCH 14/24] Frontend experience id swap (#3479) --- .../src/types/api/models/ConsentReport.ts | 1 + .../api/models/ConsentReportingSchema.ts | 2 +- .../api/models/ExperienceConfigCreate.ts | 5 +++- .../api/models/ExperienceConfigResponse.ts | 2 +- .../api/models/ExperienceConfigUpdate.ts | 8 +++--- .../api/models/PrivacyExperienceResponse.ts | 3 --- .../types/api/models/PrivacyNoticeRegion.ts | 7 +++++ .../api/models/PrivacyPreferencesRequest.ts | 2 +- clients/fides-js/src/components/Overlay.tsx | 6 ++--- clients/fides-js/src/fides.ts | 2 +- clients/fides-js/src/lib/consent-types.ts | 5 +--- clients/fides-js/src/lib/preferences.ts | 6 ++--- .../consent/NoticeDrivenConsent.tsx | 2 +- .../cypress/e2e/consent-banner.cy.ts | 26 +++++++++---------- .../cypress/e2e/consent-notices.cy.ts | 4 ++- .../cypress/fixtures/consent/experience.json | 3 --- .../fixtures/consent/overlay_experience.json | 3 --- .../fixtures/consent/test_banner_options.json | 3 --- .../public/fides-js-components-demo.html | 3 --- .../api/models/ExperienceConfigResponse.ts | 2 +- .../api/models/PrivacyExperienceResponse.ts | 3 --- .../types/api/models/PrivacyNoticeRegion.ts | 7 +++++ .../api/models/PrivacyPreferencesRequest.ts | 2 +- 23 files changed, 53 insertions(+), 54 deletions(-) diff --git a/clients/admin-ui/src/types/api/models/ConsentReport.ts b/clients/admin-ui/src/types/api/models/ConsentReport.ts index 0237a30dc5..a120d82ba1 100644 --- a/clients/admin-ui/src/types/api/models/ConsentReport.ts +++ b/clients/admin-ui/src/types/api/models/ConsentReport.ts @@ -13,6 +13,7 @@ export type ConsentReport = { opt_in: boolean; has_gpc_flag?: boolean; conflicts_with_gpc?: boolean; + id: string; identity: IdentityBase; created_at: string; updated_at: string; diff --git a/clients/admin-ui/src/types/api/models/ConsentReportingSchema.ts b/clients/admin-ui/src/types/api/models/ConsentReportingSchema.ts index 35ada0f8e0..94a00c40cb 100644 --- a/clients/admin-ui/src/types/api/models/ConsentReportingSchema.ts +++ b/clients/admin-ui/src/types/api/models/ConsentReportingSchema.ts @@ -33,7 +33,7 @@ export type ConsentReportingSchema = { url_recorded?: string; user_agent?: string; experience_config_history_id?: string; - privacy_experience_history_id?: string; + privacy_experience_id?: string; truncated_ip_address?: string; method?: ConsentMethod; }; diff --git a/clients/admin-ui/src/types/api/models/ExperienceConfigCreate.ts b/clients/admin-ui/src/types/api/models/ExperienceConfigCreate.ts index 12dd1c32cc..c897bbf87e 100644 --- a/clients/admin-ui/src/types/api/models/ExperienceConfigCreate.ts +++ b/clients/admin-ui/src/types/api/models/ExperienceConfigCreate.ts @@ -42,9 +42,12 @@ export type ExperienceConfigCreate = { * Overlay 'Privacy preferences link label' */ privacy_preferences_link_label?: string; + /** + * Regions using this ExperienceConfig + */ + regions?: Array; reject_button_label: string; save_button_label: string; title: string; component: ComponentType; - regions: Array; }; diff --git a/clients/admin-ui/src/types/api/models/ExperienceConfigResponse.ts b/clients/admin-ui/src/types/api/models/ExperienceConfigResponse.ts index bb63eb6825..92bfa2050b 100644 --- a/clients/admin-ui/src/types/api/models/ExperienceConfigResponse.ts +++ b/clients/admin-ui/src/types/api/models/ExperienceConfigResponse.ts @@ -46,6 +46,7 @@ export type ExperienceConfigResponse = { * Overlay 'Privacy preferences link label' */ privacy_preferences_link_label?: string; + regions: Array; /** * Overlay 'Reject button displayed on the Banner and 'Privacy Preferences' of Privacy Center 'Reject button label' */ @@ -64,5 +65,4 @@ export type ExperienceConfigResponse = { version: number; created_at: string; updated_at: string; - regions: Array; }; diff --git a/clients/admin-ui/src/types/api/models/ExperienceConfigUpdate.ts b/clients/admin-ui/src/types/api/models/ExperienceConfigUpdate.ts index 2dd42eedf4..17840b49ed 100644 --- a/clients/admin-ui/src/types/api/models/ExperienceConfigUpdate.ts +++ b/clients/admin-ui/src/types/api/models/ExperienceConfigUpdate.ts @@ -45,6 +45,10 @@ export type ExperienceConfigUpdate = { * Overlay 'Privacy preferences link label' */ privacy_preferences_link_label?: string; + /** + * Regions using this ExperienceConfig + */ + regions?: Array; /** * Overlay 'Reject button displayed on the Banner and 'Privacy Preferences' of Privacy Center 'Reject button label' */ @@ -57,8 +61,4 @@ export type ExperienceConfigUpdate = { * Overlay 'Banner title' or Privacy Center 'title' */ title?: string; - /** - * If None, no edits will be made to regions. If an empty list, all regions will be removed. - */ - regions?: Array; }; diff --git a/clients/admin-ui/src/types/api/models/PrivacyExperienceResponse.ts b/clients/admin-ui/src/types/api/models/PrivacyExperienceResponse.ts index 60de2d013e..6abd50347a 100644 --- a/clients/admin-ui/src/types/api/models/PrivacyExperienceResponse.ts +++ b/clients/admin-ui/src/types/api/models/PrivacyExperienceResponse.ts @@ -13,13 +13,10 @@ import type { PrivacyNoticeResponseWithUserPreferences } from "./PrivacyNoticeRe export type PrivacyExperienceResponse = { region: PrivacyNoticeRegion; component?: ComponentType; - disabled?: boolean; experience_config?: ExperienceConfigResponse; id: string; created_at: string; updated_at: string; - version: number; - privacy_experience_history_id: string; show_banner?: boolean; privacy_notices?: Array; }; diff --git a/clients/admin-ui/src/types/api/models/PrivacyNoticeRegion.ts b/clients/admin-ui/src/types/api/models/PrivacyNoticeRegion.ts index 7acdd0b6ea..797d8482b1 100644 --- a/clients/admin-ui/src/types/api/models/PrivacyNoticeRegion.ts +++ b/clients/admin-ui/src/types/api/models/PrivacyNoticeRegion.ts @@ -83,4 +83,11 @@ export enum PrivacyNoticeRegion { EU_SK = "eu_sk", EU_FI = "eu_fi", EU_SE = "eu_se", + GB_ENG = "gb_eng", + GB_SCT = "gb_sct", + GB_WLS = "gb_wls", + GB_NIR = "gb_nir", + ISL = "isl", + NOR = "nor", + LI = "li", } diff --git a/clients/admin-ui/src/types/api/models/PrivacyPreferencesRequest.ts b/clients/admin-ui/src/types/api/models/PrivacyPreferencesRequest.ts index a907e20baa..618986f031 100644 --- a/clients/admin-ui/src/types/api/models/PrivacyPreferencesRequest.ts +++ b/clients/admin-ui/src/types/api/models/PrivacyPreferencesRequest.ts @@ -15,7 +15,7 @@ export type PrivacyPreferencesRequest = { code?: string; preferences: Array; policy_key?: string; - privacy_experience_history_id?: string; + privacy_experience_id?: string; user_geography?: PrivacyNoticeRegion; method?: ConsentMethod; }; diff --git a/clients/fides-js/src/components/Overlay.tsx b/clients/fides-js/src/components/Overlay.tsx index fe823beb62..79447643ac 100644 --- a/clients/fides-js/src/components/Overlay.tsx +++ b/clients/fides-js/src/components/Overlay.tsx @@ -50,7 +50,7 @@ const Overlay: FunctionComponent = ({ }); updateConsentPreferences({ consentPreferencesToSave, - experienceHistoryId: experience.privacy_experience_history_id, + experienceId: experience.id, fidesApiUrl: options.fidesApiUrl, consentMethod: ConsentMethod.button, userLocationString: fidesRegionString, @@ -71,7 +71,7 @@ const Overlay: FunctionComponent = ({ }); updateConsentPreferences({ consentPreferencesToSave, - experienceHistoryId: experience.privacy_experience_history_id, + experienceId: experience.id, fidesApiUrl: options.fidesApiUrl, consentMethod: ConsentMethod.button, userLocationString: fidesRegionString, @@ -97,7 +97,7 @@ const Overlay: FunctionComponent = ({ }); updateConsentPreferences({ consentPreferencesToSave, - experienceHistoryId: experience.privacy_experience_history_id, + experienceId: experience.id, fidesApiUrl: options.fidesApiUrl, consentMethod: ConsentMethod.button, userLocationString: fidesRegionString, diff --git a/clients/fides-js/src/fides.ts b/clients/fides-js/src/fides.ts index 42ccb31f55..e468d172e7 100644 --- a/clients/fides-js/src/fides.ts +++ b/clients/fides-js/src/fides.ts @@ -152,7 +152,7 @@ const automaticallyApplyGPCPreferences = ( if (consentPreferencesToSave.length > 0) { updateConsentPreferences({ consentPreferencesToSave, - experienceHistoryId: effectiveExperience.privacy_experience_history_id, + experienceId: effectiveExperience.id, fidesApiUrl, consentMethod: ConsentMethod.gpc, userLocationString: fidesRegionString || undefined, diff --git a/clients/fides-js/src/lib/consent-types.ts b/clients/fides-js/src/lib/consent-types.ts index 90cef73320..31bd2b611a 100644 --- a/clients/fides-js/src/lib/consent-types.ts +++ b/clients/fides-js/src/lib/consent-types.ts @@ -57,13 +57,10 @@ export class SaveConsentPreference { export type PrivacyExperience = { region: string; // intentionally using plain string instead of Enum, since BE is susceptible to change component?: ComponentType; - disabled?: boolean; experience_config?: ExperienceConfig; id: string; created_at: string; updated_at: string; - version: number; - privacy_experience_history_id: string; show_banner?: boolean; privacy_notices?: Array; }; @@ -174,7 +171,7 @@ export type PrivacyPreferencesRequest = { code?: string; preferences: Array; policy_key?: string; // Will use default consent policy if not supplied - privacy_experience_history_id?: string; + privacy_experience_id?: string; user_geography?: string; method?: ConsentMethod; }; diff --git a/clients/fides-js/src/lib/preferences.ts b/clients/fides-js/src/lib/preferences.ts index 522757e808..ce6bbb9932 100644 --- a/clients/fides-js/src/lib/preferences.ts +++ b/clients/fides-js/src/lib/preferences.ts @@ -17,7 +17,7 @@ import { patchUserPreferenceToFidesServer } from "../services/fides/api"; */ export const updateConsentPreferences = ({ consentPreferencesToSave, - experienceHistoryId, + experienceId, fidesApiUrl, consentMethod, userLocationString, @@ -25,7 +25,7 @@ export const updateConsentPreferences = ({ debug = false, }: { consentPreferencesToSave: Array; - experienceHistoryId: string; + experienceId: string; fidesApiUrl: string; consentMethod: ConsentMethod; userLocationString?: string; @@ -59,7 +59,7 @@ export const updateConsentPreferences = ({ const privacyPreferenceCreate: PrivacyPreferencesRequest = { browser_identity: cookie.identity, preferences: fidesUserPreferences, - privacy_experience_history_id: experienceHistoryId, + privacy_experience_id: experienceId, user_geography: userLocationString, method: consentMethod, }; diff --git a/clients/privacy-center/components/consent/NoticeDrivenConsent.tsx b/clients/privacy-center/components/consent/NoticeDrivenConsent.tsx index 72d079cf31..8e11d58990 100644 --- a/clients/privacy-center/components/consent/NoticeDrivenConsent.tsx +++ b/clients/privacy-center/components/consent/NoticeDrivenConsent.tsx @@ -138,7 +138,7 @@ const NoticeDrivenConsent = () => { browser_identity: identities, preferences, user_geography: region, - privacy_experience_history_id: experience?.privacy_experience_history_id, + privacy_experience_id: experience?.id, method: ConsentMethod.BUTTON, code: verificationCode, }; diff --git a/clients/privacy-center/cypress/e2e/consent-banner.cy.ts b/clients/privacy-center/cypress/e2e/consent-banner.cy.ts index 81ac9f06eb..641c84ffb2 100644 --- a/clients/privacy-center/cypress/e2e/consent-banner.cy.ts +++ b/clients/privacy-center/cypress/e2e/consent-banner.cy.ts @@ -279,7 +279,7 @@ describe("Consent banner", () => { preference: "acknowledge", }, ], - privacy_experience_history_id: "2342345", + privacy_experience_id: "132345243", user_geography: "us_ca", method: ConsentMethod.button, }; @@ -287,8 +287,8 @@ describe("Consent banner", () => { generatedUserDeviceId = body.browser_identity.fides_user_device_id; expect(generatedUserDeviceId).to.be.a("string"); expect(body.preferences).to.eql(expected.preferences); - expect(body.privacy_experience_history_id).to.eql( - expected.privacy_experience_history_id + expect(body.privacy_experience_id).to.eql( + expected.privacy_experience_id ); expect(body.user_geography).to.eql(expected.user_geography); expect(body.method).to.eql(expected.method); @@ -376,7 +376,7 @@ describe("Consent banner", () => { preference: "acknowledge", }, ], - privacy_experience_history_id: "2342345", + privacy_experience_id: "132345243", user_geography: "us_ca", method: ConsentMethod.button, }; @@ -428,8 +428,8 @@ describe("Consent banner", () => { regions: ["us_ca"], consent_mechanism: ConsentMechanism.OPT_OUT, default_preference: UserConsentPreference.OPT_IN, - current_preference: null, - outdated_preference: null, + current_preference: undefined, + outdated_preference: undefined, has_gpc_flag: true, data_uses: ["advertising", "third_party_sharing"], enforcement_level: EnforcementLevel.SYSTEM_WIDE, @@ -463,7 +463,7 @@ describe("Consent banner", () => { preference: "opt_out", }, ], - privacy_experience_history_id: "2342345", + privacy_experience_id: "132345243", user_geography: "us_ca", method: ConsentMethod.gpc, }; @@ -471,8 +471,8 @@ describe("Consent banner", () => { generatedUserDeviceId = body.browser_identity.fides_user_device_id; expect(generatedUserDeviceId).to.be.a("string"); expect(body.preferences).to.eql(expected.preferences); - expect(body.privacy_experience_history_id).to.eql( - expected.privacy_experience_history_id + expect(body.privacy_experience_id).to.eql( + expected.privacy_experience_id ); expect(body.user_geography).to.eql(expected.user_geography); expect(body.method).to.eql(expected.method); @@ -521,8 +521,8 @@ describe("Consent banner", () => { regions: ["us_ca"], consent_mechanism: ConsentMechanism.OPT_OUT, default_preference: UserConsentPreference.OPT_IN, - current_preference: null, - outdated_preference: null, + current_preference: undefined, + outdated_preference: undefined, has_gpc_flag: false, data_uses: ["advertising", "third_party_sharing"], enforcement_level: EnforcementLevel.SYSTEM_WIDE, @@ -581,8 +581,8 @@ describe("Consent banner", () => { regions: ["us_ca"], consent_mechanism: ConsentMechanism.OPT_OUT, default_preference: UserConsentPreference.OPT_IN, - current_preference: null, - outdated_preference: null, + current_preference: undefined, + outdated_preference: undefined, has_gpc_flag: true, data_uses: ["advertising", "third_party_sharing"], enforcement_level: EnforcementLevel.SYSTEM_WIDE, diff --git a/clients/privacy-center/cypress/e2e/consent-notices.cy.ts b/clients/privacy-center/cypress/e2e/consent-notices.cy.ts index d9509159e6..5fc4438823 100644 --- a/clients/privacy-center/cypress/e2e/consent-notices.cy.ts +++ b/clients/privacy-center/cypress/e2e/consent-notices.cy.ts @@ -8,6 +8,7 @@ import { API_URL } from "../support/constants"; const VERIFICATION_CODE = "112358"; const PRIVACY_NOTICE_ID_1 = "pri_b4360591-3cc7-400d-a5ff-a9f095ab3061"; const PRIVACY_NOTICE_ID_2 = "pri_b558ab1f-5367-4f0d-94b1-ec06a81ae821"; +const PRIVACY_EXPERIENCE_ID = "pri_041acb07-c99b-4085-a435-c0d6f3a42b6f"; const GEOLOCATION_API_URL = "https://www.example.com/location"; const SETTINGS = { IS_OVERLAY_DISABLED: false, @@ -95,9 +96,10 @@ describe("Privacy notice driven consent", () => { cy.getByTestId("save-btn").click(); cy.wait("@patchPrivacyPreference").then((interception) => { const { body } = interception.request; - const { preferences, code, method } = body; + const { preferences, code, method, privacy_experience_id: id } = body; expect(method).to.eql("button"); expect(code).to.eql(VERIFICATION_CODE); + expect(id).to.eql(PRIVACY_EXPERIENCE_ID); expect( preferences.map((p: ConsentOptionCreate) => p.preference) ).to.eql(["opt_in", "opt_in"]); diff --git a/clients/privacy-center/cypress/fixtures/consent/experience.json b/clients/privacy-center/cypress/fixtures/consent/experience.json index d8d2f275f4..acb06c30ed 100644 --- a/clients/privacy-center/cypress/fixtures/consent/experience.json +++ b/clients/privacy-center/cypress/fixtures/consent/experience.json @@ -3,7 +3,6 @@ { "region": "us_ca", "component": "privacy_center", - "disabled": false, "experience_config": { "accept_button_label": "sounds good", "acknowledge_button_label": "ok", @@ -28,8 +27,6 @@ "id": "pri_041acb07-c99b-4085-a435-c0d6f3a42b6f", "created_at": "2023-06-02T15:22:02.604734+00:00", "updated_at": "2023-06-02T15:28:28.469659+00:00", - "version": 3.0, - "privacy_experience_history_id": "pri_94142e00-97da-4eb4-8f8c-9b848b65636c", "show_banner": false, "privacy_notices": [ { diff --git a/clients/privacy-center/cypress/fixtures/consent/overlay_experience.json b/clients/privacy-center/cypress/fixtures/consent/overlay_experience.json index 37c2ff89c3..1e68cd4194 100644 --- a/clients/privacy-center/cypress/fixtures/consent/overlay_experience.json +++ b/clients/privacy-center/cypress/fixtures/consent/overlay_experience.json @@ -3,7 +3,6 @@ { "region": "us_ca", "component": "overlay", - "disabled": false, "experience_config": { "accept_button_label": "Accept Test", "acknowledge_button_label": "Got it", @@ -28,8 +27,6 @@ "id": "pri_b9d1af04-5852-4499-bdfb-2778a6117fb8", "created_at": "2023-06-02T15:18:56.110785+00:00", "updated_at": "2023-06-02T16:40:37.340905+00:00", - "version": 2.0, - "privacy_experience_history_id": "pri_1255e8a0-9b39-42c2-80db-edc83fba44f8", "show_banner": true, "privacy_notices": [ { diff --git a/clients/privacy-center/cypress/fixtures/consent/test_banner_options.json b/clients/privacy-center/cypress/fixtures/consent/test_banner_options.json index f2fd0f8dab..520ce8cb5f 100644 --- a/clients/privacy-center/cypress/fixtures/consent/test_banner_options.json +++ b/clients/privacy-center/cypress/fixtures/consent/test_banner_options.json @@ -17,11 +17,8 @@ "id": "132345243", "created_at": "2023-04-24T21:29:08.870351+00:00", "updated_at": "2023-04-24T21:29:08.870351+00:00", - "version": "1.0", "component": "overlay", - "disabled": false, "region": "us_ca", - "privacy_experience_history_id": "2342345", "show_banner": true, "experience_config": { "accept_button_label": "Accept Test", diff --git a/clients/privacy-center/public/fides-js-components-demo.html b/clients/privacy-center/public/fides-js-components-demo.html index 8ec2a81df2..045eefad61 100644 --- a/clients/privacy-center/public/fides-js-components-demo.html +++ b/clients/privacy-center/public/fides-js-components-demo.html @@ -24,15 +24,12 @@ ], }, experience: { - version: "1.0", id: "132345243", - disabled: false, region: "us_ca", show_banner: true, component: "overlay", created_at: "2023-04-24T21:29:08.870351+00:00", updated_at: "2023-04-24T21:29:08.870351+00:00", - privacy_experience_history_id: "2342345", experience_config: { accept_button_label: "Accept Test", acknowledge_button_label: "OK", diff --git a/clients/privacy-center/types/api/models/ExperienceConfigResponse.ts b/clients/privacy-center/types/api/models/ExperienceConfigResponse.ts index bb63eb6825..92bfa2050b 100644 --- a/clients/privacy-center/types/api/models/ExperienceConfigResponse.ts +++ b/clients/privacy-center/types/api/models/ExperienceConfigResponse.ts @@ -46,6 +46,7 @@ export type ExperienceConfigResponse = { * Overlay 'Privacy preferences link label' */ privacy_preferences_link_label?: string; + regions: Array; /** * Overlay 'Reject button displayed on the Banner and 'Privacy Preferences' of Privacy Center 'Reject button label' */ @@ -64,5 +65,4 @@ export type ExperienceConfigResponse = { version: number; created_at: string; updated_at: string; - regions: Array; }; diff --git a/clients/privacy-center/types/api/models/PrivacyExperienceResponse.ts b/clients/privacy-center/types/api/models/PrivacyExperienceResponse.ts index 60de2d013e..6abd50347a 100644 --- a/clients/privacy-center/types/api/models/PrivacyExperienceResponse.ts +++ b/clients/privacy-center/types/api/models/PrivacyExperienceResponse.ts @@ -13,13 +13,10 @@ import type { PrivacyNoticeResponseWithUserPreferences } from "./PrivacyNoticeRe export type PrivacyExperienceResponse = { region: PrivacyNoticeRegion; component?: ComponentType; - disabled?: boolean; experience_config?: ExperienceConfigResponse; id: string; created_at: string; updated_at: string; - version: number; - privacy_experience_history_id: string; show_banner?: boolean; privacy_notices?: Array; }; diff --git a/clients/privacy-center/types/api/models/PrivacyNoticeRegion.ts b/clients/privacy-center/types/api/models/PrivacyNoticeRegion.ts index 7acdd0b6ea..797d8482b1 100644 --- a/clients/privacy-center/types/api/models/PrivacyNoticeRegion.ts +++ b/clients/privacy-center/types/api/models/PrivacyNoticeRegion.ts @@ -83,4 +83,11 @@ export enum PrivacyNoticeRegion { EU_SK = "eu_sk", EU_FI = "eu_fi", EU_SE = "eu_se", + GB_ENG = "gb_eng", + GB_SCT = "gb_sct", + GB_WLS = "gb_wls", + GB_NIR = "gb_nir", + ISL = "isl", + NOR = "nor", + LI = "li", } diff --git a/clients/privacy-center/types/api/models/PrivacyPreferencesRequest.ts b/clients/privacy-center/types/api/models/PrivacyPreferencesRequest.ts index a907e20baa..618986f031 100644 --- a/clients/privacy-center/types/api/models/PrivacyPreferencesRequest.ts +++ b/clients/privacy-center/types/api/models/PrivacyPreferencesRequest.ts @@ -15,7 +15,7 @@ export type PrivacyPreferencesRequest = { code?: string; preferences: Array; policy_key?: string; - privacy_experience_history_id?: string; + privacy_experience_id?: string; user_geography?: PrivacyNoticeRegion; method?: ConsentMethod; }; From fe4804216e03daf0e10e4265182fe8d26bf0a48f Mon Sep 17 00:00:00 2001 From: Catherine Smith Date: Tue, 6 Jun 2023 19:36:09 -0400 Subject: [PATCH 15/24] 3349 trigger modal from link (#3467) --- .../fides-js/src/components/ConsentBanner.tsx | 28 ++------ clients/fides-js/src/components/Overlay.tsx | 60 +++++++++++++--- clients/fides-js/src/components/fides.css | 5 ++ clients/fides-js/src/fides.ts | 23 +++--- clients/fides-js/src/lib/consent-links.ts | 22 ------ clients/fides-js/src/lib/consent-types.ts | 5 +- clients/fides-js/src/lib/consent.tsx | 40 ++++++----- .../privacy-center/app/server-environment.ts | 4 ++ .../cypress/e2e/consent-banner.cy.ts | 72 +++++++++++++------ clients/privacy-center/pages/api/fides-js.ts | 1 + .../public/fides-js-components-demo.html | 1 + .../sample-app/src/components/Home/index.tsx | 7 +- .../src/components/Home/style.module.scss | 10 ++- 13 files changed, 167 insertions(+), 111 deletions(-) delete mode 100644 clients/fides-js/src/lib/consent-links.ts diff --git a/clients/fides-js/src/components/ConsentBanner.tsx b/clients/fides-js/src/components/ConsentBanner.tsx index a8b7b72f47..07d1d1f862 100644 --- a/clients/fides-js/src/components/ConsentBanner.tsx +++ b/clients/fides-js/src/components/ConsentBanner.tsx @@ -1,5 +1,4 @@ import { h, FunctionComponent } from "preact"; -import { useState, useEffect } from "preact/hooks"; import { ButtonType, ExperienceConfig } from "../lib/consent-types"; import Button from "./Button"; import { useHasMounted } from "../lib/hooks"; @@ -8,19 +7,18 @@ interface BannerProps { experience: ExperienceConfig; onAcceptAll: () => void; onRejectAll: () => void; - waitBeforeShow?: number; + onManagePreferences: () => void; managePreferencesLabel?: string; - onOpenModal: () => void; + bannerIsOpen: boolean; } const ConsentBanner: FunctionComponent = ({ experience, onAcceptAll, onRejectAll, - waitBeforeShow, - onOpenModal, + onManagePreferences, + bannerIsOpen, }) => { - const [isShown, setIsShown] = useState(false); const hasMounted = useHasMounted(); const { title = "Manage your consent", @@ -31,18 +29,6 @@ const ConsentBanner: FunctionComponent = ({ privacyPreferencesLabel = "Manage preferences", } = experience; - useEffect(() => { - const delayBanner = setTimeout(() => { - setIsShown(true); - }, waitBeforeShow); - return () => clearTimeout(delayBanner); - }, [setIsShown, waitBeforeShow]); - - const handleManagePreferencesClick = (): void => { - onOpenModal(); - setIsShown(false); - }; - if (!hasMounted) { return null; } @@ -51,7 +37,7 @@ const ConsentBanner: FunctionComponent = ({
@@ -70,7 +56,7 @@ const ConsentBanner: FunctionComponent = ({
diff --git a/clients/sample-app/src/components/Home/style.module.scss b/clients/sample-app/src/components/Home/style.module.scss index 978591b5cd..70bc23462a 100644 --- a/clients/sample-app/src/components/Home/style.module.scss +++ b/clients/sample-app/src/components/Home/style.module.scss @@ -80,8 +80,16 @@ } .modalLink { - display: none; margin-left: 1em; + background: none; + border: none; + padding: 0; + color: #5b301d; + text-decoration: underline; + cursor: pointer; + font-family: "Inter", sans-serif; + font-weight: 500; + font-size: 16px; } .select { From bf6111ea3ed903951a2cbdd609c773fd8a6e875f Mon Sep 17 00:00:00 2001 From: Allison King Date: Wed, 7 Jun 2023 12:40:46 -0400 Subject: [PATCH 16/24] Fix consent-banner cypress tests (#3487) Co-authored-by: eastandwestwind --- clients/fides-js/src/lib/consent-utils.ts | 6 ++---- .../cypress/e2e/consent-banner.cy.ts | 19 +++++++++---------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/clients/fides-js/src/lib/consent-utils.ts b/clients/fides-js/src/lib/consent-utils.ts index 19cc38685d..00139326cb 100644 --- a/clients/fides-js/src/lib/consent-utils.ts +++ b/clients/fides-js/src/lib/consent-utils.ts @@ -174,8 +174,6 @@ export const experienceIsValid = ( ); return false; } - // Check if there are any notices within the experience that do not have a user preference - return effectiveExperience.privacy_notices.some( - (notice) => notice.current_preference == null - ); + + return true; }; diff --git a/clients/privacy-center/cypress/e2e/consent-banner.cy.ts b/clients/privacy-center/cypress/e2e/consent-banner.cy.ts index de2efc2f95..1c13740648 100644 --- a/clients/privacy-center/cypress/e2e/consent-banner.cy.ts +++ b/clients/privacy-center/cypress/e2e/consent-banner.cy.ts @@ -543,7 +543,7 @@ describe("Consent banner", () => { }); it("does not set user consent preference automatically", () => { // timeout means API call not made, which is expected - Cypress.on("fail", (error) => { + cy.on("fail", (error) => { if (error.message.indexOf("Timed out retrying") !== 0) { throw error; } @@ -604,7 +604,7 @@ describe("Consent banner", () => { it("does not set user consent preference automatically", () => { // timeout means API call not made, which is expected - Cypress.on("fail", (error) => { + cy.on("fail", (error) => { if (error.message.indexOf("Timed out retrying") !== 0) { throw error; } @@ -658,7 +658,7 @@ describe("Consent banner", () => { }); cy.get("div#fides-banner").should("exist"); cy.contains("button", "Accept Test").should("exist"); - cy.get("div#fides-banner.fides-banner").within(() => { + cy.get("div#fides-banner").within(() => { cy.get( "div#fides-banner-description.fides-banner-description" ).contains( @@ -687,7 +687,7 @@ describe("Consent banner", () => { cy.wait("@getGeolocation"); cy.get("div#fides-banner").should("exist"); cy.contains("button", "Accept Test").should("exist"); - cy.get("div#fides-banner.fides-banner").within(() => { + cy.get("div#fides-banner").within(() => { cy.get( "div#fides-banner-description.fides-banner-description" ).contains( @@ -721,7 +721,7 @@ describe("Consent banner", () => { }); cy.get("div#fides-banner").should("exist"); cy.contains("button", "Accept Test").should("exist"); - cy.get("div#fides-banner.fides-banner").within(() => { + cy.get("div#fides-banner").within(() => { cy.get( "div#fides-banner-description.fides-banner-description" ).contains( @@ -787,7 +787,7 @@ describe("Consent banner", () => { }); cy.get("div#fides-banner").should("exist"); cy.contains("button", "Accept Test").should("exist"); - cy.get("div#fides-banner.fides-banner").within(() => { + cy.get("div#fides-banner").within(() => { cy.get( "div#fides-banner-description.fides-banner-description" ).contains( @@ -892,7 +892,7 @@ describe("Consent banner", () => { it("does not set user consent preference automatically", () => { // timeout means API call not made, which is expected - Cypress.on("fail", (error) => { + cy.on("fail", (error) => { if (error.message.indexOf("Timed out retrying") !== 0) { throw error; } @@ -946,12 +946,11 @@ describe("Consent banner", () => { }); it("closes banner and opens modal when modal link is clicked", () => { - cy.get("div#fides-banner").should("exist"); + cy.get("div#fides-banner").should("be.visible"); cy.contains("button", "Accept Test").should("exist"); cy.get("#fides-modal-link").click(); - - cy.get("div#fides-banner").should("not.exist"); + cy.get("div#fides-banner").should("not.be.visible"); cy.getByTestId("consent-modal"); }); From 9c5aca3d7923bf8d97b0e6043f178923d9527c63 Mon Sep 17 00:00:00 2001 From: Neville Samuell Date: Wed, 7 Jun 2023 15:41:39 -0400 Subject: [PATCH 17/24] Update Cookie House sample systems with targeted advertising data use --- .../data/sample_project/sample_resources/sample_systems.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/fides/data/sample_project/sample_resources/sample_systems.yml b/src/fides/data/sample_project/sample_resources/sample_systems.yml index 92f8503dd0..aeed75ad2e 100644 --- a/src/fides/data/sample_project/sample_resources/sample_systems.yml +++ b/src/fides/data/sample_project/sample_resources/sample_systems.yml @@ -51,7 +51,7 @@ system: privacy_declarations: - data_categories: - user.contact - data_use: marketing.advertising.first_party + data_use: marketing.advertising.first_party.targeted data_subjects: - customer data_qualifier: aggregated.anonymized.unlinked_pseudonymized.pseudonymized.identified @@ -60,7 +60,7 @@ system: - fides_key: cookie_house_marketing name: Cookie House Marketing System - description: Marketing application for audience analysis. + description: Marketing application for audience analysis, targeted ads, etc. system_type: Application administrating_department: Marketing data_responsibility_title: Processor @@ -69,7 +69,7 @@ system: privacy_declarations: - data_categories: - user.device.cookie_id - data_use: marketing.advertising.first_party + data_use: marketing.advertising.first_party.targeted data_subjects: - customer data_qualifier: aggregated.anonymized.unlinked_pseudonymized.pseudonymized.identified From ed1539571d93ac4aa07000b135e5c341930340d1 Mon Sep 17 00:00:00 2001 From: Allison King Date: Wed, 7 Jun 2023 17:45:11 -0400 Subject: [PATCH 18/24] Fix modal usability (#3480) --- clients/fides-js/package.json | 1 + .../fides-js/src/components/CloseButton.tsx | 32 +++++ .../fides-js/src/components/ConsentBanner.tsx | 5 +- .../fides-js/src/components/ConsentModal.tsx | 113 +++++++++--------- .../fides-js/src/components/NoticeToggles.tsx | 9 +- clients/fides-js/src/components/Overlay.tsx | 59 ++++++--- clients/fides-js/src/components/Toggle.tsx | 3 +- clients/fides-js/src/components/fides.css | 43 +++++-- clients/fides-js/src/lib/a11y-dialog.tsx | 65 ++++++++++ clients/fides-js/src/lib/consent-utils.ts | 12 +- clients/fides-js/src/lib/consent.tsx | 8 +- clients/fides-js/src/lib/hooks.ts | 3 +- clients/package-lock.json | 14 +++ .../cypress/e2e/consent-banner.cy.ts | 39 +++--- 14 files changed, 284 insertions(+), 122 deletions(-) create mode 100644 clients/fides-js/src/components/CloseButton.tsx create mode 100644 clients/fides-js/src/lib/a11y-dialog.tsx diff --git a/clients/fides-js/package.json b/clients/fides-js/package.json index 8500c52373..0ef55208cc 100644 --- a/clients/fides-js/package.json +++ b/clients/fides-js/package.json @@ -25,6 +25,7 @@ "directory": "clients/fides-js" }, "dependencies": { + "a11y-dialog": "^7.5.2", "preact": "^10.13.2", "react": "^17.0.2", "react-dom": "^17.0.2", diff --git a/clients/fides-js/src/components/CloseButton.tsx b/clients/fides-js/src/components/CloseButton.tsx new file mode 100644 index 0000000000..8bc9a9312b --- /dev/null +++ b/clients/fides-js/src/components/CloseButton.tsx @@ -0,0 +1,32 @@ +/* eslint-disable react/require-default-props */ +import { h } from "preact"; + +const CloseButton = ({ + onClick, + ariaLabel, +}: { + onClick?: () => void; + ariaLabel?: string; +}) => ( + +); + +export default CloseButton; diff --git a/clients/fides-js/src/components/ConsentBanner.tsx b/clients/fides-js/src/components/ConsentBanner.tsx index 07d1d1f862..a6a024ae9e 100644 --- a/clients/fides-js/src/components/ConsentBanner.tsx +++ b/clients/fides-js/src/components/ConsentBanner.tsx @@ -2,13 +2,14 @@ import { h, FunctionComponent } from "preact"; import { ButtonType, ExperienceConfig } from "../lib/consent-types"; import Button from "./Button"; import { useHasMounted } from "../lib/hooks"; +import CloseButton from "./CloseButton"; interface BannerProps { experience: ExperienceConfig; onAcceptAll: () => void; onRejectAll: () => void; onManagePreferences: () => void; - managePreferencesLabel?: string; + onClose: () => void; bannerIsOpen: boolean; } @@ -17,6 +18,7 @@ const ConsentBanner: FunctionComponent = ({ onAcceptAll, onRejectAll, onManagePreferences, + onClose, bannerIsOpen, }) => { const hasMounted = useHasMounted(); @@ -42,6 +44,7 @@ const ConsentBanner: FunctionComponent = ({ >
+
{title}
diff --git a/clients/fides-js/src/components/ConsentModal.tsx b/clients/fides-js/src/components/ConsentModal.tsx index 068786f588..5ef4370cb4 100644 --- a/clients/fides-js/src/components/ConsentModal.tsx +++ b/clients/fides-js/src/components/ConsentModal.tsx @@ -1,21 +1,18 @@ +/* eslint-disable react/jsx-props-no-spreading */ import { h } from "preact"; import { useMemo, useState } from "preact/hooks"; +import { Attributes } from "../lib/a11y-dialog"; +import Button from "./Button"; import { ButtonType, - ExperienceConfig, PrivacyNotice, + ExperienceConfig, } from "../lib/consent-types"; import NoticeToggles from "./NoticeToggles"; -import Button from "./Button"; +import CloseButton from "./CloseButton"; -/** - * TODO: a11y reqs - * 1. trap focus within the modal - * 2. add a close button? - * 3. figure out how clicking outside the modal should work a11y wise - * 4. ESC to close the dialog - */ const ConsentModal = ({ + attributes, experience, notices, onClose, @@ -23,6 +20,7 @@ const ConsentModal = ({ onAcceptAll, onRejectAll, }: { + attributes: Attributes; experience: ExperienceConfig; notices: PrivacyNotice[]; onClose: () => void; @@ -30,6 +28,8 @@ const ConsentModal = ({ onAcceptAll: () => void; onRejectAll: () => void; }) => { + const { container, overlay, dialog, title, closeButton } = attributes; + const initialEnabledNoticeKeys = useMemo( () => Object.keys(window.Fides.consent).filter( @@ -58,58 +58,53 @@ const ConsentModal = ({ }; return ( -
-