From dfcdd540b058c769af824ea5cb7f99c08761055f Mon Sep 17 00:00:00 2001 From: Allison King Date: Mon, 4 Dec 2023 11:18:56 -0500 Subject: [PATCH 01/16] Initial commit for cookie refactor --- .../cypress/e2e/consent-banner-tcf.cy.ts | 22 +++---------------- 1 file changed, 3 insertions(+), 19 deletions(-) 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 dd3f796040..fe36cdd2dc 100644 --- a/clients/privacy-center/cypress/e2e/consent-banner-tcf.cy.ts +++ b/clients/privacy-center/cypress/e2e/consent-banner-tcf.cy.ts @@ -2698,25 +2698,9 @@ describe("Fides-js TCF", () => { }); it("can initialize from an AC string", () => { - const uuid = "4fbb6edf-34f6-4717-a6f1-541fd1e5d585"; - const CREATED_DATE = "2022-12-24T12:00:00.000Z"; - const UPDATED_DATE = "2022-12-25T12:00:00.000Z"; - const cookie = { - identity: { fides_user_device_id: uuid }, - fides_meta: { - version: "0.9.0", - createdAt: CREATED_DATE, - updatedAt: UPDATED_DATE, - }, - consent: {}, - tcf_consent: { - purpose_consent_preferences: { 2: false, [PURPOSE_4.id]: true }, - special_feature_preferences: { [SPECIAL_FEATURE_1.id]: true }, - system_legitimate_interests_preferences: { [SYSTEM_1.id]: false }, - vendor_consent_preferences: { [VENDOR_1.id]: false }, - }, - tc_string: "CPzbcgAPzbcgAGXABBENATEIAACAAAAAAAAAABEAAAAA.IABE", - }; + const cookie = mockCookie({ + fides_string: "CPzbcgAPzbcgAGXABBENATEIAACAAAAAAAAAABEAAAAA.IABE", + }); cy.setCookie(CONSENT_COOKIE_NAME, JSON.stringify(cookie)); cy.fixture("consent/experience_tcf.json").then((experience) => { stubConfig({ From 8903cbbcef0bd3766c9746281f4c29bcf676fd0c Mon Sep 17 00:00:00 2001 From: Allison King Date: Mon, 4 Dec 2023 11:40:26 -0500 Subject: [PATCH 02/16] Update to cypress13 and add IAB lib Cypress13 upgrades to webpack5 which is needed to parse the IAB lib's JS class files --- clients/package-lock.json | 192 +++++++++++++++++++++++++++- clients/privacy-center/package.json | 3 +- 2 files changed, 190 insertions(+), 5 deletions(-) diff --git a/clients/package-lock.json b/clients/package-lock.json index 730605a897..d0429426b2 100644 --- a/clients/package-lock.json +++ b/clients/package-lock.json @@ -2473,9 +2473,9 @@ } }, "node_modules/@iabtechlabtcf/core": { - "version": "1.5.7", - "resolved": "https://registry.npmjs.org/@iabtechlabtcf/core/-/core-1.5.7.tgz", - "integrity": "sha512-9IBcr3pPdvH4kijHqs9kAVMoM7tLkRbyKMu377BYNMP1YPsJuSFK1Z1y9U1g1vk+wcnaEj/qpGHL1aUBrGaVPw==" + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/@iabtechlabtcf/core/-/core-1.5.10.tgz", + "integrity": "sha512-HWenM2wC5wloQoAto3l/lX3H2uAB7qnoIsGAEd4J+OS74j/GlW3j7RYfMZ0JS//HEgJbW0P1jlbJ7oHRrEJoaw==" }, "node_modules/@isaacs/cliui": { "version": "8.0.2", @@ -22053,6 +22053,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, "node_modules/unique-filename": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", @@ -22984,6 +22990,7 @@ "@emotion/styled": "^11.10.6", "@fidesui/react": "^0.0.23", "@fontsource/inter": "^4.5.15", + "@iabtechlabtcf/core": "^1.5.10", "@reduxjs/toolkit": "^1.9.3", "cache-control-parser": "^2.0.4", "fides-js": "*", @@ -23015,7 +23022,7 @@ "@typescript-eslint/parser": "^5.57.0", "babel-jest": "^29.5.0", "cross-env": "^7.0.3", - "cypress": "^12.8.1", + "cypress": "^13.6.0", "cypress-wait-until": "^1.7.2", "eslint": "^8.36.0", "eslint-config-airbnb": "^19.0.4", @@ -23041,6 +23048,35 @@ "whatwg-fetch": "^3.6.2" } }, + "privacy-center/node_modules/@cypress/request": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.1.tgz", + "integrity": "sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ==", + "dev": true, + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "http-signature": "~1.3.6", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "performance-now": "^2.1.0", + "qs": "6.10.4", + "safe-buffer": "^5.1.2", + "tough-cookie": "^4.1.3", + "tunnel-agent": "^0.6.0", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">= 6" + } + }, "privacy-center/node_modules/@next/bundle-analyzer": { "version": "12.3.4", "resolved": "https://registry.npmjs.org/@next/bundle-analyzer/-/bundle-analyzer-12.3.4.tgz", @@ -23307,6 +23343,88 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "privacy-center/node_modules/cypress": { + "version": "13.6.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.6.0.tgz", + "integrity": "sha512-quIsnFmtj4dBUEJYU4OH0H12bABJpSujvWexC24Ju1gTlKMJbeT6tTO0vh7WNfiBPPjoIXLN+OUqVtiKFs6SGw==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@cypress/request": "^3.0.0", + "@cypress/xvfb": "^1.2.4", + "@types/node": "^18.17.5", + "@types/sinonjs__fake-timers": "8.1.1", + "@types/sizzle": "^2.3.2", + "arch": "^2.2.0", + "blob-util": "^2.0.2", + "bluebird": "^3.7.2", + "buffer": "^5.6.0", + "cachedir": "^2.3.0", + "chalk": "^4.1.0", + "check-more-types": "^2.24.0", + "cli-cursor": "^3.1.0", + "cli-table3": "~0.6.1", + "commander": "^6.2.1", + "common-tags": "^1.8.0", + "dayjs": "^1.10.4", + "debug": "^4.3.4", + "enquirer": "^2.3.6", + "eventemitter2": "6.4.7", + "execa": "4.1.0", + "executable": "^4.1.1", + "extract-zip": "2.0.1", + "figures": "^3.2.0", + "fs-extra": "^9.1.0", + "getos": "^3.2.1", + "is-ci": "^3.0.0", + "is-installed-globally": "~0.4.0", + "lazy-ass": "^1.6.0", + "listr2": "^3.8.3", + "lodash": "^4.17.21", + "log-symbols": "^4.0.0", + "minimist": "^1.2.8", + "ospath": "^1.2.2", + "pretty-bytes": "^5.6.0", + "process": "^0.11.10", + "proxy-from-env": "1.0.0", + "request-progress": "^3.0.0", + "semver": "^7.5.3", + "supports-color": "^8.1.1", + "tmp": "~0.2.1", + "untildify": "^4.0.0", + "yauzl": "^2.10.0" + }, + "bin": { + "cypress": "bin/cypress" + }, + "engines": { + "node": "^16.0.0 || ^18.0.0 || >=20.0.0" + } + }, + "privacy-center/node_modules/cypress/node_modules/@types/node": { + "version": "18.19.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.2.tgz", + "integrity": "sha512-6wzfBdbWpe8QykUkXBjtmO3zITA0A3FIjoy+in0Y2K4KrCiRhNYJIdwAPDffZ3G6GnaKaSLSEa9ZuORLfEoiwg==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "privacy-center/node_modules/cypress/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "privacy-center/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -23316,6 +23434,18 @@ "node": ">=8" } }, + "privacy-center/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "privacy-center/node_modules/next": { "version": "12.2.5", "resolved": "https://registry.npmjs.org/next/-/next-12.2.5.tgz", @@ -23410,6 +23540,21 @@ "node": "^10 || ^12 || >=14" } }, + "privacy-center/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "privacy-center/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -23422,6 +23567,39 @@ "node": ">=8" } }, + "privacy-center/node_modules/tough-cookie": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", + "dev": true, + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "privacy-center/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "privacy-center/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, "privacy-center/node_modules/webpack-bundle-analyzer": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.3.0.tgz", @@ -23465,6 +23643,12 @@ "optional": true } } + }, + "privacy-center/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true } } } diff --git a/clients/privacy-center/package.json b/clients/privacy-center/package.json index f48be0f2fe..6ca35b18f6 100644 --- a/clients/privacy-center/package.json +++ b/clients/privacy-center/package.json @@ -28,6 +28,7 @@ "@emotion/styled": "^11.10.6", "@fidesui/react": "^0.0.23", "@fontsource/inter": "^4.5.15", + "@iabtechlabtcf/core": "^1.5.10", "@reduxjs/toolkit": "^1.9.3", "cache-control-parser": "^2.0.4", "fides-js": "*", @@ -59,7 +60,7 @@ "@typescript-eslint/parser": "^5.57.0", "babel-jest": "^29.5.0", "cross-env": "^7.0.3", - "cypress": "^12.8.1", + "cypress": "^13.6.0", "cypress-wait-until": "^1.7.2", "eslint": "^8.36.0", "eslint-config-airbnb": "^19.0.4", From f577669d832fd7110fbc655eceabf85fbf7b5ab9 Mon Sep 17 00:00:00 2001 From: Allison King Date: Mon, 4 Dec 2023 12:01:32 -0500 Subject: [PATCH 03/16] Refactor test to check for fides string instead of cookie values --- .../cypress/e2e/consent-banner-tcf.cy.ts | 74 +++++++++++++------ 1 file changed, 52 insertions(+), 22 deletions(-) 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 fe36cdd2dc..597f6f847e 100644 --- a/clients/privacy-center/cypress/e2e/consent-banner-tcf.cy.ts +++ b/clients/privacy-center/cypress/e2e/consent-banner-tcf.cy.ts @@ -6,7 +6,9 @@ import { FidesEndpointPaths, PrivacyExperience, } from "fides-js"; +import { TCString } from "@iabtechlabtcf/core"; import { CookieKeyConsent } from "fides-js/src/lib/cookie"; +import { FIDES_SEPARATOR } from "~/../fides-js/src/lib/tcf/constants"; import { API_URL } from "../support/constants"; import { mockCookie, mockTcfVendorObjects } from "../support/mocks"; import { OVERRIDE, stubConfig } from "../support/stubs"; @@ -110,6 +112,31 @@ const checkDefaultExperienceRender = () => { }); }; +const assertTcOptIns = ({ + cookie, + modelType, + ids, +}: { + cookie: FidesCookie; + modelType: + | "purposeConsents" + | "purposeLegitimateInterests" + | "specialFeatureOptins" + | "vendorConsents" + | "vendorLegitimateInterests"; + + ids: number[]; +}) => { + const { fides_string: fidesString } = cookie; + const tcString = fidesString?.split(FIDES_SEPARATOR)[0]; + expect(tcString).to.be.a("string"); + const model = TCString.decode(tcString!); + const values = Array.from(model[modelType].values()).sort(); + expect(values).to.eql(ids.sort()); +}; + +const fidesVendorIdToId = (fidesId: string) => +fidesId.split(".")[1]; + describe("Fides-js TCF", () => { describe("banner appears when it should", () => { beforeEach(() => { @@ -583,28 +610,31 @@ describe("Fides-js TCF", () => { const cookieKeyConsent: FidesCookie = JSON.parse( decodeURIComponent(cookie!.value) ); - [PURPOSE_9.id, PURPOSE_6.id, PURPOSE_7.id, PURPOSE_4.id].forEach( - (pid) => { - expect(cookieKeyConsent.tcf_consent.purpose_consent_preferences) - .property(`${pid}`) - .is.eql(true); - } - ); - expect( - cookieKeyConsent.tcf_consent - .purpose_legitimate_interests_preferences - ) - .property(`${PURPOSE_2.id}`) - .is.eql(true); - expect(cookieKeyConsent.tcf_consent.special_feature_preferences) - .property(`${SPECIAL_FEATURE_1.id}`) - .is.eql(true); - expect(cookieKeyConsent.tcf_consent.vendor_consent_preferences) - .property(`${VENDOR_1.id}`) - .is.eql(true); - expect( - cookieKeyConsent.tcf_consent.vendor_legitimate_interests_preferences - ).to.eql({}); + assertTcOptIns({ + cookie: cookieKeyConsent, + modelType: "purposeConsents", + ids: [PURPOSE_9.id, PURPOSE_6.id, PURPOSE_7.id, PURPOSE_4.id], + }); + assertTcOptIns({ + cookie: cookieKeyConsent, + modelType: "purposeLegitimateInterests", + ids: [PURPOSE_2.id], + }); + assertTcOptIns({ + cookie: cookieKeyConsent, + modelType: "specialFeatureOptins", + ids: [SPECIAL_FEATURE_1.id], + }); + assertTcOptIns({ + cookie: cookieKeyConsent, + modelType: "vendorConsents", + ids: [fidesVendorIdToId(VENDOR_1.id)], + }); + assertTcOptIns({ + cookie: cookieKeyConsent, + modelType: "vendorLegitimateInterests", + ids: [], + }); expect( cookieKeyConsent.tcf_consent.system_consent_preferences ).to.eql({}); From 2e9a91f319661eb688c2a993768deedaad3d32d3 Mon Sep 17 00:00:00 2001 From: Allison King Date: Mon, 4 Dec 2023 12:58:41 -0500 Subject: [PATCH 04/16] Update more tests --- .../cypress/e2e/consent-banner-tcf.cy.ts | 244 +++++++++--------- 1 file changed, 129 insertions(+), 115 deletions(-) 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 597f6f847e..5886b5ae62 100644 --- a/clients/privacy-center/cypress/e2e/consent-banner-tcf.cy.ts +++ b/clients/privacy-center/cypress/e2e/consent-banner-tcf.cy.ts @@ -124,7 +124,6 @@ const assertTcOptIns = ({ | "specialFeatureOptins" | "vendorConsents" | "vendorLegitimateInterests"; - ids: number[]; }) => { const { fides_string: fidesString } = cookie; @@ -135,6 +134,23 @@ const assertTcOptIns = ({ expect(values).to.eql(ids.sort()); }; +const assertAcOptIns = ({ + cookie, + ids, +}: { + cookie: FidesCookie; + ids: number[]; +}) => { + const { fides_string: fidesString } = cookie; + const acString = fidesString?.split("1~")[1]; + expect(acString).to.be.a("string"); + const values = acString! + .split(".") + .map((id) => +id) + .sort(); + expect(values).to.eql(ids.sort()); +}; + const fidesVendorIdToId = (fidesId: string) => +fidesId.split(".")[1]; describe("Fides-js TCF", () => { @@ -723,29 +739,31 @@ describe("Fides-js TCF", () => { const cookieKeyConsent: FidesCookie = JSON.parse( decodeURIComponent(cookie!.value) ); - [PURPOSE_4.id, PURPOSE_9.id, PURPOSE_6.id, PURPOSE_7.id].forEach( - (pid) => { - expect(cookieKeyConsent.tcf_consent.purpose_consent_preferences) - .property(`${pid}`) - .is.eql(false); - } - ); - expect( - cookieKeyConsent.tcf_consent - .purpose_legitimate_interests_preferences - ) - .property(`${PURPOSE_2.id}`) - .is.eql(false); - expect(cookieKeyConsent.tcf_consent.special_feature_preferences) - .property(`${SPECIAL_FEATURE_1.id}`) - .is.eql(false); - expect(cookieKeyConsent.tcf_consent.vendor_consent_preferences) - .property(`${VENDOR_1.id}`) - .is.eql(false); - expect( - cookieKeyConsent.tcf_consent - .vendor_legitimate_interests_preferences - ).to.eql({}); + assertTcOptIns({ + cookie: cookieKeyConsent, + modelType: "purposeConsents", + ids: [], + }); + assertTcOptIns({ + cookie: cookieKeyConsent, + modelType: "purposeLegitimateInterests", + ids: [], + }); + assertTcOptIns({ + cookie: cookieKeyConsent, + modelType: "specialFeatureOptins", + ids: [], + }); + assertTcOptIns({ + cookie: cookieKeyConsent, + modelType: "vendorConsents", + ids: [], + }); + assertTcOptIns({ + cookie: cookieKeyConsent, + modelType: "vendorLegitimateInterests", + ids: [], + }); expect( cookieKeyConsent.tcf_consent.system_consent_preferences ).to.eql({}); @@ -841,29 +859,31 @@ describe("Fides-js TCF", () => { const cookieKeyConsent: FidesCookie = JSON.parse( decodeURIComponent(cookie!.value) ); - [PURPOSE_9.id, PURPOSE_6.id, PURPOSE_7.id].forEach((pid) => { - expect(cookieKeyConsent.tcf_consent.purpose_consent_preferences) - .property(`${pid}`) - .is.eql(true); + assertTcOptIns({ + cookie: cookieKeyConsent, + modelType: "purposeConsents", + ids: [PURPOSE_9.id, PURPOSE_6.id, PURPOSE_7.id], + }); + assertTcOptIns({ + cookie: cookieKeyConsent, + modelType: "purposeLegitimateInterests", + ids: [PURPOSE_2.id], + }); + assertTcOptIns({ + cookie: cookieKeyConsent, + modelType: "specialFeatureOptins", + ids: [SPECIAL_FEATURE_1.id], + }); + assertTcOptIns({ + cookie: cookieKeyConsent, + modelType: "vendorConsents", + ids: [], + }); + assertTcOptIns({ + cookie: cookieKeyConsent, + modelType: "vendorLegitimateInterests", + ids: [], }); - expect( - cookieKeyConsent.tcf_consent - .purpose_legitimate_interests_preferences - ) - .property(`${PURPOSE_2.id}`) - .is.eql(true); - expect(cookieKeyConsent.tcf_consent.purpose_consent_preferences) - .property(`${PURPOSE_4.id}`) - .is.eql(false); - expect(cookieKeyConsent.tcf_consent.special_feature_preferences) - .property(`${SPECIAL_FEATURE_1.id}`) - .is.eql(true); - expect(cookieKeyConsent.tcf_consent.vendor_consent_preferences) - .property(`${VENDOR_1.id}`) - .is.eql(false); - expect( - cookieKeyConsent.tcf_consent.vendor_legitimate_interests_preferences - ).to.eql({}); expect( cookieKeyConsent.tcf_consent.system_legitimate_interests_preferences ) @@ -974,29 +994,31 @@ describe("Fides-js TCF", () => { const cookieKeyConsent: FidesCookie = JSON.parse( decodeURIComponent(cookie!.value) ); - [PURPOSE_4.id, PURPOSE_9.id, PURPOSE_6.id, PURPOSE_7.id].forEach( - (pid) => { - expect(cookieKeyConsent.tcf_consent.purpose_consent_preferences) - .property(`${pid}`) - .is.eql(false); - } - ); - expect( - cookieKeyConsent.tcf_consent - .purpose_legitimate_interests_preferences - ) - .property(`${PURPOSE_2.id}`) - .is.eql(false); - expect(cookieKeyConsent.tcf_consent.special_feature_preferences) - .property(`${SPECIAL_FEATURE_1.id}`) - .is.eql(false); - expect(cookieKeyConsent.tcf_consent.vendor_consent_preferences) - .property(`${VENDOR_1.id}`) - .is.eql(false); - expect( - cookieKeyConsent.tcf_consent - .vendor_legitimate_interests_preferences - ).to.eql({}); + assertTcOptIns({ + cookie: cookieKeyConsent, + modelType: "purposeConsents", + ids: [], + }); + assertTcOptIns({ + cookie: cookieKeyConsent, + modelType: "purposeLegitimateInterests", + ids: [], + }); + assertTcOptIns({ + cookie: cookieKeyConsent, + modelType: "specialFeatureOptins", + ids: [], + }); + assertTcOptIns({ + cookie: cookieKeyConsent, + modelType: "vendorConsents", + ids: [], + }); + assertTcOptIns({ + cookie: cookieKeyConsent, + modelType: "vendorLegitimateInterests", + ids: [], + }); expect( cookieKeyConsent.tcf_consent.system_consent_preferences ).to.eql({}); @@ -1211,28 +1233,31 @@ describe("Fides-js TCF", () => { const cookieKeyConsent: FidesCookie = JSON.parse( decodeURIComponent(cookie!.value) ); - [PURPOSE_9.id, PURPOSE_6.id, PURPOSE_7.id].forEach((pid) => { - expect(cookieKeyConsent.tcf_consent.purpose_consent_preferences) - .property(`${pid}`) - .is.eql(true); + assertTcOptIns({ + cookie: cookieKeyConsent, + modelType: "purposeConsents", + ids: [PURPOSE_9.id, PURPOSE_6.id, PURPOSE_7.id], + }); + assertTcOptIns({ + cookie: cookieKeyConsent, + modelType: "purposeLegitimateInterests", + ids: [PURPOSE_2.id], + }); + assertTcOptIns({ + cookie: cookieKeyConsent, + modelType: "specialFeatureOptins", + ids: [SPECIAL_FEATURE_1.id], + }); + assertTcOptIns({ + cookie: cookieKeyConsent, + modelType: "vendorConsents", + ids: [], + }); + assertTcOptIns({ + cookie: cookieKeyConsent, + modelType: "vendorLegitimateInterests", + ids: [], }); - expect( - cookieKeyConsent.tcf_consent.purpose_legitimate_interests_preferences - ) - .property(`${PURPOSE_2.id}`) - .is.eql(true); - expect(cookieKeyConsent.tcf_consent.purpose_consent_preferences) - .property(`${PURPOSE_4.id}`) - .is.eql(false); - expect(cookieKeyConsent.tcf_consent.special_feature_preferences) - .property(`${SPECIAL_FEATURE_1.id}`) - .is.eql(true); - expect(cookieKeyConsent.tcf_consent.vendor_consent_preferences) - .property(`${VENDOR_1.id}`) - .is.eql(false); - expect( - cookieKeyConsent.tcf_consent.vendor_legitimate_interests_preferences - ).to.eql({}); expect( cookieKeyConsent.tcf_consent.system_legitimate_interests_preferences ) @@ -1498,14 +1523,9 @@ describe("Fides-js TCF", () => { const setFidesCookie = () => { const cookie = mockCookie({ tcf_consent: { - purpose_consent_preferences: { - [PURPOSE_4.id]: false, - [PURPOSE_9.id]: true, - }, - special_feature_preferences: { [SPECIAL_FEATURE_1.id]: true }, system_legitimate_interests_preferences: { [SYSTEM_1.id]: false }, - vendor_consent_preferences: { [VENDOR_1.id]: true }, }, + // Purpose 9, Special feature 1, Vendor consent 2 fides_string: "CPziCYAPziCYAGXABBENATEIAACAAAAAAAAAABEAAAAA.IABE", }); cy.setCookie(CONSENT_COOKIE_NAME, JSON.stringify(cookie)); @@ -1519,9 +1539,10 @@ describe("Fides-js TCF", () => { * ✅ 4) "prefetched" experience (via config.options.experience) * ❌ 5) experience API (via GET /privacy-experience) * - * EXPECTED RESULT: use preferences from local cookie + * EXPECTED RESULT: use preferences from local cookie's saved string + * TODO: CURRENTLY FAILING!! */ - it("prefers preferences from a cookie when both cookie and experience exist", () => { + it("prefers preferences from a cookie's fides_string when both cookie and experience exist", () => { setFidesCookie(); cy.fixture("consent/experience_tcf.json").then((experience) => { stubConfig({ @@ -2292,8 +2313,9 @@ describe("Fides-js TCF", () => { * ✅ 5) experience API (via GET /privacy-experience) * * EXPECTED RESULT: prefers preferences from local cookie instead of from client-side experience + * TODO: CURRENTLY FAILING!! */ - it("prefers preferences from fides_string option when both fides_string option and cookie exist and experience is fetched from API", () => { + it("prefers preferences from cookie's fides_string when cookie exists and experience is fetched from API", () => { setFidesCookie(); cy.fixture("consent/experience_tcf.json").then((experience) => { cy.fixture("consent/geolocation_tcf.json").then((geo) => { @@ -2738,7 +2760,7 @@ describe("Fides-js TCF", () => { isOverlayEnabled: true, tcfEnabled: true, // this TC string sets purpose 4 to false and purpose 7 to true - // the appended AC string sets AC 42 to true + // the appended AC string sets AC 42,43,44 to true fidesString: "CPzevcAPzevcAGXABBENATEIAAIAAAAAAAAAAAAAAAAA.IABE,1~42.43.44", }, @@ -2747,24 +2769,16 @@ describe("Fides-js TCF", () => { }); cy.get("@FidesInitialized") - .its("lastCall.args.0.detail.tcf_consent") - .then((tcfConsent) => { + .its("lastCall.args.0.detail") + .then((updatedCookie: FidesCookie) => { // TC string setting worked - expect(tcfConsent.purpose_consent_preferences).to.eql({ - 1: false, - 2: false, - 3: false, - 4: false, - 5: false, - 6: false, - 7: true, + assertTcOptIns({ + cookie: updatedCookie, + modelType: "purposeConsents", + ids: [PURPOSE_7.id], }); // AC string setting worked - expect(tcfConsent.vendor_consent_preferences).to.eql({ - "gacp.42": true, - "gacp.43": true, - "gacp.44": true, - }); + assertAcOptIns({ cookie: updatedCookie, ids: [42, 43, 44] }); }); }); }); From 2580dcdce04d4932058a75e010001921455be5c9 Mon Sep 17 00:00:00 2001 From: eastandwestwind Date: Mon, 4 Dec 2023 19:21:01 +0100 Subject: [PATCH 05/16] rely on tc string where possible for consent values, remove redundancy on cookie --- clients/fides-js/src/fides-tcf.ts | 24 +++-- clients/fides-js/src/lib/cookie.ts | 102 ++++++++++++++++------ clients/fides-js/src/lib/initialize.ts | 1 + clients/fides-js/src/lib/tcf/constants.ts | 7 +- clients/fides-js/src/lib/tcf/utils.ts | 54 +++++++++--- 5 files changed, 136 insertions(+), 52 deletions(-) diff --git a/clients/fides-js/src/fides-tcf.ts b/clients/fides-js/src/fides-tcf.ts index 7b37df720b..a77a597321 100644 --- a/clients/fides-js/src/fides-tcf.ts +++ b/clients/fides-js/src/fides-tcf.ts @@ -70,7 +70,7 @@ import { import type { Fides } from "./lib/initialize"; import { dispatchFidesEvent } from "./lib/events"; import { - buildTcfEntitiesFromCookie, + buildTcfEntitiesFromCookieAndFidesString, debugLog, experienceIsValid, FidesCookie, @@ -155,7 +155,11 @@ const updateCookieAndExperience = async ({ "Overriding preferences from client-side fetched experience with cookie fides_string consent", cookie.fides_string ); - const tcfEntities = buildTcfEntitiesFromCookie(experience, cookie); + const tcfEntities = buildTcfEntitiesFromCookieAndFidesString( + experience, + cookie, + cookie.fides_string + ); return { cookie, experience: tcfEntities }; } @@ -178,15 +182,19 @@ const updateCookieAndExperience = async ({ }; TCF_KEY_MAP.forEach(({ experienceKey, cookieKey, enabledIdsKey }) => { - tcSavePrefs[cookieKey] = []; + if (cookieKey) { + tcSavePrefs[cookieKey] = []; + } experience[experienceKey]?.forEach((record) => { const pref: UserConsentPreference = getInitialPreference(record); // map experience to tcSavePrefs (same as cookie keys) - tcSavePrefs[cookieKey]?.push({ - // @ts-ignore - id: record.id, - preference: pref, - }); + if (cookieKey) { + tcSavePrefs[cookieKey]?.push({ + // @ts-ignore + id: record.id, + preference: pref, + }); + } // add to enabledIds only if user consent is True if (transformUserPreferenceToBoolean(pref)) { if (enabledIdsKey) { diff --git a/clients/fides-js/src/lib/cookie.ts b/clients/fides-js/src/lib/cookie.ts index f531e7a688..06923fe810 100644 --- a/clients/fides-js/src/lib/cookie.ts +++ b/clients/fides-js/src/lib/cookie.ts @@ -13,6 +13,7 @@ import { LegacyConsentConfig, PrivacyExperience, SaveConsentPreference, + UserConsentPreference, } from "./consent-types"; import { debugLog, @@ -20,8 +21,15 @@ import { transformUserPreferenceToBoolean, } from "./consent-utils"; import type { TcfCookieConsent, TcfSavePreferences } from "./tcf/types"; +import { + TcfCookieKeyConsent, + TcfExperienceRecords, + TcfModelsRecord, +} from "./tcf/types"; import { TCF_KEY_MAP } from "./tcf/constants"; -import { TcfCookieKeyConsent } from "./tcf/types"; +import { getConsentFromTcModel } from "~/lib/tcf/utils"; +import { decodeFidesString } from "~/lib/tcf/fidesString"; +import { TCModel, TCString } from "@iabtechlabtcf/core"; /** * Store the user's consent preferences on the cookie, as key -> boolean pairs, e.g. @@ -285,13 +293,47 @@ export const buildCookieConsentForExperiences = ( return cookieConsent; }; +const getOrDefaultPreference = ( + tcModel: TCModel | undefined, + item: TcfModelsRecord, + cookieConsent: TcfCookieKeyConsent, + experienceKey: keyof TcfExperienceRecords, + fidesString?: string | null +): UserConsentPreference | undefined => { + if (Object.hasOwn(cookieConsent, item.id)) { + return transformConsentToFidesUserPreference( + Boolean(cookieConsent[item.id]), + ConsentMechanism.OPT_IN + ); + } + // If experience contains a tcf entity not defined by tcfEntities on the cookie, this means: + // A) If fides_string exists, we check field on tcModel. If it doesn't exist, since opt-outs are not tracked by TC string, we assume opt-out. + // B) There is a new tcf entity that requires consent. In this case we would use the default on the experience. + if (tcModel) { + const consentFromExperience: boolean = getConsentFromTcModel( + tcModel, + experienceKey, + item.id + ); + if (consentFromExperience) { + return transformConsentToFidesUserPreference( + consentFromExperience, + ConsentMechanism.OPT_IN + ); + } + return UserConsentPreference.OPT_OUT; + } + return item.default_preference; +}; + /** - * Populates TCF entities with items from cookie.tcf_consent. + * Populates TCF entities with items from cookie.tcf_consent and Fides string. * Returns TCF entities to be assigned to an experience. */ -export const buildTcfEntitiesFromCookie = ( +export const buildTcfEntitiesFromCookieAndFidesString = ( experience: PrivacyExperience, - cookie: FidesCookie + cookie: FidesCookie, + fidesString?: string | null ) => { const tcfEntities = { tcf_purpose_consents: experience.tcf_purpose_consents, @@ -307,22 +349,24 @@ export const buildTcfEntitiesFromCookie = ( }; if (cookie.tcf_consent) { + let tcModel: TCModel | undefined; + if (fidesString) { + // fixme- do we need the ac string? + const { tc: tcString } = decodeFidesString(fidesString); + tcModel = TCString.decode(tcString); + } TCF_KEY_MAP.forEach(({ cookieKey, experienceKey }) => { - const cookieConsent = cookie.tcf_consent[cookieKey] ?? {}; + const cookieConsent: TcfCookieKeyConsent = + (cookieKey && cookie.tcf_consent[cookieKey]) ?? {}; // @ts-ignore the array map should ensure we will get the right record type tcfEntities[experienceKey] = experience[experienceKey]?.map((item) => { - const defaultPreference = cookie.fides_string - ? ConsentMechanism.OPT_OUT - : item.default_preference; - const preference = Object.hasOwn(cookieConsent, item.id) - ? transformConsentToFidesUserPreference( - Boolean(cookieConsent[item.id]), - ConsentMechanism.OPT_IN - ) - : // If experience contains a tcf entity not defined by tcfEntities, this means: - // A) If fides_string exists, user has probably opted out. Since opt-outs are not tracked by TC string, in this case we assume opt-out. - // B) There is a new tcf entity that requires consent. In this case we would use the default on the experience. - defaultPreference; + const preference = getOrDefaultPreference( + tcModel, + item, + cookieConsent, + experienceKey, + fidesString + ); return { ...item, current_preference: preference }; }); }); @@ -342,10 +386,12 @@ export const updateExperienceFromCookieConsent = ({ experience, cookie, debug, + fidesString, }: { experience: PrivacyExperience; cookie: FidesCookie; debug?: boolean; + fidesString?: string | null; }): PrivacyExperience => { const noticesWithConsent = experience.privacy_notices?.map((notice) => { // Prefers preference in cookie if it exists, else uses current preference on the notice if it exists, else uses @@ -361,7 +407,11 @@ export const updateExperienceFromCookieConsent = ({ }); // Handle the TCF case, which has many keys to query - const tcfEntities = buildTcfEntitiesFromCookie(experience, cookie); + const tcfEntities = buildTcfEntitiesFromCookieAndFidesString( + experience, + cookie, + fidesString + ); if (debug) { debugLog( @@ -378,13 +428,15 @@ export const transformTcfPreferencesToCookieKeys = ( ): TcfCookieConsent => { const cookieKeys: TcfCookieConsent = {}; TCF_KEY_MAP.forEach(({ cookieKey }) => { - const preferences = tcfPreferences[cookieKey] ?? []; - cookieKeys[cookieKey] = Object.fromEntries( - preferences.map((pref) => [ - pref.id, - transformUserPreferenceToBoolean(pref.preference), - ]) - ); + if (cookieKey) { + const preferences = tcfPreferences[cookieKey] ?? []; + cookieKeys[cookieKey] = Object.fromEntries( + preferences.map((pref) => [ + pref.id, + transformUserPreferenceToBoolean(pref.preference), + ]) + ); + } }); return cookieKeys; }; diff --git a/clients/fides-js/src/lib/initialize.ts b/clients/fides-js/src/lib/initialize.ts index 5f56f522b5..f70267f16e 100644 --- a/clients/fides-js/src/lib/initialize.ts +++ b/clients/fides-js/src/lib/initialize.ts @@ -225,6 +225,7 @@ export const getInitialFides = ({ experience, cookie, debug: options.debug, + fidesString: options.fidesString, }); } diff --git a/clients/fides-js/src/lib/tcf/constants.ts b/clients/fides-js/src/lib/tcf/constants.ts index 6c77f6a56f..0b5df2220a 100644 --- a/clients/fides-js/src/lib/tcf/constants.ts +++ b/clients/fides-js/src/lib/tcf/constants.ts @@ -18,37 +18,32 @@ export const ETHYCA_CMP_ID = 407; export const FIDES_SEPARATOR = ","; export const TCF_KEY_MAP: { - cookieKey: TcfModelType; + cookieKey?: TcfModelType; experienceKey: keyof TcfExperienceRecords; tcfModelKey?: keyof TCModel; enabledIdsKey?: keyof EnabledIds; }[] = [ { - cookieKey: "purpose_consent_preferences", experienceKey: "tcf_purpose_consents", tcfModelKey: "purposeConsents", enabledIdsKey: "purposesConsent", }, { - cookieKey: "purpose_legitimate_interests_preferences", experienceKey: "tcf_purpose_legitimate_interests", tcfModelKey: "purposeLegitimateInterests", enabledIdsKey: "purposesLegint", }, { - cookieKey: "special_feature_preferences", experienceKey: "tcf_special_features", tcfModelKey: "specialFeatureOptins", enabledIdsKey: "specialFeatures", }, { - cookieKey: "vendor_consent_preferences", experienceKey: "tcf_vendor_consents", tcfModelKey: "vendorConsents", enabledIdsKey: "vendorsConsent", }, { - cookieKey: "vendor_legitimate_interests_preferences", experienceKey: "tcf_vendor_legitimate_interests", tcfModelKey: "vendorLegitimateInterests", enabledIdsKey: "vendorsLegint", diff --git a/clients/fides-js/src/lib/tcf/utils.ts b/clients/fides-js/src/lib/tcf/utils.ts index 54edf6dd9b..9f01737809 100644 --- a/clients/fides-js/src/lib/tcf/utils.ts +++ b/clients/fides-js/src/lib/tcf/utils.ts @@ -1,16 +1,40 @@ import { TCModel, TCString, Vector } from "@iabtechlabtcf/core"; -import { PrivacyExperience } from "../consent-types"; -import { EnabledIds, TcfCookieConsent, TcfCookieKeyConsent } from "./types"; +import { PrivacyExperience, UserConsentPreference } from "../consent-types"; +import { + EnabledIds, + TcfCookieConsent, + TcfCookieKeyConsent, + TcfExperienceRecords, +} from "./types"; import { TCF_KEY_MAP } from "./constants"; import { generateFidesString } from "../tcf"; import { debugLog } from "../consent-utils"; import { decodeFidesString, idsFromAcString } from "./fidesString"; +export const getConsentFromTcModel = ( + tcModel: TCModel, + experienceKey: keyof TcfExperienceRecords, + tcfRecordId: string | number +): boolean => { + const associatedTCFKey = TCF_KEY_MAP.find( + (i) => i.experienceKey === experienceKey + )?.tcfModelKey; + if (associatedTCFKey) { + (tcModel[associatedTCFKey] as Vector).forEach((consented, id) => { + if (id === tcfRecordId) { + return consented; + } + }); + return false; + } + // fixme- tcf_vendor_consents comes from both AC string and tc string? +}; + export const transformFidesStringToCookieKeys = ( fidesString: string, debug: boolean ): TcfCookieConsent => { - const { tc: tcString, ac: acString } = decodeFidesString(fidesString); + const { tc: tcString } = decodeFidesString(fidesString); const tcModel: TCModel = TCString.decode(tcString); const cookieKeys: TcfCookieConsent = {}; @@ -26,19 +50,23 @@ export const transformFidesStringToCookieKeys = ( const key = isVendorKey ? `gvl.${id}` : id; items[key] = consented; }); - cookieKeys[cookieKey] = items; + if (cookieKey) { + cookieKeys[cookieKey] = items; + } } }); + // fixme- Keep for reference but remove before merging + // from the fides_string // Set AC consents, which will only be on vendor_consents - const acIds = idsFromAcString(acString, debug); - acIds.forEach((acId) => { - if (!cookieKeys.vendor_consent_preferences) { - cookieKeys.vendor_consent_preferences = { [acId]: true }; - } else { - cookieKeys.vendor_consent_preferences[acId] = true; - } - }); + // const acIds = idsFromAcString(acString, debug); + // acIds.forEach((acId) => { + // if (!cookieKeys.vendor_consent_preferences) { + // cookieKeys.vendor_consent_preferences = { [acId]: true }; + // } else { + // cookieKeys.vendor_consent_preferences[acId] = true; + // } + // }); debugLog( debug, `Generated cookie.tcf_consent from explicit fidesString.`, @@ -63,7 +91,7 @@ export const generateFidesStringFromCookieTcfConsent = async ( TCF_KEY_MAP.forEach(({ cookieKey, enabledIdsKey }) => { const cookieKeyConsent: TcfCookieKeyConsent | undefined = - tcfConsent[cookieKey]; + cookieKey && tcfConsent[cookieKey]; if (cookieKeyConsent) { Object.keys(cookieKeyConsent).forEach((key: string | number) => { if (cookieKeyConsent[key] && enabledIdsKey) { From fd1604515190d273e37eaac3d2751c003752b8fd Mon Sep 17 00:00:00 2001 From: eastandwestwind Date: Mon, 4 Dec 2023 19:53:13 +0100 Subject: [PATCH 06/16] remove unneeded logic, get consent from ac string --- clients/fides-js/src/fides-tcf.ts | 28 --------------- clients/fides-js/src/lib/cookie.ts | 52 ++++++++++++++------------- clients/fides-js/src/lib/tcf/utils.ts | 12 +++++-- 3 files changed, 36 insertions(+), 56 deletions(-) diff --git a/clients/fides-js/src/fides-tcf.ts b/clients/fides-js/src/fides-tcf.ts index a77a597321..9fd61cfff7 100644 --- a/clients/fides-js/src/fides-tcf.ts +++ b/clients/fides-js/src/fides-tcf.ts @@ -274,34 +274,6 @@ const init = async (config: FidesConfig) => { ...getInitialCookie(config), ...overrides.consentPrefsOverrides?.consent, }; - if (config.options.fidesString) { - const { cookie: updatedCookie, success } = updateFidesCookieFromString( - cookie, - config.options.fidesString, - config.options.debug, - overrides.consentPrefsOverrides?.version_hash - ); - if (success) { - Object.assign(cookie, updatedCookie); - } - } else if ( - tcfConsentCookieObjHasSomeConsentSet(cookie.tcf_consent) && - !cookie.fides_string && - isPrivacyExperience(config.experience) && - experienceIsValid(config.experience, config.options) - ) { - // This state should not be hit, but just in case: if fidesString is missing on cookie but we have tcf consent, - // we should generate fidesString so that our CMP API accurately reflects user preference - cookie.fides_string = await generateFidesStringFromCookieTcfConsent( - config.experience, - cookie.tcf_consent - ); - debugLog( - config.options.debug, - "fides_string was missing from cookie, so it has been generated based on tcf_consent", - cookie.fides_string - ); - } const initialFides = getInitialFides({ ...config, cookie }); // Initialize the CMP API early so that listeners are established initializeTcfCmpApi(); diff --git a/clients/fides-js/src/lib/cookie.ts b/clients/fides-js/src/lib/cookie.ts index 06923fe810..e1d8255fd7 100644 --- a/clients/fides-js/src/lib/cookie.ts +++ b/clients/fides-js/src/lib/cookie.ts @@ -1,6 +1,7 @@ import { v4 as uuidv4 } from "uuid"; import { getCookie, removeCookie, setCookie, Types } from "typescript-cookie"; +import { TCModel, TCString } from "@iabtechlabtcf/core"; import { ConsentContext } from "./consent-context"; import { resolveConsentValue, @@ -29,7 +30,6 @@ import { import { TCF_KEY_MAP } from "./tcf/constants"; import { getConsentFromTcModel } from "~/lib/tcf/utils"; import { decodeFidesString } from "~/lib/tcf/fidesString"; -import { TCModel, TCString } from "@iabtechlabtcf/core"; /** * Store the user's consent preferences on the cookie, as key -> boolean pairs, e.g. @@ -295,10 +295,10 @@ export const buildCookieConsentForExperiences = ( const getOrDefaultPreference = ( tcModel: TCModel | undefined, + acString: string, item: TcfModelsRecord, cookieConsent: TcfCookieKeyConsent, - experienceKey: keyof TcfExperienceRecords, - fidesString?: string | null + experienceKey: keyof TcfExperienceRecords ): UserConsentPreference | undefined => { if (Object.hasOwn(cookieConsent, item.id)) { return transformConsentToFidesUserPreference( @@ -312,6 +312,7 @@ const getOrDefaultPreference = ( if (tcModel) { const consentFromExperience: boolean = getConsentFromTcModel( tcModel, + acString, experienceKey, item.id ); @@ -348,29 +349,30 @@ export const buildTcfEntitiesFromCookieAndFidesString = ( tcf_system_legitimate_interests: experience.tcf_system_legitimate_interests, }; - if (cookie.tcf_consent) { - let tcModel: TCModel | undefined; - if (fidesString) { - // fixme- do we need the ac string? - const { tc: tcString } = decodeFidesString(fidesString); - tcModel = TCString.decode(tcString); - } - TCF_KEY_MAP.forEach(({ cookieKey, experienceKey }) => { - const cookieConsent: TcfCookieKeyConsent = - (cookieKey && cookie.tcf_consent[cookieKey]) ?? {}; - // @ts-ignore the array map should ensure we will get the right record type - tcfEntities[experienceKey] = experience[experienceKey]?.map((item) => { - const preference = getOrDefaultPreference( - tcModel, - item, - cookieConsent, - experienceKey, - fidesString - ); - return { ...item, current_preference: preference }; - }); - }); + let tcModel: TCModel | undefined; + let acString: string; + if (fidesString) { + // fixme- use ac string to get vendor consent prefs + const { tc: tcString, ac } = decodeFidesString(fidesString); + acString = ac; + tcModel = TCString.decode(tcString); } + TCF_KEY_MAP.forEach(({ cookieKey, experienceKey }) => { + const cookieConsent: TcfCookieKeyConsent = + (cookieKey && cookie.tcf_consent[cookieKey]) ?? {}; + // @ts-ignore the array map should ensure we will get the right record type + tcfEntities[experienceKey] = experience[experienceKey]?.map((item) => { + const preference = getOrDefaultPreference( + tcModel, + acString, + item, + cookieConsent, + experienceKey + ); + return { ...item, current_preference: preference }; + }); + }); + return tcfEntities; }; diff --git a/clients/fides-js/src/lib/tcf/utils.ts b/clients/fides-js/src/lib/tcf/utils.ts index 9f01737809..430958e2a9 100644 --- a/clients/fides-js/src/lib/tcf/utils.ts +++ b/clients/fides-js/src/lib/tcf/utils.ts @@ -1,5 +1,5 @@ import { TCModel, TCString, Vector } from "@iabtechlabtcf/core"; -import { PrivacyExperience, UserConsentPreference } from "../consent-types"; +import { PrivacyExperience } from "../consent-types"; import { EnabledIds, TcfCookieConsent, @@ -13,9 +13,15 @@ import { decodeFidesString, idsFromAcString } from "./fidesString"; export const getConsentFromTcModel = ( tcModel: TCModel, + acString: string, experienceKey: keyof TcfExperienceRecords, tcfRecordId: string | number ): boolean => { + const acStringIds = idsFromAcString(acString); + if (acStringIds.find((id) => id === tcfRecordId)) { + return true; + } + // fixme- append gvl for "vendorConsents" and "vendorLegitimateInterests" keys const associatedTCFKey = TCF_KEY_MAP.find( (i) => i.experienceKey === experienceKey )?.tcfModelKey; @@ -24,10 +30,10 @@ export const getConsentFromTcModel = ( if (id === tcfRecordId) { return consented; } + return false; }); - return false; } - // fixme- tcf_vendor_consents comes from both AC string and tc string? + return false; }; export const transformFidesStringToCookieKeys = ( From 718365d1ac3715149a0f1b44cd1b49f13a7d804f Mon Sep 17 00:00:00 2001 From: Allison King Date: Mon, 4 Dec 2023 17:34:59 -0500 Subject: [PATCH 07/16] Refactor initializing from fides string --- clients/fides-js/src/fides-tcf.ts | 179 ++++++++++++++++------ clients/fides-js/src/fides.ts | 6 +- clients/fides-js/src/lib/cookie.ts | 140 ++--------------- clients/fides-js/src/lib/initialize.ts | 11 +- clients/fides-js/src/lib/tcf/constants.ts | 17 +- clients/fides-js/src/lib/tcf/types.ts | 5 - clients/fides-js/src/lib/tcf/utils.ts | 113 -------------- 7 files changed, 170 insertions(+), 301 deletions(-) delete mode 100644 clients/fides-js/src/lib/tcf/utils.ts diff --git a/clients/fides-js/src/fides-tcf.ts b/clients/fides-js/src/fides-tcf.ts index 9fd61cfff7..77657f6c03 100644 --- a/clients/fides-js/src/fides-tcf.ts +++ b/clients/fides-js/src/fides-tcf.ts @@ -46,11 +46,13 @@ * ``` */ import type { TCData } from "@iabtechlabtcf/cmpapi"; +import { TCString } from "@iabtechlabtcf/core"; import { gtm } from "./integrations/gtm"; import { meta } from "./integrations/meta"; import { shopify } from "./integrations/shopify"; import { + ConsentMechanism, FidesConfig, FidesOptionsOverrides, FidesOverrides, @@ -70,13 +72,10 @@ import { import type { Fides } from "./lib/initialize"; import { dispatchFidesEvent } from "./lib/events"; import { - buildTcfEntitiesFromCookieAndFidesString, debugLog, - experienceIsValid, FidesCookie, hasSavedTcfPreferences, - isPrivacyExperience, - tcfConsentCookieObjHasSomeConsentSet, + transformConsentToFidesUserPreference, transformTcfPreferencesToCookieKeys, transformUserPreferenceToBoolean, } from "./fides"; @@ -86,14 +85,11 @@ import { TcfModelsRecord, TcfSavePreferences, } from "./lib/tcf/types"; -import { TCF_KEY_MAP } from "./lib/tcf/constants"; -import { - generateFidesStringFromCookieTcfConsent, - transformFidesStringToCookieKeys, -} from "./lib/tcf/utils"; +import { TCF_COOKIE_KEY_MAP, TCF_KEY_MAP } from "./lib/tcf/constants"; import type { GppFunction } from "./lib/gpp/types"; import { makeStub } from "./lib/tcf/stub"; import { customGetConsentPreferences } from "./services/external/preferences"; +import { decodeFidesString, idsFromAcString } from "./lib/tcf/fidesString"; declare global { interface Window { @@ -128,6 +124,81 @@ const getInitialPreference = ( return tcfObject.default_preference ?? UserConsentPreference.OPT_OUT; }; +/** + * Populates TCF entities with items from cookie.tcf_consent and Fides string. + * Returns TCF entities to be assigned to an experience. + */ +export const buildTcfEntitiesFromCookieAndFidesString = ( + experience: PrivacyExperience, + cookie: FidesCookie, + fidesString?: string | null +) => { + const tcfEntities = { + tcf_purpose_consents: experience.tcf_purpose_consents, + tcf_purpose_legitimate_interests: + experience.tcf_purpose_legitimate_interests, + tcf_special_purposes: experience.tcf_special_purposes, + tcf_features: experience.tcf_features, + tcf_special_features: experience.tcf_special_features, + tcf_vendor_consents: experience.tcf_vendor_consents, + tcf_vendor_legitimate_interests: experience.tcf_vendor_legitimate_interests, + tcf_system_consents: experience.tcf_system_consents, + tcf_system_legitimate_interests: experience.tcf_system_legitimate_interests, + }; + + // First update tcfEntities based on the cookie (system objects) + TCF_COOKIE_KEY_MAP.forEach(({ cookieKey, experienceKey }) => { + const cookieConsent = cookie.tcf_consent[cookieKey] ?? {}; + // @ts-ignore the array map should ensure we will get the right record type + tcfEntities[experienceKey] = experience[experienceKey]?.map((item) => { + const preference = Object.hasOwn(cookieConsent, item.id) + ? transformConsentToFidesUserPreference( + Boolean(cookieConsent[item.id]), + ConsentMechanism.OPT_IN + ) + : item.default_preference; + return { ...item, current_preference: preference }; + }); + }); + + // Now update tcfEntities based on the fides string + if (fidesString) { + const { tc: tcString, ac: acString } = decodeFidesString(fidesString); + const acStringIds = idsFromAcString(acString); + + // Populate every field from tcModel + const tcModel = TCString.decode(tcString); + TCF_KEY_MAP.forEach(({ experienceKey, tcfModelKey }) => { + const isVendorKey = + tcfModelKey === "vendorConsents" || + tcfModelKey === "vendorLegitimateInterests"; + const tcIds = Array.from(tcModel[tcfModelKey]) + .filter(([, consented]) => consented) + .map(([id]) => (isVendorKey ? `gvl.${id}` : id)); + // @ts-ignore the array map should ensure we will get the right record type + tcfEntities[experienceKey] = experience[experienceKey]?.map((item) => { + let consented = !!tcIds.find((id) => id === item.id); + // Also check the AC string, which only applies to tcf_vendor_consents + if ( + experienceKey === "tcf_vendor_consents" && + acStringIds.find((id) => id === item.id) + ) { + consented = true; + } + return { + ...item, + current_preference: transformConsentToFidesUserPreference( + consented, + ConsentMechanism.OPT_IN + ), + }; + }); + }); + } + + return tcfEntities; +}; + const updateCookieAndExperience = async ({ cookie, experience, @@ -169,8 +240,11 @@ const updateCookieAndExperience = async ({ } // If the user has prefs on a client-side fetched experience, but there is no fides_string, - // we need to use the prefs on the experience to generate a fidesString and cookie.tcf_consent - const tcSavePrefs: TcfSavePreferences = {}; + // we need to use the prefs on the experience to generate + // 1. a fidesString + // 2. a cookie.tcf_consent (which only has system preferences since those are not captured in the fidesString) + + // 1. Generate a fidesString from the experience const enabledIds: EnabledIds = { purposesConsent: [], purposesLegint: [], @@ -180,21 +254,9 @@ const updateCookieAndExperience = async ({ vendorsConsent: [], vendorsLegint: [], }; - - TCF_KEY_MAP.forEach(({ experienceKey, cookieKey, enabledIdsKey }) => { - if (cookieKey) { - tcSavePrefs[cookieKey] = []; - } + TCF_KEY_MAP.forEach(({ experienceKey, enabledIdsKey }) => { experience[experienceKey]?.forEach((record) => { const pref: UserConsentPreference = getInitialPreference(record); - // map experience to tcSavePrefs (same as cookie keys) - if (cookieKey) { - tcSavePrefs[cookieKey]?.push({ - // @ts-ignore - id: record.id, - preference: pref, - }); - } // add to enabledIds only if user consent is True if (transformUserPreferenceToBoolean(pref)) { if (enabledIdsKey) { @@ -203,12 +265,23 @@ const updateCookieAndExperience = async ({ } }); }); - const fidesString = await generateFidesString({ experience, tcStringPreferences: enabledIds, }); + + // 2. Generate a cookie object from the experience + const tcSavePrefs: TcfSavePreferences = {}; + TCF_COOKIE_KEY_MAP.forEach(({ cookieKey, experienceKey }) => { + tcSavePrefs[cookieKey] = []; + experience[experienceKey]?.forEach((record) => { + const preference = getInitialPreference(record); + tcSavePrefs[cookieKey]?.push({ id: `${record.id}`, preference }); + }); + }); const tcfConsent = transformTcfPreferencesToCookieKeys(tcSavePrefs); + + // Return the updated cookie return { cookie: { ...cookie, fides_string: fidesString, tcf_consent: tcfConsent }, experience, @@ -216,37 +289,37 @@ const updateCookieAndExperience = async ({ }; /** - * If a fidesString is provided either explicitly or retrieved with a custom get preferences fn, - * we override the associated cookie props, which are then used to override associated props in the experience. + * TCF version of updating prefetched experience, based on: + * 1) experience: pre-fetched or client-side experience-based consent configuration + * 2) cookie: cookie containing user preference. + + * + * Returns updated experience with user preferences. We have a separate function for notices + * and for TCF so that the bundle trees do not overlap. */ -const updateFidesCookieFromString = ( - cookie: FidesCookie, - fidesString: string, - debug: boolean, - fidesStringVersionHash: string | undefined -): { cookie: FidesCookie; success: boolean } => { - debugLog( - debug, - "Explicit fidesString detected. Proceeding to override all TCF preferences with given fidesString" +const updateExperienceFromCookieConsent = ({ + experience, + cookie, + debug, +}: { + experience: PrivacyExperience; + cookie: FidesCookie; + debug?: boolean; +}): PrivacyExperience => { + const tcfEntities = buildTcfEntitiesFromCookieAndFidesString( + experience, + cookie, + cookie.fides_string ); - try { - const cookieKeys = transformFidesStringToCookieKeys(fidesString, debug); - return { - cookie: { - ...cookie, - tcf_consent: cookieKeys, - fides_string: fidesString, - tcf_version_hash: fidesStringVersionHash ?? cookie.tcf_version_hash, - }, - success: true, - }; - } catch (error) { + + if (debug) { debugLog( debug, - `Could not decode tcString from ${fidesString}, it may be invalid. ${error}` + `Returning updated pre-fetched experience with user consent.`, + experience ); - return { cookie, success: false }; } + return { ...experience, ...tcfEntities }; }; /** @@ -274,7 +347,11 @@ const init = async (config: FidesConfig) => { ...getInitialCookie(config), ...overrides.consentPrefsOverrides?.consent, }; - const initialFides = getInitialFides({ ...config, cookie }); + const initialFides = getInitialFides({ + ...config, + cookie, + updateExperienceFromCookieConsent, + }); // Initialize the CMP API early so that listeners are established initializeTcfCmpApi(); if (initialFides) { diff --git a/clients/fides-js/src/fides.ts b/clients/fides-js/src/fides.ts index da5bc1c87b..0d10769531 100644 --- a/clients/fides-js/src/fides.ts +++ b/clients/fides-js/src/fides.ts @@ -141,7 +141,11 @@ const init = async (config: FidesConfig) => { ...getInitialCookie(config), ...overrides.consentPrefsOverrides?.consent, }; - const initialFides = getInitialFides({ ...config, cookie }); + const initialFides = getInitialFides({ + ...config, + cookie, + updateExperienceFromCookieConsent, + }); if (initialFides) { Object.assign(_Fides, initialFides); dispatchFidesEvent("FidesInitialized", cookie, config.options.debug); diff --git a/clients/fides-js/src/lib/cookie.ts b/clients/fides-js/src/lib/cookie.ts index e1d8255fd7..e2990a20dc 100644 --- a/clients/fides-js/src/lib/cookie.ts +++ b/clients/fides-js/src/lib/cookie.ts @@ -1,20 +1,17 @@ import { v4 as uuidv4 } from "uuid"; import { getCookie, removeCookie, setCookie, Types } from "typescript-cookie"; -import { TCModel, TCString } from "@iabtechlabtcf/core"; import { ConsentContext } from "./consent-context"; import { resolveConsentValue, resolveLegacyConsentValue, } from "./consent-value"; import { - ConsentMechanism, Cookies, ExperienceMeta, LegacyConsentConfig, PrivacyExperience, SaveConsentPreference, - UserConsentPreference, } from "./consent-types"; import { debugLog, @@ -22,14 +19,7 @@ import { transformUserPreferenceToBoolean, } from "./consent-utils"; import type { TcfCookieConsent, TcfSavePreferences } from "./tcf/types"; -import { - TcfCookieKeyConsent, - TcfExperienceRecords, - TcfModelsRecord, -} from "./tcf/types"; -import { TCF_KEY_MAP } from "./tcf/constants"; -import { getConsentFromTcModel } from "~/lib/tcf/utils"; -import { decodeFidesString } from "~/lib/tcf/fidesString"; +import { TCF_COOKIE_KEY_MAP } from "./tcf/constants"; /** * Store the user's consent preferences on the cookie, as key -> boolean pairs, e.g. @@ -92,17 +82,6 @@ const CODEC: Types.CookieCodecConfig = { encodeValue: encodeURIComponent, }; -export const tcfConsentCookieObjHasSomeConsentSet = ( - tcf_consent: TcfCookieConsent | undefined -): boolean => { - if (!tcf_consent) { - return false; - } - return Object.values(tcf_consent).some( - (val: TcfCookieKeyConsent) => Object.keys(val).length >= 0 - ); -}; - export const consentCookieObjHasSomeConsentSet = ( consent: CookieKeyConsent | undefined ): boolean => { @@ -293,89 +272,6 @@ export const buildCookieConsentForExperiences = ( return cookieConsent; }; -const getOrDefaultPreference = ( - tcModel: TCModel | undefined, - acString: string, - item: TcfModelsRecord, - cookieConsent: TcfCookieKeyConsent, - experienceKey: keyof TcfExperienceRecords -): UserConsentPreference | undefined => { - if (Object.hasOwn(cookieConsent, item.id)) { - return transformConsentToFidesUserPreference( - Boolean(cookieConsent[item.id]), - ConsentMechanism.OPT_IN - ); - } - // If experience contains a tcf entity not defined by tcfEntities on the cookie, this means: - // A) If fides_string exists, we check field on tcModel. If it doesn't exist, since opt-outs are not tracked by TC string, we assume opt-out. - // B) There is a new tcf entity that requires consent. In this case we would use the default on the experience. - if (tcModel) { - const consentFromExperience: boolean = getConsentFromTcModel( - tcModel, - acString, - experienceKey, - item.id - ); - if (consentFromExperience) { - return transformConsentToFidesUserPreference( - consentFromExperience, - ConsentMechanism.OPT_IN - ); - } - return UserConsentPreference.OPT_OUT; - } - return item.default_preference; -}; - -/** - * Populates TCF entities with items from cookie.tcf_consent and Fides string. - * Returns TCF entities to be assigned to an experience. - */ -export const buildTcfEntitiesFromCookieAndFidesString = ( - experience: PrivacyExperience, - cookie: FidesCookie, - fidesString?: string | null -) => { - const tcfEntities = { - tcf_purpose_consents: experience.tcf_purpose_consents, - tcf_purpose_legitimate_interests: - experience.tcf_purpose_legitimate_interests, - tcf_special_purposes: experience.tcf_special_purposes, - tcf_features: experience.tcf_features, - tcf_special_features: experience.tcf_special_features, - tcf_vendor_consents: experience.tcf_vendor_consents, - tcf_vendor_legitimate_interests: experience.tcf_vendor_legitimate_interests, - tcf_system_consents: experience.tcf_system_consents, - tcf_system_legitimate_interests: experience.tcf_system_legitimate_interests, - }; - - let tcModel: TCModel | undefined; - let acString: string; - if (fidesString) { - // fixme- use ac string to get vendor consent prefs - const { tc: tcString, ac } = decodeFidesString(fidesString); - acString = ac; - tcModel = TCString.decode(tcString); - } - TCF_KEY_MAP.forEach(({ cookieKey, experienceKey }) => { - const cookieConsent: TcfCookieKeyConsent = - (cookieKey && cookie.tcf_consent[cookieKey]) ?? {}; - // @ts-ignore the array map should ensure we will get the right record type - tcfEntities[experienceKey] = experience[experienceKey]?.map((item) => { - const preference = getOrDefaultPreference( - tcModel, - acString, - item, - cookieConsent, - experienceKey - ); - return { ...item, current_preference: preference }; - }); - }); - - return tcfEntities; -}; - /** * Updates prefetched experience, based on: * 1) experience: pre-fetched or client-side experience-based consent configuration @@ -388,12 +284,10 @@ export const updateExperienceFromCookieConsent = ({ experience, cookie, debug, - fidesString, }: { experience: PrivacyExperience; cookie: FidesCookie; debug?: boolean; - fidesString?: string | null; }): PrivacyExperience => { const noticesWithConsent = experience.privacy_notices?.map((notice) => { // Prefers preference in cookie if it exists, else uses current preference on the notice if it exists, else uses @@ -408,12 +302,12 @@ export const updateExperienceFromCookieConsent = ({ return { ...notice, current_preference: preference }; }); - // Handle the TCF case, which has many keys to query - const tcfEntities = buildTcfEntitiesFromCookieAndFidesString( - experience, - cookie, - fidesString - ); + // // Handle the TCF case, which has many keys to query + // const tcfEntities = buildTcfEntitiesFromCookieAndFidesString( + // experience, + // cookie, + // fidesString + // ); if (debug) { debugLog( @@ -422,23 +316,21 @@ export const updateExperienceFromCookieConsent = ({ experience ); } - return { ...experience, ...tcfEntities, privacy_notices: noticesWithConsent }; + return { ...experience, privacy_notices: noticesWithConsent }; }; export const transformTcfPreferencesToCookieKeys = ( tcfPreferences: TcfSavePreferences ): TcfCookieConsent => { const cookieKeys: TcfCookieConsent = {}; - TCF_KEY_MAP.forEach(({ cookieKey }) => { - if (cookieKey) { - const preferences = tcfPreferences[cookieKey] ?? []; - cookieKeys[cookieKey] = Object.fromEntries( - preferences.map((pref) => [ - pref.id, - transformUserPreferenceToBoolean(pref.preference), - ]) - ); - } + TCF_COOKIE_KEY_MAP.forEach(({ cookieKey }) => { + const preferences = tcfPreferences[cookieKey] ?? []; + cookieKeys[cookieKey] = Object.fromEntries( + preferences.map((pref) => [ + pref.id, + transformUserPreferenceToBoolean(pref.preference), + ]) + ); }); return cookieKeys; }; diff --git a/clients/fides-js/src/lib/initialize.ts b/clients/fides-js/src/lib/initialize.ts index f70267f16e..bb064d9d30 100644 --- a/clients/fides-js/src/lib/initialize.ts +++ b/clients/fides-js/src/lib/initialize.ts @@ -13,7 +13,6 @@ import { isNewFidesCookie, makeConsentDefaultsLegacy, updateCookieFromNoticePreferences, - updateExperienceFromCookieConsent, } from "./cookie"; import { ConsentMechanism, @@ -210,9 +209,16 @@ export const getInitialFides = ({ experience, geolocation, options, + updateExperienceFromCookieConsent, }: { cookie: FidesCookie; -} & FidesConfig): Partial | null => { +} & FidesConfig & { + updateExperienceFromCookieConsent: (props: { + experience: PrivacyExperience; + cookie: FidesCookie; + debug: boolean; + }) => PrivacyExperience; + }): Partial | null => { const hasExistingCookie = !isNewFidesCookie(cookie); if (!hasExistingCookie && !options.fidesString) { // A TC str can be injected and take effect even if the user has no previous Fides Cookie @@ -225,7 +231,6 @@ export const getInitialFides = ({ experience, cookie, debug: options.debug, - fidesString: options.fidesString, }); } diff --git a/clients/fides-js/src/lib/tcf/constants.ts b/clients/fides-js/src/lib/tcf/constants.ts index 0b5df2220a..f6a44d037e 100644 --- a/clients/fides-js/src/lib/tcf/constants.ts +++ b/clients/fides-js/src/lib/tcf/constants.ts @@ -1,4 +1,3 @@ -import { TCModel } from "@iabtechlabtcf/core"; import { EnabledIds, LegalBasisEnum, @@ -18,10 +17,14 @@ export const ETHYCA_CMP_ID = 407; export const FIDES_SEPARATOR = ","; export const TCF_KEY_MAP: { - cookieKey?: TcfModelType; experienceKey: keyof TcfExperienceRecords; - tcfModelKey?: keyof TCModel; - enabledIdsKey?: keyof EnabledIds; + tcfModelKey: + | "purposeConsents" + | "purposeLegitimateInterests" + | "specialFeatureOptins" + | "vendorConsents" + | "vendorLegitimateInterests"; + enabledIdsKey: keyof EnabledIds; }[] = [ { experienceKey: "tcf_purpose_consents", @@ -48,6 +51,12 @@ export const TCF_KEY_MAP: { tcfModelKey: "vendorLegitimateInterests", enabledIdsKey: "vendorsLegint", }, +]; + +export const TCF_COOKIE_KEY_MAP: { + cookieKey: TcfModelType; + experienceKey: keyof TcfExperienceRecords; +}[] = [ { cookieKey: "system_consent_preferences", experienceKey: "tcf_system_consents", diff --git a/clients/fides-js/src/lib/tcf/types.ts b/clients/fides-js/src/lib/tcf/types.ts index 3465f78909..a9b70134ea 100644 --- a/clients/fides-js/src/lib/tcf/types.ts +++ b/clients/fides-js/src/lib/tcf/types.ts @@ -235,11 +235,6 @@ export type TcfCookieKeyConsent = { }; export interface TcfCookieConsent { - purpose_consent_preferences?: TcfCookieKeyConsent; - purpose_legitimate_interests_preferences?: TcfCookieKeyConsent; - special_feature_preferences?: TcfCookieKeyConsent; - vendor_consent_preferences?: TcfCookieKeyConsent; - vendor_legitimate_interests_preferences?: TcfCookieKeyConsent; system_consent_preferences?: TcfCookieKeyConsent; system_legitimate_interests_preferences?: TcfCookieKeyConsent; } diff --git a/clients/fides-js/src/lib/tcf/utils.ts b/clients/fides-js/src/lib/tcf/utils.ts deleted file mode 100644 index 430958e2a9..0000000000 --- a/clients/fides-js/src/lib/tcf/utils.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { TCModel, TCString, Vector } from "@iabtechlabtcf/core"; -import { PrivacyExperience } from "../consent-types"; -import { - EnabledIds, - TcfCookieConsent, - TcfCookieKeyConsent, - TcfExperienceRecords, -} from "./types"; -import { TCF_KEY_MAP } from "./constants"; -import { generateFidesString } from "../tcf"; -import { debugLog } from "../consent-utils"; -import { decodeFidesString, idsFromAcString } from "./fidesString"; - -export const getConsentFromTcModel = ( - tcModel: TCModel, - acString: string, - experienceKey: keyof TcfExperienceRecords, - tcfRecordId: string | number -): boolean => { - const acStringIds = idsFromAcString(acString); - if (acStringIds.find((id) => id === tcfRecordId)) { - return true; - } - // fixme- append gvl for "vendorConsents" and "vendorLegitimateInterests" keys - const associatedTCFKey = TCF_KEY_MAP.find( - (i) => i.experienceKey === experienceKey - )?.tcfModelKey; - if (associatedTCFKey) { - (tcModel[associatedTCFKey] as Vector).forEach((consented, id) => { - if (id === tcfRecordId) { - return consented; - } - return false; - }); - } - return false; -}; - -export const transformFidesStringToCookieKeys = ( - fidesString: string, - debug: boolean -): TcfCookieConsent => { - const { tc: tcString } = decodeFidesString(fidesString); - const tcModel: TCModel = TCString.decode(tcString); - - const cookieKeys: TcfCookieConsent = {}; - - // map tc model key to cookie key - TCF_KEY_MAP.forEach(({ tcfModelKey, cookieKey }) => { - const isVendorKey = - tcfModelKey === "vendorConsents" || - tcfModelKey === "vendorLegitimateInterests"; - if (tcfModelKey) { - const items: TcfCookieKeyConsent = {}; - (tcModel[tcfModelKey] as Vector).forEach((consented, id) => { - const key = isVendorKey ? `gvl.${id}` : id; - items[key] = consented; - }); - if (cookieKey) { - cookieKeys[cookieKey] = items; - } - } - }); - - // fixme- Keep for reference but remove before merging - // from the fides_string - // Set AC consents, which will only be on vendor_consents - // const acIds = idsFromAcString(acString, debug); - // acIds.forEach((acId) => { - // if (!cookieKeys.vendor_consent_preferences) { - // cookieKeys.vendor_consent_preferences = { [acId]: true }; - // } else { - // cookieKeys.vendor_consent_preferences[acId] = true; - // } - // }); - debugLog( - debug, - `Generated cookie.tcf_consent from explicit fidesString.`, - cookieKeys - ); - return cookieKeys; -}; - -export const generateFidesStringFromCookieTcfConsent = async ( - experience: PrivacyExperience, - tcfConsent: TcfCookieConsent -): Promise => { - const enabledIds: EnabledIds = { - purposesConsent: [], - purposesLegint: [], - specialPurposes: [], - features: [], - specialFeatures: [], - vendorsConsent: [], - vendorsLegint: [], - }; - - TCF_KEY_MAP.forEach(({ cookieKey, enabledIdsKey }) => { - const cookieKeyConsent: TcfCookieKeyConsent | undefined = - cookieKey && tcfConsent[cookieKey]; - if (cookieKeyConsent) { - Object.keys(cookieKeyConsent).forEach((key: string | number) => { - if (cookieKeyConsent[key] && enabledIdsKey) { - enabledIds[enabledIdsKey].push(key.toString()); - } - }); - } - }); - return generateFidesString({ - experience, - tcStringPreferences: enabledIds, - }); -}; From 1f4d62de7962ea18c67d94a36e3ab82dd967ab86 Mon Sep 17 00:00:00 2001 From: Allison King Date: Mon, 4 Dec 2023 18:04:38 -0500 Subject: [PATCH 08/16] Restore override logic --- clients/fides-js/src/fides-tcf.ts | 36 ++++++++++++++----- .../cypress/e2e/consent-banner-tcf.cy.ts | 5 +-- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/clients/fides-js/src/fides-tcf.ts b/clients/fides-js/src/fides-tcf.ts index 77657f6c03..94fb02780b 100644 --- a/clients/fides-js/src/fides-tcf.ts +++ b/clients/fides-js/src/fides-tcf.ts @@ -130,8 +130,7 @@ const getInitialPreference = ( */ export const buildTcfEntitiesFromCookieAndFidesString = ( experience: PrivacyExperience, - cookie: FidesCookie, - fidesString?: string | null + cookie: FidesCookie ) => { const tcfEntities = { tcf_purpose_consents: experience.tcf_purpose_consents, @@ -162,8 +161,10 @@ export const buildTcfEntitiesFromCookieAndFidesString = ( }); // Now update tcfEntities based on the fides string - if (fidesString) { - const { tc: tcString, ac: acString } = decodeFidesString(fidesString); + if (cookie.fides_string) { + const { tc: tcString, ac: acString } = decodeFidesString( + cookie.fides_string + ); const acStringIds = idsFromAcString(acString); // Populate every field from tcModel @@ -228,8 +229,7 @@ const updateCookieAndExperience = async ({ ); const tcfEntities = buildTcfEntitiesFromCookieAndFidesString( experience, - cookie, - cookie.fides_string + cookie ); return { cookie, experience: tcfEntities }; } @@ -308,8 +308,7 @@ const updateExperienceFromCookieConsent = ({ }): PrivacyExperience => { const tcfEntities = buildTcfEntitiesFromCookieAndFidesString( experience, - cookie, - cookie.fides_string + cookie ); if (debug) { @@ -347,6 +346,27 @@ const init = async (config: FidesConfig) => { ...getInitialCookie(config), ...overrides.consentPrefsOverrides?.consent, }; + // Update the fidesString if we have an override and the TC portion is valid + const { fidesString } = config.options; + if (fidesString) { + try { + // Make sure TC string is valid before we assign it + const { tc: tcString } = decodeFidesString(fidesString); + TCString.decode(tcString); + const updatedCookie: Partial = { + fides_string: fidesString, + tcf_version_hash: + overrides.consentPrefsOverrides?.version_hash ?? + cookie.tcf_version_hash, + }; + Object.assign(cookie, updatedCookie); + } catch (error) { + debugLog( + config.options.debug, + `Could not decode tcString from ${fidesString}, it may be invalid. ${error}` + ); + } + } const initialFides = getInitialFides({ ...config, cookie, 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 5886b5ae62..886735b88b 100644 --- a/clients/privacy-center/cypress/e2e/consent-banner-tcf.cy.ts +++ b/clients/privacy-center/cypress/e2e/consent-banner-tcf.cy.ts @@ -1540,7 +1540,6 @@ describe("Fides-js TCF", () => { * ❌ 5) experience API (via GET /privacy-experience) * * EXPECTED RESULT: use preferences from local cookie's saved string - * TODO: CURRENTLY FAILING!! */ it("prefers preferences from a cookie's fides_string when both cookie and experience exist", () => { setFidesCookie(); @@ -1689,6 +1688,7 @@ describe("Fides-js TCF", () => { */ it("prefers preferences from fides_string option when fides_string, experience, and cookie exist", () => { setFidesCookie(); + // Purpose 7, Special Feature 1 const fidesStringOverride = "CPzevcAPzevcAGXABBENATEIAAIAAAAAAAAAAAAAAAAA.IABE,1~"; const expectedTCString = "CPzevcAPzevcAGXABBENATEIAAIAAAAAAAAAAAAAAAAA"; // without disclosed vendors @@ -1840,8 +1840,9 @@ describe("Fides-js TCF", () => { }); cy.get("#fides-panel-Vendors").within(() => { cy.get("button").contains("Legitimate interest").click(); + // Should be checked because legitimate interest defaults to true and Systems aren't in the fides string cy.getByTestId(`toggle-${SYSTEM_1.name}`).within(() => { - cy.get("input").should("not.be.checked"); + cy.get("input").should("be.checked"); }); }); From 070a2336011c6a088ee36bd9bbf3cd09c6ad9bb7 Mon Sep 17 00:00:00 2001 From: eastandwestwind Date: Tue, 5 Dec 2023 16:43:49 +0100 Subject: [PATCH 09/16] update cookie test --- clients/fides-js/__tests__/lib/cookie.test.ts | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/clients/fides-js/__tests__/lib/cookie.test.ts b/clients/fides-js/__tests__/lib/cookie.test.ts index ed0314a8c1..c9872bd62e 100644 --- a/clients/fides-js/__tests__/lib/cookie.test.ts +++ b/clients/fides-js/__tests__/lib/cookie.test.ts @@ -346,11 +346,6 @@ describe("transformTcfPreferencesToCookieKeys", () => { it("can handle empty preferences", () => { const preferences: TcfSavePreferences = { purpose_consent_preferences: [] }; const expected: TcfCookieConsent = { - purpose_consent_preferences: {}, - purpose_legitimate_interests_preferences: {}, - special_feature_preferences: {}, - vendor_consent_preferences: {}, - vendor_legitimate_interests_preferences: {}, system_consent_preferences: {}, system_legitimate_interests_preferences: {}, }; @@ -383,11 +378,6 @@ describe("transformTcfPreferencesToCookieKeys", () => { ], }; const expected: TcfCookieConsent = { - purpose_consent_preferences: { 1: true }, - purpose_legitimate_interests_preferences: { 1: false }, - special_feature_preferences: { 1: true, 2: false }, - vendor_consent_preferences: { 1111: false }, - vendor_legitimate_interests_preferences: { 1111: true }, system_consent_preferences: { ctl_test_system: true }, system_legitimate_interests_preferences: { ctl_test_system: true }, }; @@ -555,7 +545,7 @@ describe("updateExperienceFromCookieConsent", () => { const cookie = { ...baseCookie, tcf_consent: { - purpose_consent_preferences: { + system_consent_preferences: { 1: true, 2: false, 555: false, @@ -566,7 +556,7 @@ describe("updateExperienceFromCookieConsent", () => { experience: experienceWithTcf, cookie, }); - expect(updatedExperience.tcf_purpose_consents).toEqual([ + expect(updatedExperience.tcf_system_consents).toEqual([ { id: 1, current_preference: UserConsentPreference.OPT_IN }, { id: 2, @@ -599,7 +589,7 @@ describe("updateExperienceFromCookieConsent", () => { ...baseCookie, consent: { one: true, two: false }, tcf_consent: { - purpose_consent_preferences: { + system_consent_preferences: { 1: true, 2: false, }, @@ -617,7 +607,7 @@ describe("updateExperienceFromCookieConsent", () => { }, { notice_key: "three", current_preference: undefined }, ]); - expect(updatedExperience.tcf_purpose_consents).toEqual([ + expect(updatedExperience.tcf_system_consents).toEqual([ { id: 1, current_preference: UserConsentPreference.OPT_IN }, { id: 2, From 4697684123a6df43d8dd55fe0e95094b47209e80 Mon Sep 17 00:00:00 2001 From: eastandwestwind Date: Tue, 5 Dec 2023 17:45:34 +0100 Subject: [PATCH 10/16] fix some tests, comment out some unit tests that are not working due to export issue --- clients/fides-js/__tests__/lib/cookie.test.ts | 271 +++++++----------- clients/fides-js/src/fides-tcf.ts | 4 +- clients/fides-js/src/fides.ts | 6 +- clients/fides-js/src/lib/cookie.ts | 9 +- 4 files changed, 115 insertions(+), 175 deletions(-) diff --git a/clients/fides-js/__tests__/lib/cookie.test.ts b/clients/fides-js/__tests__/lib/cookie.test.ts index c9872bd62e..48eb17ce50 100644 --- a/clients/fides-js/__tests__/lib/cookie.test.ts +++ b/clients/fides-js/__tests__/lib/cookie.test.ts @@ -12,7 +12,7 @@ import { saveFidesCookie, transformTcfPreferencesToCookieKeys, updateCookieFromNoticePreferences, - updateExperienceFromCookieConsent, + updateExperienceFromCookieConsentNotices, } from "../../src/lib/cookie"; import type { ConsentContext } from "../../src/lib/consent-context"; import { @@ -30,6 +30,7 @@ import { TcfExperienceRecords, TcfSavePreferences, } from "../../src/lib/tcf/types"; +// import {updateExperienceFromCookieConsentTcf} from "../../src/fides-tcf"; // Setup mock date const MOCK_DATE = "2023-01-01T12:00:00.000Z"; @@ -428,7 +429,7 @@ describe("updateExperienceFromCookieConsent", () => { describe("notices", () => { it("can handle an empty cookie", () => { const cookie = { ...baseCookie, consent: {} }; - const updatedExperience = updateExperienceFromCookieConsent({ + const updatedExperience = updateExperienceFromCookieConsentNotices({ experience: experienceWithNotices, cookie, }); @@ -441,7 +442,7 @@ describe("updateExperienceFromCookieConsent", () => { it("can handle updating preferences", () => { const cookie = { ...baseCookie, consent: { one: true, two: false } }; - const updatedExperience = updateExperienceFromCookieConsent({ + const updatedExperience = updateExperienceFromCookieConsentNotices({ experience: experienceWithNotices, cookie, }); @@ -460,7 +461,7 @@ describe("updateExperienceFromCookieConsent", () => { ...baseCookie, consent: { one: true, two: false, fake: true }, }; - const updatedExperience = updateExperienceFromCookieConsent({ + const updatedExperience = updateExperienceFromCookieConsentNotices({ experience: experienceWithNotices, cookie, }); @@ -475,164 +476,110 @@ describe("updateExperienceFromCookieConsent", () => { }); }); - describe("tcf", () => { - it("can handle an empty tcf cookie", () => { - const updatedExperience = updateExperienceFromCookieConsent({ - experience: experienceWithTcf, - cookie: baseCookie, - }); - expect(updatedExperience.tcf_purpose_consents).toEqual([ - { id: 1, current_preference: undefined }, - { - id: 2, - current_preference: undefined, - }, - { id: 3, current_preference: undefined }, - ]); - }); - - it("can handle updating preferences", () => { - const cookie = { - ...baseCookie, - tcf_consent: { - purpose_consent_preferences: { - 1: true, - 2: false, - }, - system_consent_preferences: { - 1111: true, - ctl_test_system: false, - }, - }, - }; - const updatedExperience = updateExperienceFromCookieConsent({ - experience: experienceWithTcf, - cookie, - }); - expect(updatedExperience.tcf_purpose_consents).toEqual([ - { id: 1, current_preference: UserConsentPreference.OPT_IN }, - { - id: 2, - current_preference: UserConsentPreference.OPT_OUT, - }, - { id: 3, current_preference: undefined }, - ]); - expect(updatedExperience.tcf_system_consents).toEqual([ - { id: "1111", current_preference: UserConsentPreference.OPT_IN }, - { - id: "ctl_test_system", - current_preference: UserConsentPreference.OPT_OUT, - }, - ]); - // The rest should be undefined - const keys: Array = [ - "tcf_purpose_legitimate_interests", - "tcf_special_purposes", - "tcf_features", - "tcf_special_features", - "tcf_vendor_consents", - "tcf_vendor_legitimate_interests", - "tcf_system_legitimate_interests", - ]; - keys.forEach((key) => { - updatedExperience[key]?.forEach((f) => { - expect(f.current_preference).toEqual(undefined); - }); - }); - }); - - it("can handle when cookie has values not in the experience", () => { - const cookie = { - ...baseCookie, - tcf_consent: { - system_consent_preferences: { - 1: true, - 2: false, - 555: false, - }, - }, - }; - const updatedExperience = updateExperienceFromCookieConsent({ - experience: experienceWithTcf, - cookie, - }); - expect(updatedExperience.tcf_system_consents).toEqual([ - { id: 1, current_preference: UserConsentPreference.OPT_IN }, - { - id: 2, - current_preference: UserConsentPreference.OPT_OUT, - }, - { id: 3, current_preference: undefined }, - ]); - - // The rest should be undefined - const keys: Array = [ - "tcf_purpose_legitimate_interests", - "tcf_special_purposes", - "tcf_features", - "tcf_special_features", - "tcf_vendor_consents", - "tcf_vendor_legitimate_interests", - "tcf_system_consents", - "tcf_system_legitimate_interests", - ]; - keys.forEach((key) => { - updatedExperience[key]?.forEach((f) => { - expect(f.current_preference).toEqual(undefined); - }); - }); - }); - }); - it("can handle both notices and tcf", () => { - const experience = { ...experienceWithNotices, ...experienceWithTcf }; - const cookie = { - ...baseCookie, - consent: { one: true, two: false }, - tcf_consent: { - system_consent_preferences: { - 1: true, - 2: false, - }, - }, - }; - const updatedExperience = updateExperienceFromCookieConsent({ - experience, - cookie, - }); - expect(updatedExperience.privacy_notices).toEqual([ - { notice_key: "one", current_preference: UserConsentPreference.OPT_IN }, - { - notice_key: "two", - current_preference: UserConsentPreference.OPT_OUT, - }, - { notice_key: "three", current_preference: undefined }, - ]); - expect(updatedExperience.tcf_system_consents).toEqual([ - { id: 1, current_preference: UserConsentPreference.OPT_IN }, - { - id: 2, - current_preference: UserConsentPreference.OPT_OUT, - }, - { id: 3, current_preference: undefined }, - ]); - - // The rest should be undefined - const keys: Array = [ - "tcf_purpose_legitimate_interests", - "tcf_special_purposes", - "tcf_features", - "tcf_special_features", - "tcf_vendor_consents", - "tcf_vendor_legitimate_interests", - "tcf_system_consents", - "tcf_system_legitimate_interests", - ]; - keys.forEach((key) => { - updatedExperience[key]?.forEach((f) => { - expect(f.current_preference).toEqual(undefined); - }); - }); - }); + // describe("tcf", () => { + // it("can handle an empty tcf cookie", () => { + // const updatedExperience = updateExperienceFromCookieConsentNotices({ + // experience: experienceWithTcf, + // cookie: baseCookie, + // }); + // expect(updatedExperience.tcf_purpose_consents).toEqual([ + // { id: 1, current_preference: undefined }, + // { + // id: 2, + // current_preference: undefined, + // }, + // { id: 3, current_preference: undefined }, + // ]); + // }); + // + // it("can handle updating preferences", () => { + // const cookie = { + // ...baseCookie, + // tcf_consent: { + // system_consent_preferences: { + // 1111: true, + // ctl_test_system: false, + // }, + // }, + // }; + // const updatedExperience = updateExperienceFromCookieConsentTcf({ + // experience: experienceWithTcf, + // cookie, + // }); + // expect(updatedExperience.tcf_purpose_consents).toEqual([ + // { id: 1, current_preference: undefined }, + // { + // id: 2, + // current_preference: undefined, + // }, + // { id: 3, current_preference: undefined }, + // ]); + // expect(updatedExperience.tcf_system_consents).toEqual([ + // { id: "1111", current_preference: UserConsentPreference.OPT_IN }, + // { + // id: "ctl_test_system", + // current_preference: UserConsentPreference.OPT_OUT, + // }, + // ]); + // // The rest should be undefined + // const keys: Array = [ + // "tcf_purpose_legitimate_interests", + // "tcf_special_purposes", + // "tcf_features", + // "tcf_special_features", + // "tcf_vendor_consents", + // "tcf_vendor_legitimate_interests", + // "tcf_system_legitimate_interests", + // ]; + // keys.forEach((key) => { + // updatedExperience[key]?.forEach((f) => { + // expect(f.current_preference).toEqual(undefined); + // }); + // }); + // }); + // + // it("can handle when cookie has values not in the experience", () => { + // const cookie = { + // ...baseCookie, + // tcf_consent: { + // system_consent_preferences: { + // 1: true, + // 2: false, + // 555: false, + // }, + // }, + // }; + // const updatedExperience = updateExperienceFromCookieConsentTcf({ + // experience: experienceWithTcf, + // cookie, + // }); + // expect(updatedExperience.tcf_system_consents).toEqual([ + // { id: 1, current_preference: UserConsentPreference.OPT_IN }, + // { + // id: 2, + // current_preference: UserConsentPreference.OPT_OUT, + // }, + // { id: 3, current_preference: undefined }, + // ]); + // + // // The rest should be undefined + // const keys: Array = [ + // "tcf_purpose_legitimate_interests", + // "tcf_special_purposes", + // "tcf_features", + // "tcf_special_features", + // "tcf_vendor_consents", + // "tcf_vendor_legitimate_interests", + // "tcf_purpose_consents", + // "tcf_system_legitimate_interests", + // ]; + // keys.forEach((key) => { + // updatedExperience[key]?.forEach((f) => { + // expect(f.current_preference).toEqual(undefined); + // }); + // }); + // }); + // }); }); describe("updateCookieFromNoticePreferences", () => { diff --git a/clients/fides-js/src/fides-tcf.ts b/clients/fides-js/src/fides-tcf.ts index 94fb02780b..4ef2690ec1 100644 --- a/clients/fides-js/src/fides-tcf.ts +++ b/clients/fides-js/src/fides-tcf.ts @@ -297,7 +297,7 @@ const updateCookieAndExperience = async ({ * Returns updated experience with user preferences. We have a separate function for notices * and for TCF so that the bundle trees do not overlap. */ -const updateExperienceFromCookieConsent = ({ +const updateExperienceFromCookieConsentTcf = ({ experience, cookie, debug, @@ -370,7 +370,7 @@ const init = async (config: FidesConfig) => { const initialFides = getInitialFides({ ...config, cookie, - updateExperienceFromCookieConsent, + updateExperienceFromCookieConsent: updateExperienceFromCookieConsentTcf, }); // Initialize the CMP API early so that listeners are established initializeTcfCmpApi(); diff --git a/clients/fides-js/src/fides.ts b/clients/fides-js/src/fides.ts index 0d10769531..dc547143b5 100644 --- a/clients/fides-js/src/fides.ts +++ b/clients/fides-js/src/fides.ts @@ -51,7 +51,7 @@ import { shopify } from "./integrations/shopify"; import { FidesCookie, buildCookieConsentForExperiences, - updateExperienceFromCookieConsent, + updateExperienceFromCookieConsentNotices, consentCookieObjHasSomeConsentSet, } from "./lib/cookie"; import { @@ -104,7 +104,7 @@ const updateCookie = async ( if (isExperienceClientSideFetched && preferencesExistOnCookie) { // If we have some preferences on the cookie, we update client-side experience with those preferences // if the name matches - updatedExperience = updateExperienceFromCookieConsent({ + updatedExperience = updateExperienceFromCookieConsentNotices({ experience, cookie: oldCookie, debug, @@ -144,7 +144,7 @@ const init = async (config: FidesConfig) => { const initialFides = getInitialFides({ ...config, cookie, - updateExperienceFromCookieConsent, + updateExperienceFromCookieConsent: updateExperienceFromCookieConsentNotices, }); if (initialFides) { Object.assign(_Fides, initialFides); diff --git a/clients/fides-js/src/lib/cookie.ts b/clients/fides-js/src/lib/cookie.ts index e2990a20dc..647177a53a 100644 --- a/clients/fides-js/src/lib/cookie.ts +++ b/clients/fides-js/src/lib/cookie.ts @@ -280,7 +280,7 @@ export const buildCookieConsentForExperiences = ( * * Returns updated experience with user preferences. */ -export const updateExperienceFromCookieConsent = ({ +export const updateExperienceFromCookieConsentNotices = ({ experience, cookie, debug, @@ -302,13 +302,6 @@ export const updateExperienceFromCookieConsent = ({ return { ...notice, current_preference: preference }; }); - // // Handle the TCF case, which has many keys to query - // const tcfEntities = buildTcfEntitiesFromCookieAndFidesString( - // experience, - // cookie, - // fidesString - // ); - if (debug) { debugLog( debug, From 7af0c9b0725a1dceb384516aca7bbc7d2e6163b0 Mon Sep 17 00:00:00 2001 From: eastandwestwind Date: Tue, 5 Dec 2023 17:50:38 +0100 Subject: [PATCH 11/16] remove unneeded tcf test from cookie unit test --- clients/fides-js/__tests__/lib/cookie.test.ts | 105 ------------------ 1 file changed, 105 deletions(-) diff --git a/clients/fides-js/__tests__/lib/cookie.test.ts b/clients/fides-js/__tests__/lib/cookie.test.ts index 48eb17ce50..2ce5aeda54 100644 --- a/clients/fides-js/__tests__/lib/cookie.test.ts +++ b/clients/fides-js/__tests__/lib/cookie.test.ts @@ -475,111 +475,6 @@ describe("updateExperienceFromCookieConsent", () => { ]); }); }); - - // describe("tcf", () => { - // it("can handle an empty tcf cookie", () => { - // const updatedExperience = updateExperienceFromCookieConsentNotices({ - // experience: experienceWithTcf, - // cookie: baseCookie, - // }); - // expect(updatedExperience.tcf_purpose_consents).toEqual([ - // { id: 1, current_preference: undefined }, - // { - // id: 2, - // current_preference: undefined, - // }, - // { id: 3, current_preference: undefined }, - // ]); - // }); - // - // it("can handle updating preferences", () => { - // const cookie = { - // ...baseCookie, - // tcf_consent: { - // system_consent_preferences: { - // 1111: true, - // ctl_test_system: false, - // }, - // }, - // }; - // const updatedExperience = updateExperienceFromCookieConsentTcf({ - // experience: experienceWithTcf, - // cookie, - // }); - // expect(updatedExperience.tcf_purpose_consents).toEqual([ - // { id: 1, current_preference: undefined }, - // { - // id: 2, - // current_preference: undefined, - // }, - // { id: 3, current_preference: undefined }, - // ]); - // expect(updatedExperience.tcf_system_consents).toEqual([ - // { id: "1111", current_preference: UserConsentPreference.OPT_IN }, - // { - // id: "ctl_test_system", - // current_preference: UserConsentPreference.OPT_OUT, - // }, - // ]); - // // The rest should be undefined - // const keys: Array = [ - // "tcf_purpose_legitimate_interests", - // "tcf_special_purposes", - // "tcf_features", - // "tcf_special_features", - // "tcf_vendor_consents", - // "tcf_vendor_legitimate_interests", - // "tcf_system_legitimate_interests", - // ]; - // keys.forEach((key) => { - // updatedExperience[key]?.forEach((f) => { - // expect(f.current_preference).toEqual(undefined); - // }); - // }); - // }); - // - // it("can handle when cookie has values not in the experience", () => { - // const cookie = { - // ...baseCookie, - // tcf_consent: { - // system_consent_preferences: { - // 1: true, - // 2: false, - // 555: false, - // }, - // }, - // }; - // const updatedExperience = updateExperienceFromCookieConsentTcf({ - // experience: experienceWithTcf, - // cookie, - // }); - // expect(updatedExperience.tcf_system_consents).toEqual([ - // { id: 1, current_preference: UserConsentPreference.OPT_IN }, - // { - // id: 2, - // current_preference: UserConsentPreference.OPT_OUT, - // }, - // { id: 3, current_preference: undefined }, - // ]); - // - // // The rest should be undefined - // const keys: Array = [ - // "tcf_purpose_legitimate_interests", - // "tcf_special_purposes", - // "tcf_features", - // "tcf_special_features", - // "tcf_vendor_consents", - // "tcf_vendor_legitimate_interests", - // "tcf_purpose_consents", - // "tcf_system_legitimate_interests", - // ]; - // keys.forEach((key) => { - // updatedExperience[key]?.forEach((f) => { - // expect(f.current_preference).toEqual(undefined); - // }); - // }); - // }); - // }); }); describe("updateCookieFromNoticePreferences", () => { From a99468e73ad111a937a0270b8b35931facf6fff4 Mon Sep 17 00:00:00 2001 From: eastandwestwind Date: Tue, 5 Dec 2023 17:52:00 +0100 Subject: [PATCH 12/16] clean up unused vars --- clients/fides-js/__tests__/lib/cookie.test.ts | 31 ------------------- 1 file changed, 31 deletions(-) diff --git a/clients/fides-js/__tests__/lib/cookie.test.ts b/clients/fides-js/__tests__/lib/cookie.test.ts index 2ce5aeda54..a9f4d9bd52 100644 --- a/clients/fides-js/__tests__/lib/cookie.test.ts +++ b/clients/fides-js/__tests__/lib/cookie.test.ts @@ -24,13 +24,9 @@ import { UserConsentPreference, } from "../../src/lib/consent-types"; import { - TCFPurposeConsentRecord, - TCFVendorConsentRecord, TcfCookieConsent, - TcfExperienceRecords, TcfSavePreferences, } from "../../src/lib/tcf/types"; -// import {updateExperienceFromCookieConsentTcf} from "../../src/fides-tcf"; // Setup mock date const MOCK_DATE = "2023-01-01T12:00:00.000Z"; @@ -399,33 +395,6 @@ describe("updateExperienceFromCookieConsent", () => { privacy_notices: notices, } as PrivacyExperience; - // TCF test data - const purposeRecords = [ - { id: 1 }, - { id: 2 }, - { id: 3 }, - ] as TCFPurposeConsentRecord[]; - const featureRecords = [ - { id: 4 }, - { id: 5 }, - { id: 6 }, - ] as TCFPurposeConsentRecord[]; - const vendorRecords = [ - { id: "1111" }, - { id: "ctl_test_system" }, - ] as TCFVendorConsentRecord[]; - const experienceWithTcf = { - tcf_purpose_consents: purposeRecords, - tcf_legitimate_interests_consent: purposeRecords, - tcf_special_purposes: purposeRecords, - tcf_features: featureRecords, - tcf_special_features: featureRecords, - tcf_vendor_consents: vendorRecords, - tcf_vendor_legitimate_interests: vendorRecords, - tcf_system_consents: vendorRecords, - tcf_system_legitimate_interests: vendorRecords, - } as unknown as PrivacyExperience; - describe("notices", () => { it("can handle an empty cookie", () => { const cookie = { ...baseCookie, consent: {} }; From f8c80d6d1fc9e9836f7fc728676b3a53bb5b4582 Mon Sep 17 00:00:00 2001 From: eastandwestwind Date: Tue, 5 Dec 2023 18:05:02 +0100 Subject: [PATCH 13/16] address some comments I left on the pr earlier --- clients/fides-js/__tests__/lib/cookie.test.ts | 5 +---- clients/fides-js/src/fides-tcf.ts | 11 ++++++----- clients/fides-js/src/lib/cookie.ts | 4 ++-- clients/fides-js/src/lib/tcf/constants.ts | 4 +++- .../cypress/e2e/consent-banner-tcf.cy.ts | 2 +- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/clients/fides-js/__tests__/lib/cookie.test.ts b/clients/fides-js/__tests__/lib/cookie.test.ts index a9f4d9bd52..05403d1942 100644 --- a/clients/fides-js/__tests__/lib/cookie.test.ts +++ b/clients/fides-js/__tests__/lib/cookie.test.ts @@ -23,10 +23,7 @@ import { SaveConsentPreference, UserConsentPreference, } from "../../src/lib/consent-types"; -import { - TcfCookieConsent, - TcfSavePreferences, -} from "../../src/lib/tcf/types"; +import { TcfCookieConsent, TcfSavePreferences } from "../../src/lib/tcf/types"; // Setup mock date const MOCK_DATE = "2023-01-01T12:00:00.000Z"; diff --git a/clients/fides-js/src/fides-tcf.ts b/clients/fides-js/src/fides-tcf.ts index 4ef2690ec1..70efac84bf 100644 --- a/clients/fides-js/src/fides-tcf.ts +++ b/clients/fides-js/src/fides-tcf.ts @@ -85,7 +85,7 @@ import { TcfModelsRecord, TcfSavePreferences, } from "./lib/tcf/types"; -import { TCF_COOKIE_KEY_MAP, TCF_KEY_MAP } from "./lib/tcf/constants"; +import { FIDES_SYSTEM_COOKIE_KEY_MAP, TCF_KEY_MAP } from "./lib/tcf/constants"; import type { GppFunction } from "./lib/gpp/types"; import { makeStub } from "./lib/tcf/stub"; import { customGetConsentPreferences } from "./services/external/preferences"; @@ -125,7 +125,8 @@ const getInitialPreference = ( }; /** - * Populates TCF entities with items from cookie.tcf_consent and Fides string. + * Populates TCF entities with items from both cookie.tcf_consent and cookie.fides_string. + * We must look at both because they contain non-overlapping info that is required for a complete TCFEntities object. * Returns TCF entities to be assigned to an experience. */ export const buildTcfEntitiesFromCookieAndFidesString = ( @@ -145,8 +146,8 @@ export const buildTcfEntitiesFromCookieAndFidesString = ( tcf_system_legitimate_interests: experience.tcf_system_legitimate_interests, }; - // First update tcfEntities based on the cookie (system objects) - TCF_COOKIE_KEY_MAP.forEach(({ cookieKey, experienceKey }) => { + // First update tcfEntities based on the `cookie.tcf_consent` obj + FIDES_SYSTEM_COOKIE_KEY_MAP.forEach(({ cookieKey, experienceKey }) => { const cookieConsent = cookie.tcf_consent[cookieKey] ?? {}; // @ts-ignore the array map should ensure we will get the right record type tcfEntities[experienceKey] = experience[experienceKey]?.map((item) => { @@ -272,7 +273,7 @@ const updateCookieAndExperience = async ({ // 2. Generate a cookie object from the experience const tcSavePrefs: TcfSavePreferences = {}; - TCF_COOKIE_KEY_MAP.forEach(({ cookieKey, experienceKey }) => { + FIDES_SYSTEM_COOKIE_KEY_MAP.forEach(({ cookieKey, experienceKey }) => { tcSavePrefs[cookieKey] = []; experience[experienceKey]?.forEach((record) => { const preference = getInitialPreference(record); diff --git a/clients/fides-js/src/lib/cookie.ts b/clients/fides-js/src/lib/cookie.ts index 647177a53a..793a610368 100644 --- a/clients/fides-js/src/lib/cookie.ts +++ b/clients/fides-js/src/lib/cookie.ts @@ -19,7 +19,7 @@ import { transformUserPreferenceToBoolean, } from "./consent-utils"; import type { TcfCookieConsent, TcfSavePreferences } from "./tcf/types"; -import { TCF_COOKIE_KEY_MAP } from "./tcf/constants"; +import { FIDES_SYSTEM_COOKIE_KEY_MAP } from "./tcf/constants"; /** * Store the user's consent preferences on the cookie, as key -> boolean pairs, e.g. @@ -316,7 +316,7 @@ export const transformTcfPreferencesToCookieKeys = ( tcfPreferences: TcfSavePreferences ): TcfCookieConsent => { const cookieKeys: TcfCookieConsent = {}; - TCF_COOKIE_KEY_MAP.forEach(({ cookieKey }) => { + FIDES_SYSTEM_COOKIE_KEY_MAP.forEach(({ cookieKey }) => { const preferences = tcfPreferences[cookieKey] ?? []; cookieKeys[cookieKey] = Object.fromEntries( preferences.map((pref) => [ diff --git a/clients/fides-js/src/lib/tcf/constants.ts b/clients/fides-js/src/lib/tcf/constants.ts index f6a44d037e..9cbdadace7 100644 --- a/clients/fides-js/src/lib/tcf/constants.ts +++ b/clients/fides-js/src/lib/tcf/constants.ts @@ -53,7 +53,9 @@ export const TCF_KEY_MAP: { }, ]; -export const TCF_COOKIE_KEY_MAP: { +// These preferences are stored in the cooke on `tcf_consent` instead of `fides_string` because they +// pertain to Fides Systems instead of vendors on the FidesString. +export const FIDES_SYSTEM_COOKIE_KEY_MAP: { cookieKey: TcfModelType; experienceKey: keyof TcfExperienceRecords; }[] = [ 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 27b2d1ed55..e2fe83b33f 100644 --- a/clients/privacy-center/cypress/e2e/consent-banner-tcf.cy.ts +++ b/clients/privacy-center/cypress/e2e/consent-banner-tcf.cy.ts @@ -8,8 +8,8 @@ import { } from "fides-js"; import { TCString } from "@iabtechlabtcf/core"; import { CookieKeyConsent } from "fides-js/src/lib/cookie"; +import { FIDES_SEPARATOR } from "fides-js/src/lib/tcf/constants"; import { API_URL, TCF_VERSION_HASH } from "../support/constants"; -import { FIDES_SEPARATOR } from "~/../fides-js/src/lib/tcf/constants"; import { mockCookie, mockTcfVendorObjects } from "../support/mocks"; import { OVERRIDE, stubConfig } from "../support/stubs"; From 9f33f264e0bc926131f00498280351f3334442f0 Mon Sep 17 00:00:00 2001 From: eastandwestwind Date: Tue, 5 Dec 2023 19:09:48 +0100 Subject: [PATCH 14/16] refactor tcf utils into own file so we can have unit tests for them --- clients/fides-js/__tests__/lib/tcf/utils.ts | 175 ++++++++++++++++++ clients/fides-js/src/fides-tcf.ts | 118 +----------- clients/fides-js/src/lib/tcf/utils.ts | 119 ++++++++++++ .../cypress/e2e/consent-banner-tcf.cy.ts | 1 - 4 files changed, 299 insertions(+), 114 deletions(-) create mode 100644 clients/fides-js/__tests__/lib/tcf/utils.ts create mode 100644 clients/fides-js/src/lib/tcf/utils.ts diff --git a/clients/fides-js/__tests__/lib/tcf/utils.ts b/clients/fides-js/__tests__/lib/tcf/utils.ts new file mode 100644 index 0000000000..0e62a10161 --- /dev/null +++ b/clients/fides-js/__tests__/lib/tcf/utils.ts @@ -0,0 +1,175 @@ +import * as uuid from "uuid"; + +import { CookieAttributes } from "typescript-cookie/dist/types"; +import { makeFidesCookie } from "~/lib/cookie"; +import { + TcfExperienceRecords, + TCFPurposeConsentRecord, + TCFVendorConsentRecord, +} from "~/lib/tcf/types"; +import { updateExperienceFromCookieConsentTcf } from "~/lib/tcf/utils"; +import { PrivacyExperience, UserConsentPreference } from "~/lib/consent-types"; + +// Setup mock date +const MOCK_DATE = "2023-01-01T12:00:00.000Z"; +jest.useFakeTimers().setSystemTime(new Date(MOCK_DATE)); + +// Setup mock uuid +const MOCK_UUID = "fae7e16d-37fd-40ed-b2a8-a020ad90106d"; +jest.mock("uuid"); +const mockUuid = jest.mocked(uuid); +mockUuid.v4.mockReturnValue(MOCK_UUID); + +// Setup mock typescript-cookie +// NOTE: the default module mocking just *doesn't* work for typescript-cookie +// for some mysterious reason (see note in jest.config.js), so we define a +// minimal mock implementation here +const mockGetCookie = jest.fn((): string | undefined => "mockGetCookie return"); +const mockSetCookie = jest.fn( + /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ + (name: string, value: string, attributes: object, encoding: object) => + `mock setCookie return (value=${value})` +); +const mockRemoveCookie = jest.fn( + /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ + (name: string, attributes?: CookieAttributes) => undefined +); +jest.mock("typescript-cookie", () => ({ + getCookie: () => mockGetCookie(), + setCookie: ( + name: string, + value: string, + attributes: object, + encoding: object + ) => mockSetCookie(name, value, attributes, encoding), + removeCookie: (name: string, attributes?: CookieAttributes) => + mockRemoveCookie(name, attributes), +})); + +describe("updateExperienceFromCookieConsentTcf", () => { + const baseCookie = makeFidesCookie(); + + // TCF test data + const purposeRecords = [ + { id: 1 }, + { id: 2 }, + { id: 3 }, + ] as TCFPurposeConsentRecord[]; + const featureRecords = [ + { id: 4 }, + { id: 5 }, + { id: 6 }, + ] as TCFPurposeConsentRecord[]; + const vendorRecords = [ + { id: "1111" }, + { id: "ctl_test_system" }, + ] as TCFVendorConsentRecord[]; + const experienceWithTcf = { + tcf_purpose_consents: purposeRecords, + tcf_legitimate_interests_consent: purposeRecords, + tcf_special_purposes: purposeRecords, + tcf_features: featureRecords, + tcf_special_features: featureRecords, + tcf_vendor_consents: vendorRecords, + tcf_vendor_legitimate_interests: vendorRecords, + tcf_system_consents: vendorRecords, + tcf_system_legitimate_interests: vendorRecords, + } as unknown as PrivacyExperience; + + describe("tcf", () => { + it("can handle an empty tcf cookie", () => { + const updatedExperience = updateExperienceFromCookieConsentTcf({ + experience: experienceWithTcf, + cookie: baseCookie, + }); + expect(updatedExperience.tcf_purpose_consents).toEqual([ + { id: 1, current_preference: undefined }, + { + id: 2, + current_preference: undefined, + }, + { id: 3, current_preference: undefined }, + ]); + }); + + it("can handle updating preferences", () => { + const cookie = { + ...baseCookie, + tcf_consent: { + system_consent_preferences: { + 1111: true, + ctl_test_system: false, + }, + }, + }; + const updatedExperience = updateExperienceFromCookieConsentTcf({ + experience: experienceWithTcf, + cookie, + }); + expect(updatedExperience.tcf_system_consents).toEqual([ + { id: "1111", current_preference: UserConsentPreference.OPT_IN }, + { + id: "ctl_test_system", + current_preference: UserConsentPreference.OPT_OUT, + }, + ]); + // The rest should be undefined + const keys: Array = [ + "tcf_purpose_legitimate_interests", + "tcf_special_purposes", + "tcf_features", + "tcf_special_features", + "tcf_vendor_consents", + "tcf_purpose_consents", + "tcf_vendor_legitimate_interests", + "tcf_system_legitimate_interests", + ]; + keys.forEach((key) => { + updatedExperience[key]?.forEach((f) => { + expect(f.current_preference).toEqual(undefined); + }); + }); + }); + + it("can handle when cookie has values not in the experience", () => { + const cookie = { + ...baseCookie, + tcf_consent: { + system_consent_preferences: { + 1111: false, + 2: false, + 555: false, + }, + }, + }; + const updatedExperience = updateExperienceFromCookieConsentTcf({ + experience: experienceWithTcf, + cookie, + }); + expect(updatedExperience.tcf_system_consents).toEqual([ + { id: "1111", current_preference: UserConsentPreference.OPT_OUT }, + { + id: "ctl_test_system", + current_preference: undefined, + }, + ]); + + // The rest should be undefined + const keys: Array = [ + "tcf_purpose_legitimate_interests", + "tcf_special_purposes", + "tcf_features", + "tcf_special_features", + "tcf_vendor_consents", + "tcf_vendor_legitimate_interests", + "tcf_purpose_consents", + "tcf_system_legitimate_interests", + ]; + keys.forEach((key) => { + updatedExperience[key]?.forEach((f) => { + expect(f.current_preference).toEqual(undefined); + }); + }); + }); + }); +}); diff --git a/clients/fides-js/src/fides-tcf.ts b/clients/fides-js/src/fides-tcf.ts index 70efac84bf..2307226127 100644 --- a/clients/fides-js/src/fides-tcf.ts +++ b/clients/fides-js/src/fides-tcf.ts @@ -52,7 +52,6 @@ import { meta } from "./integrations/meta"; import { shopify } from "./integrations/shopify"; import { - ConsentMechanism, FidesConfig, FidesOptionsOverrides, FidesOverrides, @@ -75,7 +74,6 @@ import { debugLog, FidesCookie, hasSavedTcfPreferences, - transformConsentToFidesUserPreference, transformTcfPreferencesToCookieKeys, transformUserPreferenceToBoolean, } from "./fides"; @@ -89,7 +87,11 @@ import { FIDES_SYSTEM_COOKIE_KEY_MAP, TCF_KEY_MAP } from "./lib/tcf/constants"; import type { GppFunction } from "./lib/gpp/types"; import { makeStub } from "./lib/tcf/stub"; import { customGetConsentPreferences } from "./services/external/preferences"; -import { decodeFidesString, idsFromAcString } from "./lib/tcf/fidesString"; +import { decodeFidesString } from "./lib/tcf/fidesString"; +import { + buildTcfEntitiesFromCookieAndFidesString, + updateExperienceFromCookieConsentTcf, +} from "./lib/tcf/utils"; declare global { interface Window { @@ -124,83 +126,6 @@ const getInitialPreference = ( return tcfObject.default_preference ?? UserConsentPreference.OPT_OUT; }; -/** - * Populates TCF entities with items from both cookie.tcf_consent and cookie.fides_string. - * We must look at both because they contain non-overlapping info that is required for a complete TCFEntities object. - * Returns TCF entities to be assigned to an experience. - */ -export const buildTcfEntitiesFromCookieAndFidesString = ( - experience: PrivacyExperience, - cookie: FidesCookie -) => { - const tcfEntities = { - tcf_purpose_consents: experience.tcf_purpose_consents, - tcf_purpose_legitimate_interests: - experience.tcf_purpose_legitimate_interests, - tcf_special_purposes: experience.tcf_special_purposes, - tcf_features: experience.tcf_features, - tcf_special_features: experience.tcf_special_features, - tcf_vendor_consents: experience.tcf_vendor_consents, - tcf_vendor_legitimate_interests: experience.tcf_vendor_legitimate_interests, - tcf_system_consents: experience.tcf_system_consents, - tcf_system_legitimate_interests: experience.tcf_system_legitimate_interests, - }; - - // First update tcfEntities based on the `cookie.tcf_consent` obj - FIDES_SYSTEM_COOKIE_KEY_MAP.forEach(({ cookieKey, experienceKey }) => { - const cookieConsent = cookie.tcf_consent[cookieKey] ?? {}; - // @ts-ignore the array map should ensure we will get the right record type - tcfEntities[experienceKey] = experience[experienceKey]?.map((item) => { - const preference = Object.hasOwn(cookieConsent, item.id) - ? transformConsentToFidesUserPreference( - Boolean(cookieConsent[item.id]), - ConsentMechanism.OPT_IN - ) - : item.default_preference; - return { ...item, current_preference: preference }; - }); - }); - - // Now update tcfEntities based on the fides string - if (cookie.fides_string) { - const { tc: tcString, ac: acString } = decodeFidesString( - cookie.fides_string - ); - const acStringIds = idsFromAcString(acString); - - // Populate every field from tcModel - const tcModel = TCString.decode(tcString); - TCF_KEY_MAP.forEach(({ experienceKey, tcfModelKey }) => { - const isVendorKey = - tcfModelKey === "vendorConsents" || - tcfModelKey === "vendorLegitimateInterests"; - const tcIds = Array.from(tcModel[tcfModelKey]) - .filter(([, consented]) => consented) - .map(([id]) => (isVendorKey ? `gvl.${id}` : id)); - // @ts-ignore the array map should ensure we will get the right record type - tcfEntities[experienceKey] = experience[experienceKey]?.map((item) => { - let consented = !!tcIds.find((id) => id === item.id); - // Also check the AC string, which only applies to tcf_vendor_consents - if ( - experienceKey === "tcf_vendor_consents" && - acStringIds.find((id) => id === item.id) - ) { - consented = true; - } - return { - ...item, - current_preference: transformConsentToFidesUserPreference( - consented, - ConsentMechanism.OPT_IN - ), - }; - }); - }); - } - - return tcfEntities; -}; - const updateCookieAndExperience = async ({ cookie, experience, @@ -289,39 +214,6 @@ const updateCookieAndExperience = async ({ }; }; -/** - * TCF version of updating prefetched experience, based on: - * 1) experience: pre-fetched or client-side experience-based consent configuration - * 2) cookie: cookie containing user preference. - - * - * Returns updated experience with user preferences. We have a separate function for notices - * and for TCF so that the bundle trees do not overlap. - */ -const updateExperienceFromCookieConsentTcf = ({ - experience, - cookie, - debug, -}: { - experience: PrivacyExperience; - cookie: FidesCookie; - debug?: boolean; -}): PrivacyExperience => { - const tcfEntities = buildTcfEntitiesFromCookieAndFidesString( - experience, - cookie - ); - - if (debug) { - debugLog( - debug, - `Returning updated pre-fetched experience with user consent.`, - experience - ); - } - return { ...experience, ...tcfEntities }; -}; - /** * Initialize the global Fides object with the given configuration values */ diff --git a/clients/fides-js/src/lib/tcf/utils.ts b/clients/fides-js/src/lib/tcf/utils.ts new file mode 100644 index 0000000000..0e5c8e6f0a --- /dev/null +++ b/clients/fides-js/src/lib/tcf/utils.ts @@ -0,0 +1,119 @@ +import { TCString } from "@iabtechlabtcf/core"; +import { ConsentMechanism, PrivacyExperience } from "../consent-types"; +import { FidesCookie } from "../cookie"; +import { + debugLog, + transformConsentToFidesUserPreference, +} from "../consent-utils"; +import { FIDES_SYSTEM_COOKIE_KEY_MAP, TCF_KEY_MAP } from "./constants"; +import { decodeFidesString, idsFromAcString } from "./fidesString"; + +/** + * Populates TCF entities with items from both cookie.tcf_consent and cookie.fides_string. + * We must look at both because they contain non-overlapping info that is required for a complete TCFEntities object. + * Returns TCF entities to be assigned to an experience. + */ +export const buildTcfEntitiesFromCookieAndFidesString = ( + experience: PrivacyExperience, + cookie: FidesCookie +) => { + const tcfEntities = { + tcf_purpose_consents: experience.tcf_purpose_consents, + tcf_purpose_legitimate_interests: + experience.tcf_purpose_legitimate_interests, + tcf_special_purposes: experience.tcf_special_purposes, + tcf_features: experience.tcf_features, + tcf_special_features: experience.tcf_special_features, + tcf_vendor_consents: experience.tcf_vendor_consents, + tcf_vendor_legitimate_interests: experience.tcf_vendor_legitimate_interests, + tcf_system_consents: experience.tcf_system_consents, + tcf_system_legitimate_interests: experience.tcf_system_legitimate_interests, + }; + + // First update tcfEntities based on the `cookie.tcf_consent` obj + FIDES_SYSTEM_COOKIE_KEY_MAP.forEach(({ cookieKey, experienceKey }) => { + const cookieConsent = cookie.tcf_consent[cookieKey] ?? {}; + // @ts-ignore the array map should ensure we will get the right record type + tcfEntities[experienceKey] = experience[experienceKey]?.map((item) => { + const preference = Object.hasOwn(cookieConsent, item.id) + ? transformConsentToFidesUserPreference( + Boolean(cookieConsent[item.id]), + ConsentMechanism.OPT_IN + ) + : item.default_preference; + return { ...item, current_preference: preference }; + }); + }); + + // Now update tcfEntities based on the fides string + if (cookie.fides_string) { + const { tc: tcString, ac: acString } = decodeFidesString( + cookie.fides_string + ); + const acStringIds = idsFromAcString(acString); + + // Populate every field from tcModel + const tcModel = TCString.decode(tcString); + TCF_KEY_MAP.forEach(({ experienceKey, tcfModelKey }) => { + const isVendorKey = + tcfModelKey === "vendorConsents" || + tcfModelKey === "vendorLegitimateInterests"; + const tcIds = Array.from(tcModel[tcfModelKey]) + .filter(([, consented]) => consented) + .map(([id]) => (isVendorKey ? `gvl.${id}` : id)); + // @ts-ignore the array map should ensure we will get the right record type + tcfEntities[experienceKey] = experience[experienceKey]?.map((item) => { + let consented = !!tcIds.find((id) => id === item.id); + // Also check the AC string, which only applies to tcf_vendor_consents + if ( + experienceKey === "tcf_vendor_consents" && + acStringIds.find((id) => id === item.id) + ) { + consented = true; + } + return { + ...item, + current_preference: transformConsentToFidesUserPreference( + consented, + ConsentMechanism.OPT_IN + ), + }; + }); + }); + } + + return tcfEntities; +}; + +/** + * TCF version of updating prefetched experience, based on: + * 1) experience: pre-fetched or client-side experience-based consent configuration + * 2) cookie: cookie containing user preference. + + * + * Returns updated experience with user preferences. We have a separate function for notices + * and for TCF so that the bundle trees do not overlap. + */ +export const updateExperienceFromCookieConsentTcf = ({ + experience, + cookie, + debug, +}: { + experience: PrivacyExperience; + cookie: FidesCookie; + debug?: boolean; +}): PrivacyExperience => { + const tcfEntities = buildTcfEntitiesFromCookieAndFidesString( + experience, + cookie + ); + + if (debug) { + debugLog( + debug, + `Returning updated pre-fetched experience with user consent.`, + experience + ); + } + return { ...experience, ...tcfEntities }; +}; 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 e2fe83b33f..13c7cff3b8 100644 --- a/clients/privacy-center/cypress/e2e/consent-banner-tcf.cy.ts +++ b/clients/privacy-center/cypress/e2e/consent-banner-tcf.cy.ts @@ -2313,7 +2313,6 @@ describe("Fides-js TCF", () => { * ✅ 5) experience API (via GET /privacy-experience) * * EXPECTED RESULT: prefers preferences from local cookie instead of from client-side experience - * TODO: CURRENTLY FAILING!! */ it("prefers preferences from cookie's fides_string when cookie exists and experience is fetched from API", () => { setFidesCookie(); From b331bea6448dace13b2f43f5d38c64944ecc6685 Mon Sep 17 00:00:00 2001 From: eastandwestwind Date: Tue, 5 Dec 2023 19:19:27 +0100 Subject: [PATCH 15/16] lint --- clients/privacy-center/cypress/e2e/consent-banner-tcf.cy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 7b3abd47b0..ab5cbcafad 100644 --- a/clients/privacy-center/cypress/e2e/consent-banner-tcf.cy.ts +++ b/clients/privacy-center/cypress/e2e/consent-banner-tcf.cy.ts @@ -8,12 +8,12 @@ import { } from "fides-js"; import { TCString } from "@iabtechlabtcf/core"; import { CookieKeyConsent } from "fides-js/src/lib/cookie"; +import { FIDES_SEPARATOR } from "fides-js/src/lib/tcf/constants"; import { API_URL, TCF_VERSION_HASH, TEST_OVERRIDE_WINDOW_PATH, } from "../support/constants"; -import { FIDES_SEPARATOR } from "fides-js/src/lib/tcf/constants"; import { mockCookie, mockTcfVendorObjects } from "../support/mocks"; import { OVERRIDE, stubConfig } from "../support/stubs"; From cdf0655100f2382ed7eef3f2be331d097ae85d76 Mon Sep 17 00:00:00 2001 From: eastandwestwind Date: Tue, 5 Dec 2023 19:21:29 +0100 Subject: [PATCH 16/16] add to changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45d34199a3..d1b5624b3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ The types of changes are: ### Changed - Increased max number of preferences allowed in privacy preference API calls [#4469](https://github.com/ethyca/fides/pull/4469) +- Reduce size of tcf_consent payload in fides_consent cookie [#4480](https://github.com/ethyca/fides/pull/4480) ### Fixed - Fix type errors when TCF vendors have no dataDeclaration [#4465](https://github.com/ethyca/fides/pull/4465)