From e2277b6bcbabc379f209af340c373d3f289ffc39 Mon Sep 17 00:00:00 2001 From: Allison King Date: Thu, 28 Sep 2023 10:28:27 -0400 Subject: [PATCH 1/7] Change type of EnabledIds --- .../src/components/tcf/TcfOverlay.tsx | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/clients/fides-js/src/components/tcf/TcfOverlay.tsx b/clients/fides-js/src/components/tcf/TcfOverlay.tsx index 1fe9de3f561..57fcb4782d8 100644 --- a/clients/fides-js/src/components/tcf/TcfOverlay.tsx +++ b/clients/fides-js/src/components/tcf/TcfOverlay.tsx @@ -14,16 +14,16 @@ import Overlay from "../Overlay"; import { TcfConsentButtons } from "./TcfConsentButtons"; import { OverlayProps } from "../types"; -import type { - TCFFeatureRecord, - TCFFeatureSave, - TCFPurposeRecord, - TCFPurposeSave, - TCFSpecialFeatureSave, - TCFSpecialPurposeSave, - TCFVendorRecord, - TCFVendorSave, - TcfSavePreferences, +import { + type TCFFeatureRecord, + type TCFFeatureSave, + type TCFPurposeRecord, + type TCFPurposeSave, + type TCFSpecialFeatureSave, + type TCFSpecialPurposeSave, + type TCFVendorRecord, + type TCFVendorSave, + type TcfSavePreferences, } from "../../lib/tcf/types"; import { updateConsentPreferences } from "../../lib/preferences"; @@ -83,7 +83,8 @@ export interface EnabledIds { specialPurposes: string[]; features: string[]; specialFeatures: string[]; - vendors: string[]; + vendorsConsent: string[]; + vendorsLegint: string[]; systems: string[]; } @@ -130,7 +131,9 @@ const createTcfSavePayload = ({ }) as TCFSpecialFeatureSave[], vendor_preferences: transformTcfModelToTcfSave({ modelList: experience.tcf_vendors, - enabledIds: enabledIds.vendors, + // TODO: once the backend is storing this, we should send vendorsConsent + // and vendorsLegint to separate fields. + enabledIds: enabledIds.vendorsConsent, }) as TCFVendorSave[], system_preferences: transformTcfModelToTcfSave({ modelList: experience.tcf_systems, @@ -175,7 +178,9 @@ const TcfOverlay: FunctionComponent = ({ specialPurposes: getEnabledIds(specialPurposes), features: getEnabledIds(features), specialFeatures: getEnabledIds(specialFeatures), - vendors: getEnabledIds(vendors), + // TODO: make this right + vendorsConsent: getEnabledIds(vendors), + vendorsLegint: getEnabledIds(vendors), systems: getEnabledIds(systems), }; }, [experience]); From e6d237075943453009c364edc103f9cd025ba13f Mon Sep 17 00:00:00 2001 From: Allison King Date: Thu, 28 Sep 2023 15:48:45 -0400 Subject: [PATCH 2/7] Start supporting both consent and legint vendors --- .../fides-js/src/components/DataUseToggle.tsx | 4 + clients/fides-js/src/components/fides.css | 16 +++ .../src/components/tcf/TcfConsentButtons.tsx | 9 +- .../src/components/tcf/TcfOverlay.tsx | 101 +++++++++++++----- .../fides-js/src/components/tcf/TcfTabs.tsx | 10 +- .../src/components/tcf/TcfVendors.tsx | 90 ++++++++++++---- .../src/components/tcf/VendorInfoBanner.tsx | 43 ++------ clients/fides-js/src/lib/tcf/vendors.ts | 19 +++- 8 files changed, 204 insertions(+), 88 deletions(-) diff --git a/clients/fides-js/src/components/DataUseToggle.tsx b/clients/fides-js/src/components/DataUseToggle.tsx index 4cd9a3193dd..d51bf17d861 100644 --- a/clients/fides-js/src/components/DataUseToggle.tsx +++ b/clients/fides-js/src/components/DataUseToggle.tsx @@ -17,6 +17,7 @@ const DataUseToggle = ({ disabled, isHeader, includeToggle = true, + secondToggle, }: { dataUse: DataUse; checked: boolean; @@ -27,6 +28,7 @@ const DataUseToggle = ({ disabled?: boolean; isHeader?: boolean; includeToggle?: boolean; + secondToggle?: ComponentChildren; }) => { const { isOpen, @@ -36,6 +38,7 @@ const DataUseToggle = ({ } = useDisclosure({ id: dataUse.key, }); + console.log({ secondToggle }); const handleKeyDown = (event: KeyboardEvent) => { if (event.code === "Space" || event.code === "Enter") { @@ -81,6 +84,7 @@ const DataUseToggle = ({ disabled={disabled} /> ) : null} + {secondToggle || null} {children ?
{children}
: null} diff --git a/clients/fides-js/src/components/fides.css b/clients/fides-js/src/components/fides.css index 28bd399f9ce..3136b7729ae 100644 --- a/clients/fides-js/src/components/fides.css +++ b/clients/fides-js/src/components/fides.css @@ -649,6 +649,10 @@ div#fides-modal .fides-modal-button-group { align-items: center; } +.fides-margin-right { + margin-right: 0.2em; +} + /* TCF toggles */ .fides-tcf-toggle-content { margin-right: 60px; @@ -681,6 +685,18 @@ div#fides-modal .fides-modal-button-group { margin-left: 0; } +.fides-tcf-vendor-toggles { + display: flex; +} + +.fides-legal-basis-labels { + display: flex; + align-items: end; + justify-content: end; + font-size: 0.8em; + font-weight: 500; +} + /* Vendor purpose table */ .fides-vendor-details-table { width: 100%; diff --git a/clients/fides-js/src/components/tcf/TcfConsentButtons.tsx b/clients/fides-js/src/components/tcf/TcfConsentButtons.tsx index 1af50e76260..40a30674d76 100644 --- a/clients/fides-js/src/components/tcf/TcfConsentButtons.tsx +++ b/clients/fides-js/src/components/tcf/TcfConsentButtons.tsx @@ -45,8 +45,9 @@ export const TcfConsentButtons = ({ specialPurposes: getAllIds(experience.tcf_special_purposes), features: getAllIds(experience.tcf_features), specialFeatures: getAllIds(experience.tcf_special_features), - vendors: getAllIds(experience.tcf_vendors), - systems: getAllIds(experience.tcf_systems), + // TODO: separate these out once the backend can handle it + vendorsConsent: getAllIds(experience.tcf_vendors), + vendorsLegint: getAllIds(experience.tcf_vendors), }; onSave(allIds); }; @@ -56,8 +57,8 @@ export const TcfConsentButtons = ({ specialPurposes: [], features: [], specialFeatures: [], - vendors: [], - systems: [], + vendorsConsent: [], + vendorsLegint: [], }; onSave(emptyIds); }; diff --git a/clients/fides-js/src/components/tcf/TcfOverlay.tsx b/clients/fides-js/src/components/tcf/TcfOverlay.tsx index 57fcb4782d8..3414be05af8 100644 --- a/clients/fides-js/src/components/tcf/TcfOverlay.tsx +++ b/clients/fides-js/src/components/tcf/TcfOverlay.tsx @@ -15,6 +15,7 @@ import { TcfConsentButtons } from "./TcfConsentButtons"; import { OverlayProps } from "../types"; import { + LegalBasisForProcessingEnum, type TCFFeatureRecord, type TCFFeatureSave, type TCFPurposeRecord, @@ -31,6 +32,7 @@ import { ButtonType, ConsentMethod, PrivacyExperience, + UserConsentPreference, } from "../../lib/consent-types"; import { generateTcString } from "../../lib/tcf"; import { @@ -41,6 +43,7 @@ import InitialLayer from "./InitialLayer"; import TcfTabs from "./TcfTabs"; import Button from "../Button"; import VendorInfoBanner from "./VendorInfoBanner"; +import { vendorRecordsWithLegalBasis } from "../../lib/tcf/vendors"; const resolveConsentValueFromTcfModel = ( model: TCFPurposeRecord | TCFFeatureRecord | TCFVendorRecord @@ -78,6 +81,25 @@ const getEnabledIds = (modelList: TcfModels) => { .map((model) => `${model.id}`); }; +const getVendorEnabledIds = ( + modelList: TCFVendorRecord[] | undefined, + legalBasis: LegalBasisForProcessingEnum +) => { + if (!modelList) { + return []; + } + const records = vendorRecordsWithLegalBasis(modelList, legalBasis); + if (legalBasis === LegalBasisForProcessingEnum.LEGITIMATE_INTERESTS) { + // TODO: the backend should eventually return legint fields with a default proference of OPT_IN + const modifiedRecords = records.map((record) => ({ + ...record, + default_preference: UserConsentPreference.OPT_IN, + })); + return getEnabledIds(modifiedRecords); + } + return getEnabledIds(records); +}; + export interface EnabledIds { purposes: string[]; specialPurposes: string[]; @@ -85,7 +107,7 @@ export interface EnabledIds { specialFeatures: string[]; vendorsConsent: string[]; vendorsLegint: string[]; - systems: string[]; + // systems: string[]; } export interface UpdateEnabledIds { @@ -120,26 +142,49 @@ const createTcfSavePayload = ({ }: { experience: PrivacyExperience; enabledIds: EnabledIds; -}): TcfSavePreferences => ({ - purpose_preferences: transformTcfModelToTcfSave({ - modelList: experience.tcf_purposes, - enabledIds: enabledIds.purposes, - }) as TCFPurposeSave[], - special_feature_preferences: transformTcfModelToTcfSave({ - modelList: experience.tcf_special_features, - enabledIds: enabledIds.specialFeatures, - }) as TCFSpecialFeatureSave[], - vendor_preferences: transformTcfModelToTcfSave({ - modelList: experience.tcf_vendors, - // TODO: once the backend is storing this, we should send vendorsConsent - // and vendorsLegint to separate fields. - enabledIds: enabledIds.vendorsConsent, - }) as TCFVendorSave[], - system_preferences: transformTcfModelToTcfSave({ - modelList: experience.tcf_systems, - enabledIds: enabledIds.systems, - }) as TCFVendorSave[], -}); +}): TcfSavePreferences => { + const systemIds = experience.tcf_systems + ? experience.tcf_systems.map((s) => s.id) + : []; + // TODO: separate these out once the backend can support it + const enabledSystemIds: string[] = []; + const enabledVendorIds: string[] = []; + enabledIds.vendorsConsent.forEach((id) => { + if (systemIds.includes(id)) { + enabledSystemIds.push(id); + } else { + enabledVendorIds.push(id); + } + }); + enabledIds.vendorsLegint.forEach((id) => { + if (systemIds.includes(id)) { + enabledSystemIds.push(id); + } else { + enabledVendorIds.push(id); + } + }); + + return { + purpose_preferences: transformTcfModelToTcfSave({ + modelList: experience.tcf_purposes, + enabledIds: enabledIds.purposes, + }) as TCFPurposeSave[], + special_feature_preferences: transformTcfModelToTcfSave({ + modelList: experience.tcf_special_features, + enabledIds: enabledIds.specialFeatures, + }) as TCFSpecialFeatureSave[], + vendor_preferences: transformTcfModelToTcfSave({ + modelList: experience.tcf_vendors, + // TODO: once the backend is storing this, we should send vendorsConsent + // and vendorsLegint to separate fields. + enabledIds: enabledVendorIds, + }) as TCFVendorSave[], + system_preferences: transformTcfModelToTcfSave({ + modelList: experience.tcf_systems, + enabledIds: enabledSystemIds, + }) as TCFVendorSave[], + }; +}; const updateCookie = async ( oldCookie: FidesCookie, @@ -173,15 +218,21 @@ const TcfOverlay: FunctionComponent = ({ tcf_systems: systems, } = experience; + const vendorsAndSystems = [...(vendors || []), ...(systems || [])]; + return { purposes: getEnabledIds(purposes), specialPurposes: getEnabledIds(specialPurposes), features: getEnabledIds(features), specialFeatures: getEnabledIds(specialFeatures), - // TODO: make this right - vendorsConsent: getEnabledIds(vendors), - vendorsLegint: getEnabledIds(vendors), - systems: getEnabledIds(systems), + vendorsConsent: getVendorEnabledIds( + vendorsAndSystems, + LegalBasisForProcessingEnum.CONSENT + ), + vendorsLegint: getVendorEnabledIds( + vendorsAndSystems, + LegalBasisForProcessingEnum.LEGITIMATE_INTERESTS + ), }; }, [experience]); diff --git a/clients/fides-js/src/components/tcf/TcfTabs.tsx b/clients/fides-js/src/components/tcf/TcfTabs.tsx index 4e2f5d90fea..e531f717635 100644 --- a/clients/fides-js/src/components/tcf/TcfTabs.tsx +++ b/clients/fides-js/src/components/tcf/TcfTabs.tsx @@ -72,10 +72,12 @@ const TcfTabs = ({ purposes. diff --git a/clients/fides-js/src/components/tcf/TcfVendors.tsx b/clients/fides-js/src/components/tcf/TcfVendors.tsx index a96338c21be..59c7f126131 100644 --- a/clients/fides-js/src/components/tcf/TcfVendors.tsx +++ b/clients/fides-js/src/components/tcf/TcfVendors.tsx @@ -10,13 +10,18 @@ import { TCFVendorRecord, GvlVendorUrl, GvlDataDeclarations, + LegalBasisForProcessingEnum, } from "../../lib/tcf/types"; import { PrivacyExperience } from "../../lib/consent-types"; import { UpdateEnabledIds } from "./TcfOverlay"; import DataUseToggle from "../DataUseToggle"; import FilterButtons from "./FilterButtons"; -import { vendorIsGvl } from "../../lib/tcf/vendors"; +import { + vendorIsGvl, + vendorRecordsWithLegalBasis, +} from "../../lib/tcf/vendors"; import ExternalLink from "../ExternalLink"; +import Toggle from "../Toggle"; const FILTERS = [{ name: "All vendors" }, { name: "IAB TCF vendors" }]; @@ -157,35 +162,39 @@ const StorageDisclosure = ({ vendor }: { vendor: Vendor }) => { }; const TcfVendors = ({ - allVendors, - allSystems, - enabledVendorIds, - enabledSystemIds, + vendors, + enabledVendorConsentIds, + enabledVendorLegintIds, onChange, gvl, }: { - allVendors: PrivacyExperience["tcf_vendors"]; - allSystems: PrivacyExperience["tcf_systems"]; - enabledVendorIds: string[]; - enabledSystemIds: string[]; + vendors: PrivacyExperience["tcf_vendors"]; + enabledVendorConsentIds: string[]; + enabledVendorLegintIds: string[]; onChange: (payload: UpdateEnabledIds) => void; gvl?: GVLJson; }) => { const [isFiltered, setIsFiltered] = useState(false); - // Vendors and Systems are the same for the FE, but are 2 separate - // objects in the backend. We combine them here but keep them separate - // when patching preferences - const vendors = [...(allVendors || []), ...(allSystems || [])]; - const enabledIds = [...enabledVendorIds, ...enabledSystemIds]; - - if (vendors.length === 0) { + if (!vendors || vendors.length === 0) { // TODO: empty state? return null; } - const handleToggle = (vendor: TCFVendorRecord) => { - const modelType = vendor.has_vendor_id ? "vendors" : "systems"; + const handleToggle = ( + vendor: TCFVendorRecord, + legalBasis: + | LegalBasisForProcessingEnum.CONSENT + | LegalBasisForProcessingEnum.LEGITIMATE_INTERESTS + ) => { + const enabledIds = + legalBasis === LegalBasisForProcessingEnum.CONSENT + ? enabledVendorConsentIds + : enabledVendorLegintIds; + const modelType = + legalBasis === LegalBasisForProcessingEnum.CONSENT + ? "vendorsConsent" + : "vendorsLegint"; if (enabledIds.indexOf(vendor.id) !== -1) { onChange({ newEnabledIds: enabledIds.filter((e) => e !== vendor.id), @@ -214,6 +223,12 @@ const TcfVendors = ({ return (
+ {/* DEFER: ideally we use a table object, but then DataUseToggles would need to be reworked + or we would need a separate component. */} +
+ Legitimate interest + Consent +
{vendorsToDisplay.map((vendor) => { const gvlVendor = vendorIsGvl(vendor, gvl); // @ts-ignore the IAB-TCF lib doesn't support GVL v3 types yet @@ -223,14 +238,47 @@ const TcfVendors = ({ const dataCategories: GvlDataCategories | undefined = // @ts-ignore the IAB-TCF lib doesn't support GVL v3 types yet gvl?.dataCategories; + const isConsent = + vendorRecordsWithLegalBasis( + [vendor], + LegalBasisForProcessingEnum.CONSENT + ).length === 1; + const isLegint = + vendorRecordsWithLegalBasis( + [vendor], + LegalBasisForProcessingEnum.LEGITIMATE_INTERESTS + ).length === 1; return ( { - handleToggle(vendor); + handleToggle( + vendor, + LegalBasisForProcessingEnum.LEGITIMATE_INTERESTS + ); }} - checked={enabledIds.indexOf(vendor.id) !== -1} + checked={enabledVendorLegintIds.indexOf(vendor.id) !== -1} badge={gvlVendor ? "IAB TCF" : undefined} + secondToggle={ +
+ {isConsent ? ( + + handleToggle(vendor, LegalBasisForProcessingEnum.CONSENT) + } + /> + ) : null} +
+ } + includeToggle={isLegint} >
{gvlVendor ? : null} diff --git a/clients/fides-js/src/components/tcf/VendorInfoBanner.tsx b/clients/fides-js/src/components/tcf/VendorInfoBanner.tsx index d87d6dbd156..06cee4a575c 100644 --- a/clients/fides-js/src/components/tcf/VendorInfoBanner.tsx +++ b/clients/fides-js/src/components/tcf/VendorInfoBanner.tsx @@ -1,10 +1,8 @@ import { h } from "preact"; import { useMemo } from "preact/hooks"; import { PrivacyExperience } from "../../lib/consent-types"; -import { - LegalBasisForProcessingEnum, - TCFVendorRecord, -} from "../../lib/tcf/types"; +import { LegalBasisForProcessingEnum } from "../../lib/tcf/types"; +import { vendorRecordsWithLegalBasis } from "../../lib/tcf/vendors"; const VendorInfo = ({ label, @@ -31,23 +29,6 @@ const VendorInfo = ({
); -const countVendorRecordsWithLegalBasis = ( - records: TCFVendorRecord[], - legalBasis: LegalBasisForProcessingEnum -) => - records.filter((record) => { - const { purposes, special_purposes: specialPurposes } = record; - const hasApplicablePurposes = purposes?.filter((purpose) => - purpose.legal_bases?.includes(legalBasis) - ); - const hasApplicableSpecialPurposes = specialPurposes?.filter((purpose) => - purpose.legal_bases?.includes(legalBasis) - ); - return ( - hasApplicablePurposes?.length || hasApplicableSpecialPurposes?.length - ); - }).length; - const VendorInfoBanner = ({ experience, goToVendorTab, @@ -62,25 +43,21 @@ const VendorInfoBanner = ({ // consent count const consent = - countVendorRecordsWithLegalBasis( - systems, - LegalBasisForProcessingEnum.CONSENT - ) + - countVendorRecordsWithLegalBasis( - vendors, - LegalBasisForProcessingEnum.CONSENT - ); + vendorRecordsWithLegalBasis(systems, LegalBasisForProcessingEnum.CONSENT) + .length + + vendorRecordsWithLegalBasis(vendors, LegalBasisForProcessingEnum.CONSENT) + .length; // legint count const legint = - countVendorRecordsWithLegalBasis( + vendorRecordsWithLegalBasis( systems, LegalBasisForProcessingEnum.LEGITIMATE_INTERESTS - ) + - countVendorRecordsWithLegalBasis( + ).length + + vendorRecordsWithLegalBasis( vendors, LegalBasisForProcessingEnum.LEGITIMATE_INTERESTS - ); + ).length; return { total, consent, legint }; }, [experience]); diff --git a/clients/fides-js/src/lib/tcf/vendors.ts b/clients/fides-js/src/lib/tcf/vendors.ts index 2d8f8158509..fabe3ff279a 100644 --- a/clients/fides-js/src/lib/tcf/vendors.ts +++ b/clients/fides-js/src/lib/tcf/vendors.ts @@ -1,4 +1,4 @@ -import { GVLJson, TCFVendorRecord } from "./types"; +import { GVLJson, LegalBasisForProcessingEnum, TCFVendorRecord } from "./types"; export const vendorIsGvl = ( vendor: Pick, @@ -9,3 +9,20 @@ export const vendorIsGvl = ( } return gvl.vendors[vendor.id]; }; + +export const vendorRecordsWithLegalBasis = ( + records: TCFVendorRecord[], + legalBasis: LegalBasisForProcessingEnum +) => + records.filter((record) => { + const { purposes, special_purposes: specialPurposes } = record; + const hasApplicablePurposes = purposes?.filter((purpose) => + purpose.legal_bases?.includes(legalBasis) + ); + const hasApplicableSpecialPurposes = specialPurposes?.filter((purpose) => + purpose.legal_bases?.includes(legalBasis) + ); + return ( + hasApplicablePurposes?.length || hasApplicableSpecialPurposes?.length + ); + }); From a73c13dcfcaedbf0bcafb8be6247f2d8665b117b Mon Sep 17 00:00:00 2001 From: Allison King Date: Thu, 28 Sep 2023 20:26:51 -0400 Subject: [PATCH 3/7] Store vendor consents in TC string and read from it --- .../fides-js/src/components/DataUseToggle.tsx | 1 - .../src/components/tcf/TcfOverlay.tsx | 82 ++++++++--- clients/fides-js/src/fides-tcf.ts | 93 ++++++------ clients/fides-js/src/lib/tcf.ts | 136 ++++++++---------- clients/fides-js/src/lib/tcf/types.ts | 9 ++ 5 files changed, 178 insertions(+), 143 deletions(-) diff --git a/clients/fides-js/src/components/DataUseToggle.tsx b/clients/fides-js/src/components/DataUseToggle.tsx index d51bf17d861..dd84874dbbd 100644 --- a/clients/fides-js/src/components/DataUseToggle.tsx +++ b/clients/fides-js/src/components/DataUseToggle.tsx @@ -38,7 +38,6 @@ const DataUseToggle = ({ } = useDisclosure({ id: dataUse.key, }); - console.log({ secondToggle }); const handleKeyDown = (event: KeyboardEvent) => { if (event.code === "Space" || event.code === "Enter") { diff --git a/clients/fides-js/src/components/tcf/TcfOverlay.tsx b/clients/fides-js/src/components/tcf/TcfOverlay.tsx index 3414be05af8..4bac5749856 100644 --- a/clients/fides-js/src/components/tcf/TcfOverlay.tsx +++ b/clients/fides-js/src/components/tcf/TcfOverlay.tsx @@ -1,5 +1,6 @@ import { h, FunctionComponent } from "preact"; import { useState, useCallback, useMemo } from "preact/hooks"; +import { TCString } from "@iabtechlabtcf/core"; import ConsentBanner from "../ConsentBanner"; import { @@ -15,6 +16,7 @@ import { TcfConsentButtons } from "./TcfConsentButtons"; import { OverlayProps } from "../types"; import { + EnabledIds, LegalBasisForProcessingEnum, type TCFFeatureRecord, type TCFFeatureSave, @@ -90,7 +92,7 @@ const getVendorEnabledIds = ( } const records = vendorRecordsWithLegalBasis(modelList, legalBasis); if (legalBasis === LegalBasisForProcessingEnum.LEGITIMATE_INTERESTS) { - // TODO: the backend should eventually return legint fields with a default proference of OPT_IN + // TODO: the backend should eventually return legint fields with a default preference of OPT_IN const modifiedRecords = records.map((record) => ({ ...record, default_preference: UserConsentPreference.OPT_IN, @@ -100,16 +102,6 @@ const getVendorEnabledIds = ( return getEnabledIds(records); }; -export interface EnabledIds { - purposes: string[]; - specialPurposes: string[]; - features: string[]; - specialFeatures: string[]; - vendorsConsent: string[]; - vendorsLegint: string[]; - // systems: string[]; -} - export interface UpdateEnabledIds { newEnabledIds: string[]; modelType: keyof EnabledIds; @@ -143,10 +135,11 @@ const createTcfSavePayload = ({ experience: PrivacyExperience; enabledIds: EnabledIds; }): TcfSavePreferences => { + // Because systems were combined with vendors to make the UI easier to work with, + // we need to separate them out now (the backend treats them as separate entities). const systemIds = experience.tcf_systems ? experience.tcf_systems.map((s) => s.id) : []; - // TODO: separate these out once the backend can support it const enabledSystemIds: string[] = []; const enabledVendorIds: string[] = []; enabledIds.vendorsConsent.forEach((id) => { @@ -188,11 +181,20 @@ const createTcfSavePayload = ({ const updateCookie = async ( oldCookie: FidesCookie, + /** + * `tcf` and `enabledIds` should represent the same data, where `tcf` is what is + * sent to the backend, and `enabledIds` is what the FE uses. They have diverged + * because the backend has not implemented separate vendor legint/consents yet. + * Therefore, we need both entities right now, but eventually we should be able to + * only use one. In other words, `enabledIds` has a field for `vendorsConsent` and + * `vendorsLegint` but `tcf` only has `vendors`. + */ tcf: TcfSavePreferences, + enabledIds: EnabledIds, experience: PrivacyExperience ): Promise => { const tcString = await generateTcString({ - tcStringPreferences: tcf, + tcStringPreferences: enabledIds, experience, }); return { @@ -219,22 +221,54 @@ const TcfOverlay: FunctionComponent = ({ } = experience; const vendorsAndSystems = [...(vendors || []), ...(systems || [])]; + let vendorsConsent = getVendorEnabledIds( + vendorsAndSystems, + LegalBasisForProcessingEnum.CONSENT + ); + let vendorsLegint = getVendorEnabledIds( + vendorsAndSystems, + LegalBasisForProcessingEnum.LEGITIMATE_INTERESTS + ); + + // Initialize vendor values from the TC string if it's available. The cookie only + // stores what the backend has, while the TC string has a little more right now. + // (fidesplus#1128) + if (cookie.tc_string && cookie.tc_string !== "") { + const tcModel = TCString.decode(cookie.tc_string || ""); + vendorsConsent = []; + vendorsLegint = []; + tcModel.vendorConsents.forEach((consented, id) => { + if (consented) { + vendorsConsent.push(`${id}`); + } + }); + tcModel.vendorLegitimateInterests.forEach((consented, id) => { + if (consented) { + vendorsLegint.push(`${id}`); + } + }); + // but we still need to join system data to this + const systemConsents = getVendorEnabledIds( + systems, + LegalBasisForProcessingEnum.CONSENT + ); + const systemLegints = getVendorEnabledIds( + systems, + LegalBasisForProcessingEnum.LEGITIMATE_INTERESTS + ); + vendorsConsent = [...vendorsConsent, ...systemConsents]; + vendorsLegint = [...vendorsLegint, ...systemLegints]; + } return { purposes: getEnabledIds(purposes), specialPurposes: getEnabledIds(specialPurposes), features: getEnabledIds(features), specialFeatures: getEnabledIds(specialFeatures), - vendorsConsent: getVendorEnabledIds( - vendorsAndSystems, - LegalBasisForProcessingEnum.CONSENT - ), - vendorsLegint: getVendorEnabledIds( - vendorsAndSystems, - LegalBasisForProcessingEnum.LEGITIMATE_INTERESTS - ), + vendorsConsent, + vendorsLegint, }; - }, [experience]); + }, [experience, cookie]); const [draftIds, setDraftIds] = useState(initialEnabledIds); @@ -254,6 +288,7 @@ const TcfOverlay: FunctionComponent = ({ const handleUpdateAllPreferences = useCallback( (enabledIds: EnabledIds) => { const tcf = createTcfSavePayload({ experience, enabledIds }); + console.log({ tcf }); updateConsentPreferences({ consentPreferencesToSave: [], experienceId: experience.id, @@ -264,7 +299,8 @@ const TcfOverlay: FunctionComponent = ({ debug: options.debug, servedNotices: null, // TODO: served notices tcf, - updateCookie: (oldCookie) => updateCookie(oldCookie, tcf, experience), + updateCookie: (oldCookie) => + updateCookie(oldCookie, tcf, enabledIds, experience), }); setDraftIds(enabledIds); }, diff --git a/clients/fides-js/src/fides-tcf.ts b/clients/fides-js/src/fides-tcf.ts index 5b0f6b2b4c3..62f4be74684 100644 --- a/clients/fides-js/src/fides-tcf.ts +++ b/clients/fides-js/src/fides-tcf.ts @@ -50,13 +50,9 @@ import { gtm } from "./integrations/gtm"; import { meta } from "./integrations/meta"; import { shopify } from "./integrations/shopify"; -import { - FidesConfig, - PrivacyExperience, - UserConsentPreference, -} from "./lib/consent-types"; +import { FidesConfig, PrivacyExperience } from "./lib/consent-types"; -import { generateTcString, tcf } from "./lib/tcf"; +import { tcf } from "./lib/tcf"; import { getInitialCookie, getInitialFides, @@ -64,14 +60,8 @@ import { } from "./lib/initialize"; import type { Fides } from "./lib/initialize"; import { dispatchFidesEvent } from "./lib/events"; -import { - FidesCookie, - hasSavedTcfPreferences, - isNewFidesCookie, - transformTcfPreferencesToCookieKeys, -} from "./fides"; +import { FidesCookie, hasSavedTcfPreferences, isNewFidesCookie } from "./fides"; import { renderOverlay } from "./lib/tcf/renderOverlay"; -import { TCFPurposeRecord, TcfSavePreferences } from "./lib/tcf/types"; declare global { interface Window { @@ -90,15 +80,27 @@ declare global { // eslint-disable-next-line no-underscore-dangle,@typescript-eslint/naming-convention let _Fides: Fides; -/** Helper function to determine the initial value of a TCF object */ -const getInitialPreference = ( - tcfObject: Pick -) => { - if (tcfObject.current_preference) { - return tcfObject.current_preference; - } - return tcfObject.default_preference ?? UserConsentPreference.OPT_OUT; -}; +// /** Helper function to determine the initial value of a TCF object */ +// const getInitialEnabledIds = ( +// tcfRecords: +// | Pick< +// TCFPurposeRecord, +// "id" | "current_preference" | "default_preference" +// >[] +// | undefined +// ) => { +// if (!tcfRecords) { +// return []; +// } +// // TODO: take legal basis into account +// return tcfRecords +// .filter( +// (record) => +// record.current_preference === UserConsentPreference.OPT_IN || +// record.current_preference === UserConsentPreference.ACKNOWLEDGE +// ) +// .map((record) => `${record.id}`); +// }; const updateCookie = async ( oldCookie: FidesCookie, @@ -109,30 +111,31 @@ const updateCookie = async ( return { ...oldCookie, tc_string: "" }; } - const tcStringPreferences: TcfSavePreferences = { - purpose_preferences: experience.tcf_purposes?.map((purpose) => ({ - id: purpose.id, - preference: getInitialPreference(purpose), - })), - special_feature_preferences: experience.tcf_special_features?.map( - (feature) => ({ - id: feature.id, - preference: getInitialPreference(feature), - }) - ), - vendor_preferences: experience.tcf_vendors?.map((vendor) => ({ - id: vendor.id, - preference: getInitialPreference(vendor), - })), - system_preferences: experience.tcf_systems?.map((system) => ({ - id: system.id, - preference: getInitialPreference(system), - })), - }; + // const tcStringPreferences: TcfSavePreferences = { + // purpose_preferences: experience.tcf_purposes?.map((purpose) => ({ + // id: purpose.id, + // preference: getInitialPreference(purpose), + // })), + // special_feature_preferences: experience.tcf_special_features?.map( + // (feature) => ({ + // id: feature.id, + // preference: getInitialPreference(feature), + // }) + // ), + // vendor_preferences: experience.tcf_vendors?.map((vendor) => ({ + // id: vendor.id, + // preference: getInitialPreference(vendor), + // })), + // system_preferences: experience.tcf_systems?.map((system) => ({ + // id: system.id, + // preference: getInitialPreference(system), + // })), + // }; - const tcString = await generateTcString({ tcStringPreferences, experience }); - const tcfConsent = transformTcfPreferencesToCookieKeys(tcStringPreferences); - return { ...oldCookie, tc_string: tcString, tcf_consent: tcfConsent }; + // const tcString = await generateTcString({ tcStringPreferences, experience }); + // const tcfConsent = transformTcfPreferencesToCookieKeys(tcStringPreferences); + // return { ...oldCookie, tc_string: tcString, tcf_consent: tcfConsent }; + return oldCookie; }; /** diff --git a/clients/fides-js/src/lib/tcf.ts b/clients/fides-js/src/lib/tcf.ts index 726a85a4bad..f9deff3cb15 100644 --- a/clients/fides-js/src/lib/tcf.ts +++ b/clients/fides-js/src/lib/tcf.ts @@ -8,11 +8,11 @@ import { CmpApi } from "@iabtechlabtcf/cmpapi"; import { TCModel, TCString, GVL } from "@iabtechlabtcf/core"; import { makeStub } from "./tcf/stub"; -import { transformUserPreferenceToBoolean } from "./consent-utils"; + import { + EnabledIds, LegalBasisForProcessingEnum, TCFPurposeRecord, - TcfSavePreferences, } from "./tcf/types"; import { vendorIsGvl } from "./tcf/vendors"; import { PrivacyExperience } from "./consent-types"; @@ -48,7 +48,7 @@ export const generateTcString = async ({ experience, tcStringPreferences, }: { - tcStringPreferences?: TcfSavePreferences; + tcStringPreferences?: EnabledIds; experience: PrivacyExperience; }): Promise => { let encodedString = ""; @@ -64,94 +64,82 @@ export const generateTcString = async ({ if (tcStringPreferences) { if ( - tcStringPreferences.vendor_preferences && - tcStringPreferences.vendor_preferences.length > 0 + tcStringPreferences.vendorsConsent && + tcStringPreferences.vendorsConsent.length > 0 ) { - tcStringPreferences.vendor_preferences.forEach((vendorPreference) => { - const consented = transformUserPreferenceToBoolean( - vendorPreference.preference - ); - if (consented && vendorIsGvl(vendorPreference, experience.gvl)) { - tcModel.vendorConsents.set(+vendorPreference.id); - const thisVendor = experience.tcf_vendors?.filter( - (v) => v.id === vendorPreference.id - )[0]; - const vendorPurposes = thisVendor?.purposes; - // Handle the case where a vendor has forbidden legint purposes set - let skipSetLegInt = false; - if (vendorPurposes) { - const legIntPurposeIds = vendorPurposes - .filter((p) => - p.legal_bases?.includes( - LegalBasisForProcessingEnum.LEGITIMATE_INTERESTS - ) + tcStringPreferences.vendorsConsent.forEach((vendorId) => { + if (vendorIsGvl({ id: vendorId }, experience.gvl)) { + tcModel.vendorConsents.set(+vendorId); + } + }); + tcStringPreferences.vendorsLegint.forEach((vendorId) => { + const thisVendor = experience.tcf_vendors?.filter( + (v) => v.id === vendorId + )[0]; + + const vendorPurposes = thisVendor?.purposes; + // Handle the case where a vendor has forbidden legint purposes set + let skipSetLegInt = false; + if (vendorPurposes) { + const legIntPurposeIds = vendorPurposes + .filter((p) => + p.legal_bases?.includes( + LegalBasisForProcessingEnum.LEGITIMATE_INTERESTS ) - .map((p) => p.id); - if ( - legIntPurposeIds.filter((id) => - FORBIDDEN_LEGITIMATE_INTEREST_PURPOSE_IDS.includes(id) - ).length - ) { - skipSetLegInt = true; - } - } - if (!skipSetLegInt) { - tcModel.vendorLegitimateInterests.set(+vendorPreference.id); + ) + .map((p) => p.id); + if ( + legIntPurposeIds.filter((id) => + FORBIDDEN_LEGITIMATE_INTEREST_PURPOSE_IDS.includes(id) + ).length + ) { + skipSetLegInt = true; } } + if (!skipSetLegInt) { + tcModel.vendorLegitimateInterests.set(+vendorId); + } }); } // Set purpose consent on tcModel if ( - tcStringPreferences.purpose_preferences && - tcStringPreferences.purpose_preferences.length > 0 + tcStringPreferences.purposes && + tcStringPreferences.purposes.length > 0 ) { - tcStringPreferences.purpose_preferences.forEach((purposePreference) => { - const consented = transformUserPreferenceToBoolean( - purposePreference.preference - ); - if (consented) { - const id = +purposePreference.id; - if ( - purposeHasLegalBasis({ - id, - purposes: experience.tcf_purposes, - legalBasis: LegalBasisForProcessingEnum.CONSENT, - }) - ) { - tcModel.purposeConsents.set(id); - } - if ( - purposeHasLegalBasis({ - id, - purposes: experience.tcf_purposes, - legalBasis: LegalBasisForProcessingEnum.LEGITIMATE_INTERESTS, - }) && - // per the IAB, make sure we never set purposes 1, 3, 4, 5, or 6 - !FORBIDDEN_LEGITIMATE_INTEREST_PURPOSE_IDS.includes(id) - ) { - tcModel.purposeLegitimateInterests.set(id); - } + tcStringPreferences.purposes.forEach((purposeId) => { + const id = +purposeId; + if ( + purposeHasLegalBasis({ + id, + purposes: experience.tcf_purposes, + legalBasis: LegalBasisForProcessingEnum.CONSENT, + }) + ) { + tcModel.purposeConsents.set(id); + } + if ( + purposeHasLegalBasis({ + id, + purposes: experience.tcf_purposes, + legalBasis: LegalBasisForProcessingEnum.LEGITIMATE_INTERESTS, + }) && + // per the IAB, make sure we never set purposes 1, 3, 4, 5, or 6 + !FORBIDDEN_LEGITIMATE_INTEREST_PURPOSE_IDS.includes(id) + ) { + tcModel.purposeLegitimateInterests.set(id); } }); } // Set special feature opt-ins on tcModel if ( - tcStringPreferences.special_feature_preferences && - tcStringPreferences.special_feature_preferences.length > 0 + tcStringPreferences.specialFeatures && + tcStringPreferences.specialFeatures.length > 0 ) { - tcStringPreferences.special_feature_preferences.forEach( - (specialFeaturePreference) => { - const consented = transformUserPreferenceToBoolean( - specialFeaturePreference.preference - ); - if (consented) { - tcModel.specialFeatureOptins.set(+specialFeaturePreference.id); - } - } - ); + tcStringPreferences.specialFeatures.forEach((id) => { + tcModel.specialFeatureOptins.set(+id); + }); } // note that we cannot set consent for special purposes nor features because the IAB policy states diff --git a/clients/fides-js/src/lib/tcf/types.ts b/clients/fides-js/src/lib/tcf/types.ts index 5c2ae733e51..57803184e03 100644 --- a/clients/fides-js/src/lib/tcf/types.ts +++ b/clients/fides-js/src/lib/tcf/types.ts @@ -169,6 +169,15 @@ export enum LegalBasisForProcessingEnum { LEGITIMATE_INTERESTS = "Legitimate interests", } +export interface EnabledIds { + purposes: string[]; + specialPurposes: string[]; + features: string[]; + specialFeatures: string[]; + vendorsConsent: string[]; + vendorsLegint: string[]; +} + export type GVLJson = Pick< GVL, | "gvlSpecificationVersion" From 658620f6973a8ff5cab29e2d5c10f3d6cf0d5853 Mon Sep 17 00:00:00 2001 From: Allison King Date: Thu, 28 Sep 2023 21:32:52 -0400 Subject: [PATCH 4/7] Small fixes --- .../src/components/tcf/TcfConsentButtons.tsx | 12 ++++++++---- clients/fides-js/src/components/tcf/TcfOverlay.tsx | 5 ++--- .../cypress/e2e/consent-banner-tcf.cy.ts | 2 +- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/clients/fides-js/src/components/tcf/TcfConsentButtons.tsx b/clients/fides-js/src/components/tcf/TcfConsentButtons.tsx index 40a30674d76..abee0121776 100644 --- a/clients/fides-js/src/components/tcf/TcfConsentButtons.tsx +++ b/clients/fides-js/src/components/tcf/TcfConsentButtons.tsx @@ -2,7 +2,7 @@ import { VNode, h } from "preact"; import { PrivacyExperience } from "../../lib/consent-types"; import { ConsentButtons } from "../ConsentButtons"; -import type { EnabledIds } from "./TcfOverlay"; +import type { EnabledIds } from "../../lib/tcf/types"; import { TCFPurposeRecord, TCFFeatureRecord, @@ -40,14 +40,18 @@ export const TcfConsentButtons = ({ } const handleAcceptAll = () => { + const vendorsAndSystems = [ + ...(experience.tcf_vendors || []), + ...(experience.tcf_systems || []), + ]; const allIds: EnabledIds = { purposes: getAllIds(experience.tcf_purposes), specialPurposes: getAllIds(experience.tcf_special_purposes), features: getAllIds(experience.tcf_features), specialFeatures: getAllIds(experience.tcf_special_features), - // TODO: separate these out once the backend can handle it - vendorsConsent: getAllIds(experience.tcf_vendors), - vendorsLegint: getAllIds(experience.tcf_vendors), + // TODO: make these read from separate fields once the backend supports it (fidesplus1128) + vendorsConsent: getAllIds(vendorsAndSystems), + vendorsLegint: getAllIds(vendorsAndSystems), }; onSave(allIds); }; diff --git a/clients/fides-js/src/components/tcf/TcfOverlay.tsx b/clients/fides-js/src/components/tcf/TcfOverlay.tsx index 4bac5749856..79b48db6d08 100644 --- a/clients/fides-js/src/components/tcf/TcfOverlay.tsx +++ b/clients/fides-js/src/components/tcf/TcfOverlay.tsx @@ -16,7 +16,7 @@ import { TcfConsentButtons } from "./TcfConsentButtons"; import { OverlayProps } from "../types"; import { - EnabledIds, + type EnabledIds, LegalBasisForProcessingEnum, type TCFFeatureRecord, type TCFFeatureSave, @@ -169,7 +169,7 @@ const createTcfSavePayload = ({ vendor_preferences: transformTcfModelToTcfSave({ modelList: experience.tcf_vendors, // TODO: once the backend is storing this, we should send vendorsConsent - // and vendorsLegint to separate fields. + // and vendorsLegint to separate fields (fidesplus1128) enabledIds: enabledVendorIds, }) as TCFVendorSave[], system_preferences: transformTcfModelToTcfSave({ @@ -288,7 +288,6 @@ const TcfOverlay: FunctionComponent = ({ const handleUpdateAllPreferences = useCallback( (enabledIds: EnabledIds) => { const tcf = createTcfSavePayload({ experience, enabledIds }); - console.log({ tcf }); updateConsentPreferences({ consentPreferencesToSave: [], experienceId: experience.id, diff --git a/clients/privacy-center/cypress/e2e/consent-banner-tcf.cy.ts b/clients/privacy-center/cypress/e2e/consent-banner-tcf.cy.ts index 557e0cfa6d5..5154d86a13c 100644 --- a/clients/privacy-center/cypress/e2e/consent-banner-tcf.cy.ts +++ b/clients/privacy-center/cypress/e2e/consent-banner-tcf.cy.ts @@ -180,7 +180,7 @@ describe("Fides-js TCF", () => { }); cy.get("#fides-tab-Vendors"); cy.getByTestId(`toggle-${VENDOR_1.name}`); - cy.getByTestId(`toggle-${VENDOR_2.name}`); + cy.getByTestId(`toggle-${VENDOR_2.name}-consent`); }); }); From 46c8b4c0fb318f02c8c82d8f909308bcd246909a Mon Sep 17 00:00:00 2001 From: Allison King Date: Thu, 28 Sep 2023 21:43:23 -0400 Subject: [PATCH 5/7] Fix if placement --- clients/fides-js/src/lib/tcf.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/clients/fides-js/src/lib/tcf.ts b/clients/fides-js/src/lib/tcf.ts index f9deff3cb15..61959b55cf4 100644 --- a/clients/fides-js/src/lib/tcf.ts +++ b/clients/fides-js/src/lib/tcf.ts @@ -95,9 +95,9 @@ export const generateTcString = async ({ ) { skipSetLegInt = true; } - } - if (!skipSetLegInt) { - tcModel.vendorLegitimateInterests.set(+vendorId); + if (!skipSetLegInt) { + tcModel.vendorLegitimateInterests.set(+vendorId); + } } }); } From 076fe7e4a08428401170baba9fc17fad9aefe210 Mon Sep 17 00:00:00 2001 From: Allison King Date: Mon, 2 Oct 2023 10:38:01 -0400 Subject: [PATCH 6/7] Update comments --- .../src/components/tcf/TcfOverlay.tsx | 6 +-- clients/fides-js/src/fides-tcf.ts | 51 ++----------------- 2 files changed, 8 insertions(+), 49 deletions(-) diff --git a/clients/fides-js/src/components/tcf/TcfOverlay.tsx b/clients/fides-js/src/components/tcf/TcfOverlay.tsx index 2429905e4ab..c747d6c4a6e 100644 --- a/clients/fides-js/src/components/tcf/TcfOverlay.tsx +++ b/clients/fides-js/src/components/tcf/TcfOverlay.tsx @@ -230,9 +230,9 @@ const TcfOverlay: FunctionComponent = ({ LegalBasisForProcessingEnum.LEGITIMATE_INTERESTS ); - // Initialize vendor values from the TC string if it's available. The cookie only - // stores what the backend has, while the TC string has a little more right now. - // (fidesplus#1128) + // Initialize vendor values from the TC string if it's available. Neither the + // backend nor the cookie store vendorsConsent or vendorsLegint yet, so we must + // look at the string. (fidesplus#1128) if (cookie.tc_string && cookie.tc_string !== "") { const tcModel = TCString.decode(cookie.tc_string || ""); vendorsConsent = []; diff --git a/clients/fides-js/src/fides-tcf.ts b/clients/fides-js/src/fides-tcf.ts index 62f4be74684..ef64194ca3c 100644 --- a/clients/fides-js/src/fides-tcf.ts +++ b/clients/fides-js/src/fides-tcf.ts @@ -80,28 +80,6 @@ declare global { // eslint-disable-next-line no-underscore-dangle,@typescript-eslint/naming-convention let _Fides: Fides; -// /** Helper function to determine the initial value of a TCF object */ -// const getInitialEnabledIds = ( -// tcfRecords: -// | Pick< -// TCFPurposeRecord, -// "id" | "current_preference" | "default_preference" -// >[] -// | undefined -// ) => { -// if (!tcfRecords) { -// return []; -// } -// // TODO: take legal basis into account -// return tcfRecords -// .filter( -// (record) => -// record.current_preference === UserConsentPreference.OPT_IN || -// record.current_preference === UserConsentPreference.ACKNOWLEDGE -// ) -// .map((record) => `${record.id}`); -// }; - const updateCookie = async ( oldCookie: FidesCookie, experience: PrivacyExperience @@ -111,30 +89,11 @@ const updateCookie = async ( return { ...oldCookie, tc_string: "" }; } - // const tcStringPreferences: TcfSavePreferences = { - // purpose_preferences: experience.tcf_purposes?.map((purpose) => ({ - // id: purpose.id, - // preference: getInitialPreference(purpose), - // })), - // special_feature_preferences: experience.tcf_special_features?.map( - // (feature) => ({ - // id: feature.id, - // preference: getInitialPreference(feature), - // }) - // ), - // vendor_preferences: experience.tcf_vendors?.map((vendor) => ({ - // id: vendor.id, - // preference: getInitialPreference(vendor), - // })), - // system_preferences: experience.tcf_systems?.map((system) => ({ - // id: system.id, - // preference: getInitialPreference(system), - // })), - // }; - - // const tcString = await generateTcString({ tcStringPreferences, experience }); - // const tcfConsent = transformTcfPreferencesToCookieKeys(tcStringPreferences); - // return { ...oldCookie, tc_string: tcString, tcf_consent: tcfConsent }; + // Usually at this point, we'd look at the Experience from the backend and update + // the user's browser cookie to match the preferences in the Experience. However, + // TCF requires pre-fetching an experience. A prefetch'd experience will never have user + // specific consents. We rely on the cookie to fill those in. Therefore, we do nothing + // here, since the cookie is the source of truth, not the backend Experience. return oldCookie; }; From be5a1272f4ac2a3c096a2cb51f769d21467d3b85 Mon Sep 17 00:00:00 2001 From: Allison King Date: Mon, 2 Oct 2023 10:39:04 -0400 Subject: [PATCH 7/7] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 070a9ccbea3..5176eefcf19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,9 @@ The types of changes are: ### Added - Added an option to link to vendor tab from an experience config description [#4191](https://github.com/ethyca/fides/pull/4191) +- Added two toggles for vendors in the TCF overlay, one for Consent, and one for Legitimate Interest [#4189](https://github.com/ethyca/fides/pull/4189) + + ### Changed - Removed `TCF_ENABLED` environment variable from the privacy center in favor of dynamically figuring out which `fides-js` bundle to send [#4131](https://github.com/ethyca/fides/pull/4131) - Updated copy of info boxes on each TCF tab [#4191](https://github.com/ethyca/fides/pull/4191)