Skip to content

Commit

Permalink
Returning GVL.JSON through Experiences API (#4143)
Browse files Browse the repository at this point in the history
Co-authored-by: Allison King <[email protected]>
Co-authored-by: Allison King <[email protected]>
  • Loading branch information
3 people authored Sep 26, 2023
1 parent 57493f6 commit 5c741f1
Show file tree
Hide file tree
Showing 17 changed files with 20,937 additions and 5,047 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,14 @@ The types of changes are:
- Fides-js can now display preliminary TCF data [#3879](https://github.com/ethyca/fides/pull/3879)
- Fides-js can persist TCF preferences to the backend [#3887](https://github.com/ethyca/fides/pull/3887)
- TCF modal now supports setting legitimate interest fields [#4037](https://github.com/ethyca/fides/pull/4037)
- Embed the GVL in the GET Experiences response [#4143](https://github.com/ethyca/fides/pull/4143)
- Button to view how many vendors and to open the vendor tab in the TCF modal [#4144](https://github.com/ethyca/fides/pull/4144)

### Changed
- Added further config options to customize the privacy center [#4090](https://github.com/ethyca/fides/pull/4090)
- CORS configuration page [#4073](https://github.com/ethyca/fides/pull/4073)
- Refactored `fides.js` components so that they can take data structures that are not necessarily privacy notices [#3870](https://github.com/ethyca/fides/pull/3870)
- Use hosted GVL.json from the backend [#4159](https://github.com/ethyca/fides/pull/4159)
- Features and Special Purposes in the TCF modal do not render toggles [#4139](https://github.com/ethyca/fides/pull/4139)
- Moved the initial TCF layer to the banner [#4142](https://github.com/ethyca/fides/pull/4142)

Expand Down
4 changes: 2 additions & 2 deletions clients/fides-js/rollup.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ const GZIP_SIZE_ERROR_KB = 20; // fail build if bundle size exceeds this
const GZIP_SIZE_WARN_KB = 15; // log a warning if bundle size exceeds this

// TCF
const GZIP_SIZE_TCF_ERROR_KB = 100;
const GZIP_SIZE_TCF_WARN_KB = 90;
const GZIP_SIZE_TCF_ERROR_KB = 40;
const GZIP_SIZE_TCF_WARN_KB = 35;

const preactAliases = {
entries: [
Expand Down
23 changes: 10 additions & 13 deletions clients/fides-js/src/components/tcf/InitialLayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,11 @@ import { useMemo } from "preact/hooks";
import { PrivacyExperience } from "../../lib/consent-types";

import {
Stack,
createStacks,
getIdsNotRepresentedInStacks,
} from "../../lib/tcf/stacks";
import GVL_JSON from "../../lib/tcf/gvl.json";
import InitialLayerAccordion from "./InitialLayerAccordion";

const STACKS: Record<string, Stack> = GVL_JSON.stacks;

const InitialLayer = ({ experience }: { experience: PrivacyExperience }) => {
const purposeIds = useMemo(
() =>
Expand All @@ -27,15 +23,16 @@ const InitialLayer = ({ experience }: { experience: PrivacyExperience }) => {
[experience.tcf_special_features]
);

const stacks = useMemo(
() =>
createStacks({
purposeIds,
specialFeatureIds,
stacks: STACKS,
}),
[purposeIds, specialFeatureIds]
);
const stacks = useMemo(() => {
if (!experience.gvl) {
return [];
}
return createStacks({
purposeIds,
specialFeatureIds,
stacks: experience.gvl.stacks,
});
}, [purposeIds, specialFeatureIds, experience.gvl]);

const purposes = useMemo(() => {
if (!experience.tcf_purposes) {
Expand Down
1 change: 1 addition & 0 deletions clients/fides-js/src/components/tcf/TcfTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ const TcfTabs = ({
enabledVendorIds={enabledIds.vendors}
enabledSystemIds={enabledIds.systems}
onChange={onChange}
gvl={experience.gvl}
/>
),
},
Expand Down
7 changes: 5 additions & 2 deletions clients/fides-js/src/components/tcf/TcfVendors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useState } from "preact/hooks";
import {
EmbeddedLineItem,
EmbeddedPurpose,
GVLJson,
TCFVendorRecord,
} from "../../lib/tcf/types";
import { PrivacyExperience } from "../../lib/consent-types";
Expand Down Expand Up @@ -85,12 +86,14 @@ const TcfVendors = ({
enabledVendorIds,
enabledSystemIds,
onChange,
gvl,
}: {
allVendors: PrivacyExperience["tcf_vendors"];
allSystems: PrivacyExperience["tcf_systems"];
enabledVendorIds: string[];
enabledSystemIds: string[];
onChange: (payload: UpdateEnabledIds) => void;
gvl?: GVLJson;
}) => {
const [isFiltered, setIsFiltered] = useState(false);

Expand Down Expand Up @@ -129,7 +132,7 @@ const TcfVendors = ({
};

const vendorsToDisplay = isFiltered
? vendors.filter((v) => vendorIsGvl(v))
? vendors.filter((v) => vendorIsGvl(v, gvl))
: vendors;

return (
Expand All @@ -142,7 +145,7 @@ const TcfVendors = ({
handleToggle(vendor);
}}
checked={enabledIds.indexOf(vendor.id) !== -1}
badge={vendorIsGvl(vendor) ? "GVL" : undefined}
badge={vendorIsGvl(vendor, gvl) ? "GVL" : undefined}
>
<div>
<p>{vendor.description}</p>
Expand Down
2 changes: 2 additions & 0 deletions clients/fides-js/src/lib/consent-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
TCFFeatureSave,
TCFSpecialFeatureSave,
TCFVendorSave,
GVLJson,
} from "./tcf/types";

export type EmptyExperience = Record<PropertyKey, never>;
Expand Down Expand Up @@ -85,6 +86,7 @@ export type PrivacyExperience = {
tcf_systems?: Array<TCFVendorRecord>;
tcf_features?: Array<TCFFeatureRecord>;
tcf_special_features?: Array<TCFFeatureRecord>;
gvl?: GVLJson;
};

export type ExperienceConfig = {
Expand Down
208 changes: 101 additions & 107 deletions clients/fides-js/src/lib/tcf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,9 @@
*/

import { CmpApi } from "@iabtechlabtcf/cmpapi";
import {
TCModel,
TCString,
GVL,
VersionOrVendorList,
} from "@iabtechlabtcf/core";
import { TCModel, TCString, GVL } from "@iabtechlabtcf/core";
import { makeStub } from "./tcf/stub";
import { transformUserPreferenceToBoolean } from "./consent-utils";
import gvlJson from "./tcf/gvl.json";
import {
LegalBasisForProcessingEnum,
TCFPurposeRecord,
Expand Down Expand Up @@ -57,119 +51,119 @@ export const generateTcString = async ({
tcStringPreferences?: TcfSavePreferences;
experience: PrivacyExperience;
}): Promise<string> => {
// Creates a new TC string based on an old GVL version
// (https://vendor-list.consensu.org/v2/archives/vendor-list-v1.json)
// due to TCF library not yet supporting latest GVL (https://vendor-list.consensu.org/v3/vendor-list.json).
// We'll need to update this with our own hosted GVL once the lib is updated
// https://github.com/InteractiveAdvertisingBureau/iabtcf-es/pull/389
const tcModel = new TCModel(new GVL(gvlJson as VersionOrVendorList));

let encodedString = "";
try {
const tcModel = new TCModel(new GVL(experience.gvl));

// Some fields will not be populated until a GVL is loaded
await tcModel.gvl.readyPromise;
// Some fields will not be populated until a GVL is loaded
await tcModel.gvl.readyPromise;

tcModel.cmpId = CMP_ID;
tcModel.cmpVersion = CMP_VERSION;
tcModel.consentScreen = 1; // todo- On which 'screen' consent was captured; this is a CMP proprietary number encoded into the TC string
tcModel.cmpId = CMP_ID;
tcModel.cmpVersion = CMP_VERSION;
tcModel.consentScreen = 1; // todo- On which 'screen' consent was captured; this is a CMP proprietary number encoded into the TC string

if (tcStringPreferences) {
if (
tcStringPreferences.vendor_preferences &&
tcStringPreferences.vendor_preferences.length > 0
) {
tcStringPreferences.vendor_preferences.forEach((vendorPreference) => {
const consented = transformUserPreferenceToBoolean(
vendorPreference.preference
);
if (consented && vendorIsGvl(vendorPreference)) {
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
if (tcStringPreferences) {
if (
tcStringPreferences.vendor_preferences &&
tcStringPreferences.vendor_preferences.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
)
)
)
.map((p) => p.id);
if (
legIntPurposeIds.filter((id) =>
FORBIDDEN_LEGITIMATE_INTEREST_PURPOSE_IDS.includes(id)
).length
) {
skipSetLegInt = true;
.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);
}
}
if (!skipSetLegInt) {
tcModel.vendorLegitimateInterests.set(+vendorPreference.id);
}
}
});
}
});
}

// Set purpose consent on tcModel
if (
tcStringPreferences.purpose_preferences &&
tcStringPreferences.purpose_preferences.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);
}
}
});
}

// Set special feature opt-ins on tcModel
if (
tcStringPreferences.special_feature_preferences &&
tcStringPreferences.special_feature_preferences.length > 0
) {
tcStringPreferences.special_feature_preferences.forEach(
(specialFeaturePreference) => {
// Set purpose consent on tcModel
if (
tcStringPreferences.purpose_preferences &&
tcStringPreferences.purpose_preferences.length > 0
) {
tcStringPreferences.purpose_preferences.forEach((purposePreference) => {
const consented = transformUserPreferenceToBoolean(
specialFeaturePreference.preference
purposePreference.preference
);
if (consented) {
tcModel.specialFeatureOptins.set(+specialFeaturePreference.id);
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);
}
}
}
);
}
});
}

// Set special feature opt-ins on tcModel
if (
tcStringPreferences.special_feature_preferences &&
tcStringPreferences.special_feature_preferences.length > 0
) {
tcStringPreferences.special_feature_preferences.forEach(
(specialFeaturePreference) => {
const consented = transformUserPreferenceToBoolean(
specialFeaturePreference.preference
);
if (consented) {
tcModel.specialFeatureOptins.set(+specialFeaturePreference.id);
}
}
);
}

// note that we cannot set consent for special purposes nor features because the IAB policy states
// the user is not given choice by a CMP.
// See https://iabeurope.eu/iab-europe-transparency-consent-framework-policies/
// and https://github.com/InteractiveAdvertisingBureau/iabtcf-es/issues/63#issuecomment-581798996
encodedString = TCString.encode(tcModel);
// note that we cannot set consent for special purposes nor features because the IAB policy states
// the user is not given choice by a CMP.
// See https://iabeurope.eu/iab-europe-transparency-consent-framework-policies/
// and https://github.com/InteractiveAdvertisingBureau/iabtcf-es/issues/63#issuecomment-581798996
encodedString = TCString.encode(tcModel);
}
} catch (e) {
// eslint-disable-next-line no-console
console.error("Unable to instantiate GVL: ", e);
return Promise.resolve("");
}
return Promise.resolve(encodedString);
};
Expand Down
15 changes: 15 additions & 0 deletions clients/fides-js/src/lib/tcf/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { GVL } from "@iabtechlabtcf/core";
import type {
PrivacyPreferencesRequest,
UserConsentPreference,
Expand Down Expand Up @@ -143,3 +144,17 @@ export enum LegalBasisForProcessingEnum {
PUBLIC_INTEREST = "Public interest",
LEGITIMATE_INTERESTS = "Legitimate interests",
}

export type GVLJson = Pick<
GVL,
| "gvlSpecificationVersion"
| "vendorListVersion"
| "tcfPolicyVersion"
| "lastUpdated"
| "stacks"
| "purposes"
| "specialPurposes"
| "features"
| "specialFeatures"
| "vendors"
>;
Loading

0 comments on commit 5c741f1

Please sign in to comment.