From d3469435dc195b461a489ca031549dca135f648d Mon Sep 17 00:00:00 2001 From: Allison King Date: Fri, 10 Nov 2023 16:03:36 -0500 Subject: [PATCH 01/14] wip gpp --- clients/fides-js/src/fides-tcf.ts | 7 +- clients/fides-js/src/lib/gpp.ts | 1 + clients/fides-js/src/lib/gpp/stub.ts | 187 ++++++++++++++++++++++++++ clients/fides-js/src/lib/gpp/types.ts | 1 + clients/fides-js/src/lib/tcf.ts | 2 +- 5 files changed, 195 insertions(+), 3 deletions(-) create mode 100644 clients/fides-js/src/lib/gpp.ts create mode 100644 clients/fides-js/src/lib/gpp/stub.ts create mode 100644 clients/fides-js/src/lib/gpp/types.ts diff --git a/clients/fides-js/src/fides-tcf.ts b/clients/fides-js/src/fides-tcf.ts index 005e2a33b3..a9dae3ef1f 100644 --- a/clients/fides-js/src/fides-tcf.ts +++ b/clients/fides-js/src/fides-tcf.ts @@ -58,7 +58,7 @@ import { UserConsentPreference, } from "./lib/consent-types"; -import { generateFidesString, initializeCmpApi } from "./lib/tcf"; +import { generateFidesString, initializeTcfCmpApi } from "./lib/tcf"; import { getInitialCookie, getInitialFides, @@ -89,6 +89,7 @@ import { generateFidesStringFromCookieTcfConsent, transformFidesStringToCookieKeys, } from "./lib/tcf/utils"; +import type { GppFunction } from "./lib/gpp/types"; declare global { interface Window { @@ -104,6 +105,8 @@ declare global { // DEFER (PROD-1243): support a configurable "custom options" path tc_info: OverrideOptions; }; + __gpp?: GppFunction; + __gppLocator?: Window; } } @@ -279,7 +282,7 @@ const init = async (config: FidesConfig) => { } const initialFides = getInitialFides({ ...config, cookie }); // Initialize the CMP API early so that listeners are established - initializeCmpApi(); + initializeTcfCmpApi(); if (initialFides) { Object.assign(_Fides, initialFides); dispatchFidesEvent("FidesInitialized", cookie, config.options.debug); diff --git a/clients/fides-js/src/lib/gpp.ts b/clients/fides-js/src/lib/gpp.ts new file mode 100644 index 0000000000..16aec231cb --- /dev/null +++ b/clients/fides-js/src/lib/gpp.ts @@ -0,0 +1 @@ +export const initializeGppCmpApi = () => {}; diff --git a/clients/fides-js/src/lib/gpp/stub.ts b/clients/fides-js/src/lib/gpp/stub.ts new file mode 100644 index 0000000000..e3f8e8e0ea --- /dev/null +++ b/clients/fides-js/src/lib/gpp/stub.ts @@ -0,0 +1,187 @@ +// Typescript adaptation of https://github.com/IABTechLab/iabgpp-es/blob/master/modules/stub + +/* eslint-disable no-underscore-dangle */ + +const GPP_LOCATOR_NAME = "__gppLocator"; + +export const makeStub = () => { + const addFrame = () => { + if (!window.frames[GPP_LOCATOR_NAME]) { + if (document.body) { + const i = document.createElement("iframe"); + i.style.cssText = "display:none"; + i.name = GPP_LOCATOR_NAME; + document.body.appendChild(i); + } else { + window.setTimeout(addFrame, 10); + } + } + }; + + const gppAPIHandler = () => { + const b = arguments; + __gpp.queue = __gpp.queue || []; + __gpp.events = __gpp.events || []; + + if (!b.length || (b.length == 1 && b[0] == "queue")) { + return __gpp.queue; + } + + if (b.length == 1 && b[0] == "events") { + return __gpp.events; + } + + const cmd = b[0]; + const clb = b.length > 1 ? b[1] : null; + const par = b.length > 2 ? b[2] : null; + if (cmd === "ping") { + clb( + { + gppVersion: "1.1", // must be “Version.Subversion”, current: “1.1” + cmpStatus: "stub", // possible values: stub, loading, loaded, error + cmpDisplayStatus: "hidden", // possible values: hidden, visible, disabled + signalStatus: "not ready", // possible values: not ready, ready + supportedAPIs: [ + "2:tcfeuv2", + "5:tcfcav1", + "6:uspv1", + "7:usnatv1", + "8:uscav1", + "9:usvav1", + "10:uscov1", + "11:usutv1", + "12:usctv1", + ], // list of supported APIs + cmpId: 0, // IAB assigned CMP ID, may be 0 during stub/loading + sectionList: [], + applicableSections: [], + gppString: "", + parsedSections: {}, + }, + true + ); + } else if (cmd === "addEventListener") { + if (!("lastId" in __gpp)) { + __gpp.lastId = 0; + } + __gpp.lastId++; + const lnr = __gpp.lastId; + __gpp.events.push({ + id: lnr, + callback: clb, + parameter: par, + }); + clb( + { + eventName: "listenerRegistered", + listenerId: lnr, // Registered ID of the listener + data: true, // positive signal + pingData: { + gppVersion: "1.1", // must be “Version.Subversion”, current: “1.1” + cmpStatus: "stub", // possible values: stub, loading, loaded, error + cmpDisplayStatus: "hidden", // possible values: hidden, visible, disabled + signalStatus: "not ready", // possible values: not ready, ready + supportedAPIs: [ + "2:tcfeuv2", + "5:tcfcav1", + "6:uspv1", + "7:usnatv1", + "8:uscav1", + "9:usvav1", + "10:uscov1", + "11:usutv1", + "12:usctv1", + ], // list of supported APIs + cmpId: 0, // IAB assigned CMP ID, may be 0 during stub/loading + sectionList: [], + applicableSections: [], + gppString: "", + parsedSections: {}, + }, + }, + true + ); + } else if (cmd === "removeEventListener") { + let success = false; + for (let i = 0; i < __gpp.events.length; i++) { + if (__gpp.events[i].id == par) { + __gpp.events.splice(i, 1); + success = true; + break; + } + } + clb( + { + eventName: "listenerRemoved", + listenerId: par, // Registered ID of the listener + data: success, // status info + pingData: { + gppVersion: "1.1", // must be “Version.Subversion”, current: “1.1” + cmpStatus: "stub", // possible values: stub, loading, loaded, error + cmpDisplayStatus: "hidden", // possible values: hidden, visible, disabled + signalStatus: "not ready", // possible values: not ready, ready + supportedAPIs: [ + "2:tcfeuv2", + "5:tcfcav1", + "6:uspv1", + "7:usnatv1", + "8:uscav1", + "9:usvav1", + "10:uscov1", + "11:usutv1", + "12:usctv1", + ], // list of supported APIs + cmpId: 0, // IAB assigned CMP ID, may be 0 during stub/loading + sectionList: [], + applicableSections: [], + gppString: "", + parsedSections: {}, + }, + }, + true + ); + } else if (cmd === "hasSection") { + clb(false, true); + } else if (cmd === "getSection" || cmd === "getField") { + clb(null, true); + } + // queue all other commands + else { + __gpp.queue.push([].slice.apply(b)); + } + }; + const postMessageEventHandler = (event: MessageEvent) => { + const msgIsString = typeof event.data === "string"; + try { + var json = msgIsString ? JSON.parse(event.data) : event.data; + } catch (e) { + var json = null; + } + if (typeof json === "object" && json !== null && "__gppCall" in json) { + const i = json.__gppCall; + window.__gpp( + i.command, + (retValue, success) => { + const returnMsg = { + __gppReturn: { + returnValue: retValue, + success, + callId: i.callId, + }, + }; + event.source.postMessage( + msgIsString ? JSON.stringify(returnMsg) : returnMsg, + "*" + ); + }, + "parameter" in i ? i.parameter : null, + "version" in i ? i.version : "1.1" + ); + } + }; + if (!("__gpp" in window) || typeof window.__gpp !== "function") { + addFrame(); + window.__gpp = gppAPIHandler; + window.addEventListener("message", postMessageEventHandler, false); + } +}; diff --git a/clients/fides-js/src/lib/gpp/types.ts b/clients/fides-js/src/lib/gpp/types.ts new file mode 100644 index 0000000000..19981da1b0 --- /dev/null +++ b/clients/fides-js/src/lib/gpp/types.ts @@ -0,0 +1 @@ +export type GppFunction = () => void; diff --git a/clients/fides-js/src/lib/tcf.ts b/clients/fides-js/src/lib/tcf.ts index 78c94a7eab..2f29c9f4d2 100644 --- a/clients/fides-js/src/lib/tcf.ts +++ b/clients/fides-js/src/lib/tcf.ts @@ -172,7 +172,7 @@ const fidesEventToTcString = (event: FidesEvent) => { * Initializes the CMP API, including setting up listeners on FidesEvents to update * the CMP API accordingly. */ -export const initializeCmpApi = () => { +export const initializeTcfCmpApi = () => { makeStub(); const isServiceSpecific = true; // TODO: determine this from the backend? const cmpApi = new CmpApi(CMP_ID, CMP_VERSION, isServiceSpecific, { From f733f8c5f24f4b75214ff81927af7247fa25932b Mon Sep 17 00:00:00 2001 From: Allison King Date: Mon, 13 Nov 2023 16:04:36 -0500 Subject: [PATCH 02/14] Refactor stub files --- clients/fides-js/src/lib/cmp-stubs.ts | 51 +++++++++ clients/fides-js/src/lib/gpp/stub.ts | 144 ++++++++++++++++---------- clients/fides-js/src/lib/gpp/types.ts | 7 +- clients/fides-js/src/lib/tcf/stub.ts | 46 +------- 4 files changed, 149 insertions(+), 99 deletions(-) create mode 100644 clients/fides-js/src/lib/cmp-stubs.ts diff --git a/clients/fides-js/src/lib/cmp-stubs.ts b/clients/fides-js/src/lib/cmp-stubs.ts new file mode 100644 index 0000000000..f76edb6de8 --- /dev/null +++ b/clients/fides-js/src/lib/cmp-stubs.ts @@ -0,0 +1,51 @@ +export const TCF_FRAME_NAME = "__tcfapiLocator"; +export const GPP_FRAME_NAME = "__gppLocator"; + +export type IabFrameName = typeof TCF_FRAME_NAME | typeof GPP_FRAME_NAME; + +/** + * If an iframe of `name` doesn't already exist, add it to the DOM. + * If one exists already then we are not the master CMP and will not queue commands. + * */ +export const addFrame = (name: IabFrameName) => { + const otherCmp = !!window.frames[name]; + + if (!otherCmp) { + if (window.document.body) { + const iframe = window.document.createElement("iframe"); + iframe.style.cssText = "display:none"; + iframe.name = name; + window.document.body.appendChild(iframe); + } else { + setTimeout(addFrame, 5); + } + } +}; + +/** + * Iterate up to the top window checking for an already-created + * "__tcfapilLocator" frame on every level. + */ +export const locateFrame = (name: IabFrameName) => { + let frameLocator = window; + let cmpFrame; + while (frameLocator) { + try { + if (frameLocator.frames[name]) { + cmpFrame = frameLocator; + break; + } + } catch (ignore) { + // empty + } + + // if we're at the top and no cmpFrame + if (frameLocator === window.top) { + break; + } + // Move up + // @ts-ignore + frameLocator = frameLocator.parent; + } + return cmpFrame; +}; diff --git a/clients/fides-js/src/lib/gpp/stub.ts b/clients/fides-js/src/lib/gpp/stub.ts index e3f8e8e0ea..20ea27b2f9 100644 --- a/clients/fides-js/src/lib/gpp/stub.ts +++ b/clients/fides-js/src/lib/gpp/stub.ts @@ -1,41 +1,49 @@ -// Typescript adaptation of https://github.com/IABTechLab/iabgpp-es/blob/master/modules/stub +/** + * Typescript adaptation of https://github.com/IABTechLab/iabgpp-es/blob/master/modules/stub + * Refactored to share code with the TCF version + */ + +import { GPP_FRAME_NAME, addFrame, locateFrame } from "../cmp-stubs"; /* eslint-disable no-underscore-dangle */ -const GPP_LOCATOR_NAME = "__gppLocator"; +interface GppEvent { + id: number; + callback: () => void; + parameter: any; +} -export const makeStub = () => { - const addFrame = () => { - if (!window.frames[GPP_LOCATOR_NAME]) { - if (document.body) { - const i = document.createElement("iframe"); - i.style.cssText = "display:none"; - i.name = GPP_LOCATOR_NAME; - document.body.appendChild(i); - } else { - window.setTimeout(addFrame, 10); - } - } +interface MessageData { + __gppCall: { + command: string; + parameter?: string | number; + version: string; + callId: string | number; }; +} - const gppAPIHandler = () => { - const b = arguments; - __gpp.queue = __gpp.queue || []; - __gpp.events = __gpp.events || []; +const isMessageData = (data: unknown): data is MessageData => + typeof data === "object" && data != null && "__gppCall" in data; - if (!b.length || (b.length == 1 && b[0] == "queue")) { - return __gpp.queue; +export const makeStub = () => { + const queue: any[] = []; + const events: GppEvent[] = []; + let lastId: number | undefined; + + const gppAPIHandler = (...args: any[]) => { + if (!args.length || (args.length === 1 && args[0] === "queue")) { + return queue; } - if (b.length == 1 && b[0] == "events") { - return __gpp.events; + if (args.length === 1 && args[0] === "events") { + return events; } - const cmd = b[0]; - const clb = b.length > 1 ? b[1] : null; - const par = b.length > 2 ? b[2] : null; + const cmd = args[0]; + const callback = args.length > 1 ? args[1] : null; + const params = args.length > 2 ? args[2] : null; if (cmd === "ping") { - clb( + callback( { gppVersion: "1.1", // must be “Version.Subversion”, current: “1.1” cmpStatus: "stub", // possible values: stub, loading, loaded, error @@ -61,20 +69,20 @@ export const makeStub = () => { true ); } else if (cmd === "addEventListener") { - if (!("lastId" in __gpp)) { - __gpp.lastId = 0; + if (!lastId) { + lastId = 0; } - __gpp.lastId++; - const lnr = __gpp.lastId; - __gpp.events.push({ - id: lnr, - callback: clb, - parameter: par, + lastId += 1; + const listenerId = lastId; + events.push({ + id: listenerId, + callback, + parameter: params, }); - clb( + callback( { eventName: "listenerRegistered", - listenerId: lnr, // Registered ID of the listener + listenerId, // Registered ID of the listener data: true, // positive signal pingData: { gppVersion: "1.1", // must be “Version.Subversion”, current: “1.1” @@ -103,17 +111,18 @@ export const makeStub = () => { ); } else if (cmd === "removeEventListener") { let success = false; - for (let i = 0; i < __gpp.events.length; i++) { - if (__gpp.events[i].id == par) { - __gpp.events.splice(i, 1); + // eslint-disable-next-line no-plusplus + for (let i = 0; i < events.length; i++) { + if (events[i].id === params) { + events.splice(i, 1); success = true; break; } } - clb( + callback( { eventName: "listenerRemoved", - listenerId: par, // Registered ID of the listener + listenerId: params, // Registered ID of the listener data: success, // status info pingData: { gppVersion: "1.1", // must be “Version.Subversion”, current: “1.1” @@ -141,23 +150,38 @@ export const makeStub = () => { true ); } else if (cmd === "hasSection") { - clb(false, true); + callback(false, true); } else if (cmd === "getSection" || cmd === "getField") { - clb(null, true); + callback(null, true); } // queue all other commands else { - __gpp.queue.push([].slice.apply(b)); + queue.push([].slice.apply(args)); } + return null; }; + const postMessageEventHandler = (event: MessageEvent) => { const msgIsString = typeof event.data === "string"; - try { - var json = msgIsString ? JSON.parse(event.data) : event.data; - } catch (e) { - var json = null; + let json = {}; + + if (msgIsString) { + try { + json = JSON.parse(event.data); + } catch (ignore) { + json = {}; + } + } else { + json = event.data; + } + + if (!isMessageData(json)) { + return null; } - if (typeof json === "object" && json !== null && "__gppCall" in json) { + + const payload = json.__gppCall; + + if (payload && window.__gpp) { const i = json.__gppCall; window.__gpp( i.command, @@ -169,18 +193,26 @@ export const makeStub = () => { callId: i.callId, }, }; - event.source.postMessage( - msgIsString ? JSON.stringify(returnMsg) : returnMsg, - "*" - ); + + if (event && event.source && event.source.postMessage) { + event.source.postMessage( + msgIsString ? JSON.stringify(returnMsg) : returnMsg, + // @ts-ignore + "*" + ); + } }, - "parameter" in i ? i.parameter : null, + "parameter" in i ? i.parameter : undefined, "version" in i ? i.version : "1.1" ); } + return null; }; - if (!("__gpp" in window) || typeof window.__gpp !== "function") { - addFrame(); + + const cmpFrame = locateFrame(GPP_FRAME_NAME); + + if (!cmpFrame) { + addFrame(GPP_FRAME_NAME); window.__gpp = gppAPIHandler; window.addEventListener("message", postMessageEventHandler, false); } diff --git a/clients/fides-js/src/lib/gpp/types.ts b/clients/fides-js/src/lib/gpp/types.ts index 19981da1b0..c5fee049b2 100644 --- a/clients/fides-js/src/lib/gpp/types.ts +++ b/clients/fides-js/src/lib/gpp/types.ts @@ -1 +1,6 @@ -export type GppFunction = () => void; +export type GppFunction = ( + command: string, + callback: (event: any, success: boolean) => void, + parameter?: number | string, + version?: string +) => void; diff --git a/clients/fides-js/src/lib/tcf/stub.ts b/clients/fides-js/src/lib/tcf/stub.ts index 2391486a82..5a12d072ac 100644 --- a/clients/fides-js/src/lib/tcf/stub.ts +++ b/clients/fides-js/src/lib/tcf/stub.ts @@ -1,5 +1,7 @@ // Typescript adaptation of https://github.com/InteractiveAdvertisingBureau/iabtcf-es/tree/master/modules/stub +import { TCF_FRAME_NAME, addFrame, locateFrame } from "../cmp-stubs"; + /* eslint-disable no-underscore-dangle */ interface MessageData { @@ -15,32 +17,10 @@ const isMessageData = (data: unknown): data is MessageData => typeof data === "object" && data != null && "__tcfapiCall" in data; export const makeStub = () => { - const TCF_LOCATOR_NAME = "__tcfapiLocator"; const queue: any[] = []; const currentWindow = window; - let frameLocator = currentWindow; - let cmpFrame; let gdprApplies: boolean; - function addFrame() { - const doc = currentWindow.document; - const otherCMP = !!currentWindow.frames[TCF_LOCATOR_NAME]; - - if (!otherCMP) { - if (doc.body) { - const iframe = doc.createElement("iframe"); - - iframe.style.cssText = "display:none"; - iframe.name = TCF_LOCATOR_NAME; - doc.body.appendChild(iframe); - } else { - setTimeout(addFrame, 5); - } - } - - return !otherCMP; - } - function tcfAPIHandler(...args: any[]) { if (!args.length) { /** @@ -150,29 +130,11 @@ export const makeStub = () => { * "__tcfapilLocator" frame on every level. If one exists already then we are * not the master CMP and will not queue commands. */ - while (frameLocator) { - try { - if (frameLocator.frames[TCF_LOCATOR_NAME]) { - cmpFrame = frameLocator; - break; - } - } catch (ignore) { - /* empty */ - } - - // if we're at the top and no cmpFrame - if (frameLocator === currentWindow.top) { - break; - } - - // Move up - // @ts-ignore - frameLocator = frameLocator.parent; - } + const cmpFrame = locateFrame(TCF_FRAME_NAME); if (!cmpFrame) { // we have recur'd up the windows and have found no __tcfapiLocator frame - addFrame(); + addFrame(TCF_FRAME_NAME); currentWindow.__tcfapi = tcfAPIHandler; currentWindow.addEventListener("message", postMessageEventHandler, false); } From 32e89e8a11949597446393232e09f6869d7e3979 Mon Sep 17 00:00:00 2001 From: Allison King Date: Mon, 13 Nov 2023 16:09:07 -0500 Subject: [PATCH 03/14] Add gpp library and update types --- clients/fides-js/package.json | 1 + clients/fides-js/src/lib/gpp/stub.ts | 2 +- clients/fides-js/src/lib/gpp/types.ts | 4 +++- clients/package-lock.json | 6 ++++++ 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/clients/fides-js/package.json b/clients/fides-js/package.json index 0f71b93e66..0f92f97671 100644 --- a/clients/fides-js/package.json +++ b/clients/fides-js/package.json @@ -26,6 +26,7 @@ "directory": "clients/fides-js" }, "dependencies": { + "@iabgpp/cmpapi": "^3.1.0", "@iabtechlabtcf/cmpapi": "^1.5.8", "@iabtechlabtcf/core": "^1.5.7", "a11y-dialog": "^7.5.2", diff --git a/clients/fides-js/src/lib/gpp/stub.ts b/clients/fides-js/src/lib/gpp/stub.ts index 20ea27b2f9..5ab50c7054 100644 --- a/clients/fides-js/src/lib/gpp/stub.ts +++ b/clients/fides-js/src/lib/gpp/stub.ts @@ -161,7 +161,7 @@ export const makeStub = () => { return null; }; - const postMessageEventHandler = (event: MessageEvent) => { + const postMessageEventHandler = (event: MessageEvent) => { const msgIsString = typeof event.data === "string"; let json = {}; diff --git a/clients/fides-js/src/lib/gpp/types.ts b/clients/fides-js/src/lib/gpp/types.ts index c5fee049b2..6c28ce45cc 100644 --- a/clients/fides-js/src/lib/gpp/types.ts +++ b/clients/fides-js/src/lib/gpp/types.ts @@ -1,6 +1,8 @@ +import type { PingData, EventData } from "@iabgpp/cmpapi"; + export type GppFunction = ( command: string, - callback: (event: any, success: boolean) => void, + callback: (event: PingData | EventData, success: boolean) => void, parameter?: number | string, version?: string ) => void; diff --git a/clients/package-lock.json b/clients/package-lock.json index 05c326a178..730605a897 100644 --- a/clients/package-lock.json +++ b/clients/package-lock.json @@ -98,6 +98,7 @@ "version": "0.0.1", "license": "Apache-2.0", "dependencies": { + "@iabgpp/cmpapi": "^3.1.0", "@iabtechlabtcf/cmpapi": "^1.5.8", "@iabtechlabtcf/core": "^1.5.7", "a11y-dialog": "^7.5.2", @@ -2458,6 +2459,11 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "node_modules/@iabgpp/cmpapi": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@iabgpp/cmpapi/-/cmpapi-3.1.0.tgz", + "integrity": "sha512-k1a+0HmF6uXKuTqcgIwHwdCMKbUxkh9AmqNUj6sr1bpoFQJSHebR9bzjhaioeIIqwzm8LW+fsJE8EPsCG2uVQA==" + }, "node_modules/@iabtechlabtcf/cmpapi": { "version": "1.5.8", "resolved": "https://registry.npmjs.org/@iabtechlabtcf/cmpapi/-/cmpapi-1.5.8.tgz", From 93b9a9896a765be26e5e4d9c1685b1c68ddd0e18 Mon Sep 17 00:00:00 2001 From: Allison King Date: Mon, 13 Nov 2023 16:54:16 -0500 Subject: [PATCH 04/14] Firm up types --- clients/fides-js/src/lib/gpp.ts | 8 +- clients/fides-js/src/lib/gpp/constants.ts | 1 + clients/fides-js/src/lib/gpp/stub.ts | 112 +++++++--------------- clients/fides-js/src/lib/gpp/types.ts | 7 +- 4 files changed, 46 insertions(+), 82 deletions(-) create mode 100644 clients/fides-js/src/lib/gpp/constants.ts diff --git a/clients/fides-js/src/lib/gpp.ts b/clients/fides-js/src/lib/gpp.ts index 16aec231cb..df565f8488 100644 --- a/clients/fides-js/src/lib/gpp.ts +++ b/clients/fides-js/src/lib/gpp.ts @@ -1 +1,7 @@ -export const initializeGppCmpApi = () => {}; +import { makeStub } from "./gpp/stub"; + +export const initializeGppCmpApi = () => { + makeStub(); + + // TODO: instantiate a real (non-stubbed) GPP CMP API and set up listeners +}; diff --git a/clients/fides-js/src/lib/gpp/constants.ts b/clients/fides-js/src/lib/gpp/constants.ts new file mode 100644 index 0000000000..793de32cb3 --- /dev/null +++ b/clients/fides-js/src/lib/gpp/constants.ts @@ -0,0 +1 @@ +export const SUPPORTED_APIS = ["2:tcfeuv2"]; diff --git a/clients/fides-js/src/lib/gpp/stub.ts b/clients/fides-js/src/lib/gpp/stub.ts index 5ab50c7054..e3770ac591 100644 --- a/clients/fides-js/src/lib/gpp/stub.ts +++ b/clients/fides-js/src/lib/gpp/stub.ts @@ -3,13 +3,21 @@ * Refactored to share code with the TCF version */ +import { + CmpDisplayStatus, + CmpStatus, + PingData, + SignalStatus, +} from "@iabgpp/cmpapi"; import { GPP_FRAME_NAME, addFrame, locateFrame } from "../cmp-stubs"; +import { GppCallback, GppFunction } from "./types"; +import { SUPPORTED_APIS } from "./constants"; /* eslint-disable no-underscore-dangle */ interface GppEvent { id: number; - callback: () => void; + callback: GppCallback; parameter: any; } @@ -30,44 +38,30 @@ export const makeStub = () => { const events: GppEvent[] = []; let lastId: number | undefined; - const gppAPIHandler = (...args: any[]) => { - if (!args.length || (args.length === 1 && args[0] === "queue")) { + const gppAPIHandler: GppFunction = (cmd, callback, parameter, version) => { + if (cmd === "queue") { return queue; } - if (args.length === 1 && args[0] === "events") { + if (cmd === "events") { return events; } - const cmd = args[0]; - const callback = args.length > 1 ? args[1] : null; - const params = args.length > 2 ? args[2] : null; + const pingData: PingData = { + gppVersion: "1.1", // must be “Version.Subversion”, current: “1.1” + cmpStatus: CmpStatus.STUB, // possible values: stub, loading, loaded, error + cmpDisplayStatus: CmpDisplayStatus.HIDDEN, // possible values: hidden, visible, disabled + signalStatus: SignalStatus.NOT_READY, // possible values: not ready, ready + supportedAPIs: SUPPORTED_APIS, + cmpId: 0, // IAB assigned CMP ID, may be 0 during stub/loading + sectionList: [], + applicableSections: [], + gppString: "", + parsedSections: {}, + }; + if (cmd === "ping") { - callback( - { - gppVersion: "1.1", // must be “Version.Subversion”, current: “1.1” - cmpStatus: "stub", // possible values: stub, loading, loaded, error - cmpDisplayStatus: "hidden", // possible values: hidden, visible, disabled - signalStatus: "not ready", // possible values: not ready, ready - supportedAPIs: [ - "2:tcfeuv2", - "5:tcfcav1", - "6:uspv1", - "7:usnatv1", - "8:uscav1", - "9:usvav1", - "10:uscov1", - "11:usutv1", - "12:usctv1", - ], // list of supported APIs - cmpId: 0, // IAB assigned CMP ID, may be 0 during stub/loading - sectionList: [], - applicableSections: [], - gppString: "", - parsedSections: {}, - }, - true - ); + callback(pingData, true); } else if (cmd === "addEventListener") { if (!lastId) { lastId = 0; @@ -77,35 +71,14 @@ export const makeStub = () => { events.push({ id: listenerId, callback, - parameter: params, + parameter, }); callback( { eventName: "listenerRegistered", listenerId, // Registered ID of the listener data: true, // positive signal - pingData: { - gppVersion: "1.1", // must be “Version.Subversion”, current: “1.1” - cmpStatus: "stub", // possible values: stub, loading, loaded, error - cmpDisplayStatus: "hidden", // possible values: hidden, visible, disabled - signalStatus: "not ready", // possible values: not ready, ready - supportedAPIs: [ - "2:tcfeuv2", - "5:tcfcav1", - "6:uspv1", - "7:usnatv1", - "8:uscav1", - "9:usvav1", - "10:uscov1", - "11:usutv1", - "12:usctv1", - ], // list of supported APIs - cmpId: 0, // IAB assigned CMP ID, may be 0 during stub/loading - sectionList: [], - applicableSections: [], - gppString: "", - parsedSections: {}, - }, + pingData, }, true ); @@ -113,7 +86,7 @@ export const makeStub = () => { let success = false; // eslint-disable-next-line no-plusplus for (let i = 0; i < events.length; i++) { - if (events[i].id === params) { + if (events[i].id === parameter) { events.splice(i, 1); success = true; break; @@ -122,30 +95,9 @@ export const makeStub = () => { callback( { eventName: "listenerRemoved", - listenerId: params, // Registered ID of the listener + listenerId: parameter as number, // Registered ID of the listener data: success, // status info - pingData: { - gppVersion: "1.1", // must be “Version.Subversion”, current: “1.1” - cmpStatus: "stub", // possible values: stub, loading, loaded, error - cmpDisplayStatus: "hidden", // possible values: hidden, visible, disabled - signalStatus: "not ready", // possible values: not ready, ready - supportedAPIs: [ - "2:tcfeuv2", - "5:tcfcav1", - "6:uspv1", - "7:usnatv1", - "8:uscav1", - "9:usvav1", - "10:uscov1", - "11:usutv1", - "12:usctv1", - ], // list of supported APIs - cmpId: 0, // IAB assigned CMP ID, may be 0 during stub/loading - sectionList: [], - applicableSections: [], - gppString: "", - parsedSections: {}, - }, + pingData, }, true ); @@ -156,7 +108,7 @@ export const makeStub = () => { } // queue all other commands else { - queue.push([].slice.apply(args)); + queue.push([].slice.apply([cmd, callback, parameter, version])); } return null; }; diff --git a/clients/fides-js/src/lib/gpp/types.ts b/clients/fides-js/src/lib/gpp/types.ts index 6c28ce45cc..8400b445dd 100644 --- a/clients/fides-js/src/lib/gpp/types.ts +++ b/clients/fides-js/src/lib/gpp/types.ts @@ -1,8 +1,13 @@ import type { PingData, EventData } from "@iabgpp/cmpapi"; +export type GppCallback = ( + event: PingData | EventData | boolean | null, + success: boolean +) => void; + export type GppFunction = ( command: string, - callback: (event: PingData | EventData, success: boolean) => void, + callback: GppCallback, parameter?: number | string, version?: string ) => void; From c92520b76a67e7c79ebc73141174f3d672e2ae13 Mon Sep 17 00:00:00 2001 From: Allison King Date: Tue, 14 Nov 2023 13:48:03 -0500 Subject: [PATCH 05/14] Move files around --- clients/fides-js/src/extensions/gpp.ts | 16 ++++++++++++++++ clients/fides-js/src/lib/gpp.ts | 7 ------- 2 files changed, 16 insertions(+), 7 deletions(-) create mode 100644 clients/fides-js/src/extensions/gpp.ts delete mode 100644 clients/fides-js/src/lib/gpp.ts diff --git a/clients/fides-js/src/extensions/gpp.ts b/clients/fides-js/src/extensions/gpp.ts new file mode 100644 index 0000000000..ee5cc1279e --- /dev/null +++ b/clients/fides-js/src/extensions/gpp.ts @@ -0,0 +1,16 @@ +/** + * Extension for GPP + * + * Usage: + * Include as a script tag as early as possible (even before fides.js) + */ + +import { makeStub } from "../lib/gpp/stub"; + +export const initializeGppCmpApi = () => { + makeStub(); + + // TODO: instantiate a real (non-stubbed) GPP CMP API and set up listeners +}; + +initializeGppCmpApi(); diff --git a/clients/fides-js/src/lib/gpp.ts b/clients/fides-js/src/lib/gpp.ts deleted file mode 100644 index df565f8488..0000000000 --- a/clients/fides-js/src/lib/gpp.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { makeStub } from "./gpp/stub"; - -export const initializeGppCmpApi = () => { - makeStub(); - - // TODO: instantiate a real (non-stubbed) GPP CMP API and set up listeners -}; From bdfcb04dfca54a61e35ad56040e8a514672daf1e Mon Sep 17 00:00:00 2001 From: Allison King Date: Tue, 14 Nov 2023 14:23:07 -0500 Subject: [PATCH 06/14] Add gpp stub unit tests --- clients/fides-js/__tests__/lib/gpp/stub.ts | 99 ++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 clients/fides-js/__tests__/lib/gpp/stub.ts diff --git a/clients/fides-js/__tests__/lib/gpp/stub.ts b/clients/fides-js/__tests__/lib/gpp/stub.ts new file mode 100644 index 0000000000..779ef34b6a --- /dev/null +++ b/clients/fides-js/__tests__/lib/gpp/stub.ts @@ -0,0 +1,99 @@ +/* eslint-disable no-underscore-dangle */ +import { EventData, PingData } from "@iabgpp/cmpapi"; +import { makeStub } from "../../../src/lib/gpp/stub"; +import { GppCallback } from "../../../src/lib/gpp/types"; + +const EXPECTED_PING_DATA = { + gppVersion: "1.1", + cmpStatus: "stub", + cmpDisplayStatus: "hidden", + signalStatus: "not ready", + supportedAPIs: ["2:tcfeuv2"], + cmpId: 0, + sectionList: [], + applicableSections: [], + gppString: "", + parsedSections: {}, +}; + +describe("makeStub", () => { + it("can make stub functions", () => { + makeStub(); + expect(window.__gpp).toBeTruthy(); + const gppCall = window.__gpp!; + + // Check 'queue' and 'events' + const queue = gppCall("queue", jest.fn()); + const events = gppCall("events", jest.fn()); + expect(queue).toEqual([]); + expect(events).toEqual([]); + + // Check 'ping' + gppCall("ping", (data) => { + const pingData = data as PingData; + expect(pingData).toEqual(EXPECTED_PING_DATA); + }); + + // Check 'addEventListener' and that it updates the `events` obj + const expectedListenerId = 1; + const addEventListener: GppCallback = (evt, success) => { + expect(success).toBe(true); + const eventData = evt as EventData; + expect(eventData.listenerId).toEqual(expectedListenerId); + expect(eventData.eventName).toEqual("listenerRegistered"); + expect(eventData.data).toBe(true); + expect(eventData.pingData).toEqual(EXPECTED_PING_DATA); + }; + gppCall("addEventListener", addEventListener); + const updatedEvents = gppCall("events", jest.fn()); + expect(updatedEvents).toEqual([ + { + id: expectedListenerId, + callback: addEventListener, + parameter: undefined, + }, + ]); + + // Check 'removeEventListener' and that it updates the `events` obj + const removeEventListener: GppCallback = (evt, success) => { + expect(success).toBe(true); + const eventData = evt as EventData; + expect(eventData.listenerId).toEqual(expectedListenerId); + expect(eventData.eventName).toEqual("listenerRemoved"); + expect(eventData.data).toBe(true); + expect(eventData.pingData).toEqual(EXPECTED_PING_DATA); + }; + gppCall("removeEventListener", removeEventListener, expectedListenerId); + expect(gppCall("events", jest.fn())).toEqual([]); + + // hasSection + gppCall( + "hasSection", + (data, success) => { + expect(success).toBe(true); + expect(data).toBe(false); + }, + "tcfcav1" + ); + + // getSection + gppCall( + "getField", + (data, success) => { + expect(success).toBe(true); + expect(data).toBe(null); + }, + "tcfcav1" + ); + + // getField + gppCall( + "getField", + (data, success) => { + expect(success).toBe(true); + expect(data).toBe(null); + }, + "tcfcav1.LastUpdated" + ); + }); +}); From 34605e7cf65c3512ea159d15ccb60f969b3adc40 Mon Sep 17 00:00:00 2001 From: Allison King Date: Tue, 14 Nov 2023 16:04:39 -0500 Subject: [PATCH 07/14] Subscribe GPP to TCF calls --- clients/fides-js/src/extensions/gpp.ts | 37 +++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/clients/fides-js/src/extensions/gpp.ts b/clients/fides-js/src/extensions/gpp.ts index ee5cc1279e..4483aee16c 100644 --- a/clients/fides-js/src/extensions/gpp.ts +++ b/clients/fides-js/src/extensions/gpp.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-underscore-dangle */ /** * Extension for GPP * @@ -5,12 +6,46 @@ * Include as a script tag as early as possible (even before fides.js) */ +import { + CmpApi, + CmpDisplayStatus, + CmpStatus, + SignalStatus, +} from "@iabgpp/cmpapi"; import { makeStub } from "../lib/gpp/stub"; +const CMP_ID = 407; // TODO: is this supposed to be the same as TCF, or is this separate? +const CMP_VERSION = 1; +const TCF_VERSION = 2; + +const TCF_SECTION_ID = 2; + export const initializeGppCmpApi = () => { makeStub(); - // TODO: instantiate a real (non-stubbed) GPP CMP API and set up listeners + const cmpApi = new CmpApi(CMP_ID, CMP_VERSION); + cmpApi.setApplicableSections([TCF_SECTION_ID]); + cmpApi.setCmpStatus(CmpStatus.LOADED); + cmpApi.setSignalStatus(SignalStatus.READY); + + if (window.__tcfapi) { + window.__tcfapi("addEventListener", TCF_VERSION, (tcData, success) => { + if (!success) { + return; + } + if (tcData.eventStatus === "cmpuishown") { + cmpApi.setCmpDisplayStatus(CmpDisplayStatus.VISIBLE); + } + if (tcData.eventStatus === "useractioncomplete") { + cmpApi.setCmpDisplayStatus(CmpDisplayStatus.HIDDEN); + } + // Workaround for bug in base library https://github.com/IABTechLab/iabgpp-es/issues/35 + cmpApi.setFieldValueBySectionId(TCF_SECTION_ID, "CmpId", CMP_ID); + const { tcString } = tcData; + cmpApi.setSectionStringById(TCF_SECTION_ID, tcString); + cmpApi.fireSectionChange("tcfeuv2"); + }); + } }; initializeGppCmpApi(); From 0af833569de77e38a49534a583835522fad0d20d Mon Sep 17 00:00:00 2001 From: Allison King Date: Tue, 14 Nov 2023 16:56:21 -0500 Subject: [PATCH 08/14] Add success check to ping test --- clients/fides-js/__tests__/lib/gpp/stub.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/clients/fides-js/__tests__/lib/gpp/stub.ts b/clients/fides-js/__tests__/lib/gpp/stub.ts index 779ef34b6a..ac871143ba 100644 --- a/clients/fides-js/__tests__/lib/gpp/stub.ts +++ b/clients/fides-js/__tests__/lib/gpp/stub.ts @@ -29,7 +29,8 @@ describe("makeStub", () => { expect(events).toEqual([]); // Check 'ping' - gppCall("ping", (data) => { + gppCall("ping", (data, success) => { + expect(success).toBe(true); const pingData = data as PingData; expect(pingData).toEqual(EXPECTED_PING_DATA); }); From 79646f34244f10a1d9f6ba379dbc9c0e27c113b0 Mon Sep 17 00:00:00 2001 From: Allison King Date: Tue, 14 Nov 2023 16:57:09 -0500 Subject: [PATCH 09/14] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c7ea8b7574..81023c4506 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,9 @@ The types of changes are: ## [Unreleased](https://github.com/ethyca/fides/compare/2.24.0...main) +### Added +- Stub for initial GPP support [#4431](https://github.com/ethyca/fides/pull/4431) + ## [2.24.0](https://github.com/ethyca/fides/compare/2.23.3...2.24.0) From c1463dcf31dfb3128899b02c0e50d264680e6555 Mon Sep 17 00:00:00 2001 From: Allison King Date: Wed, 15 Nov 2023 17:38:33 -0500 Subject: [PATCH 10/14] Set up GPP listeners from FidesUI events instead of TCF --- clients/fides-js/src/extensions/gpp.ts | 47 ++++++++++++++------------ clients/fides-js/src/lib/tcf.ts | 21 +----------- clients/fides-js/src/lib/tcf/events.ts | 21 ++++++++++++ 3 files changed, 48 insertions(+), 41 deletions(-) create mode 100644 clients/fides-js/src/lib/tcf/events.ts diff --git a/clients/fides-js/src/extensions/gpp.ts b/clients/fides-js/src/extensions/gpp.ts index 4483aee16c..a8d7352a7c 100644 --- a/clients/fides-js/src/extensions/gpp.ts +++ b/clients/fides-js/src/extensions/gpp.ts @@ -13,10 +13,10 @@ import { SignalStatus, } from "@iabgpp/cmpapi"; import { makeStub } from "../lib/gpp/stub"; +import { fidesEventToTcString } from "../lib/tcf/events"; const CMP_ID = 407; // TODO: is this supposed to be the same as TCF, or is this separate? const CMP_VERSION = 1; -const TCF_VERSION = 2; const TCF_SECTION_ID = 2; @@ -26,26 +26,31 @@ export const initializeGppCmpApi = () => { const cmpApi = new CmpApi(CMP_ID, CMP_VERSION); cmpApi.setApplicableSections([TCF_SECTION_ID]); cmpApi.setCmpStatus(CmpStatus.LOADED); - cmpApi.setSignalStatus(SignalStatus.READY); - - if (window.__tcfapi) { - window.__tcfapi("addEventListener", TCF_VERSION, (tcData, success) => { - if (!success) { - return; - } - if (tcData.eventStatus === "cmpuishown") { - cmpApi.setCmpDisplayStatus(CmpDisplayStatus.VISIBLE); - } - if (tcData.eventStatus === "useractioncomplete") { - cmpApi.setCmpDisplayStatus(CmpDisplayStatus.HIDDEN); - } - // Workaround for bug in base library https://github.com/IABTechLab/iabgpp-es/issues/35 - cmpApi.setFieldValueBySectionId(TCF_SECTION_ID, "CmpId", CMP_ID); - const { tcString } = tcData; - cmpApi.setSectionStringById(TCF_SECTION_ID, tcString); - cmpApi.fireSectionChange("tcfeuv2"); - }); - } + + window.addEventListener("FidesUIInitialized", () => { + // TODO: if we don't need to resurface consent, we can set + // cmpApi.setSignalStatus(SignalStatus.READY); + // But we don't have easy access to tell if we should resurface consent here yet + // (we need the experience too) + }); + + window.addEventListener("FidesUIShown", () => { + cmpApi.setSignalStatus(SignalStatus.NOT_READY); + cmpApi.setCmpDisplayStatus(CmpDisplayStatus.VISIBLE); + }); + + window.addEventListener("FidesModalClosed", () => { + cmpApi.setCmpDisplayStatus(CmpDisplayStatus.HIDDEN); + }); + + window.addEventListener("FidesUpdated", (event) => { + const tcString = fidesEventToTcString(event); + // Workaround for bug in base library https://github.com/IABTechLab/iabgpp-es/issues/35 + cmpApi.setFieldValueBySectionId(TCF_SECTION_ID, "CmpId", CMP_ID); + cmpApi.setSectionStringById(TCF_SECTION_ID, tcString ?? ""); + cmpApi.fireSectionChange("tcfeuv2"); + cmpApi.setSignalStatus(SignalStatus.READY); + }); }; initializeGppCmpApi(); diff --git a/clients/fides-js/src/lib/tcf.ts b/clients/fides-js/src/lib/tcf.ts index 2f29c9f4d2..68dd8973d0 100644 --- a/clients/fides-js/src/lib/tcf.ts +++ b/clients/fides-js/src/lib/tcf.ts @@ -18,7 +18,7 @@ import { } from "./tcf/vendors"; import { PrivacyExperience } from "./consent-types"; import { FIDES_SEPARATOR } from "./tcf/constants"; -import { FidesEvent } from "./events"; +import { fidesEventToTcString } from "./tcf/events"; // TCF const CMP_ID = 407; @@ -149,25 +149,6 @@ export const generateFidesString = async ({ return Promise.resolve(encodedString); }; -/** - * Extract just the TC string from a FidesEvent. This will also remove parts of the - * TC string that we do not want to surface with our CMP API events, such as - * `vendors_disclosed` and our own AC string addition. - */ -const fidesEventToTcString = (event: FidesEvent) => { - const { fides_string: cookieString } = event.detail; - if (cookieString) { - // Remove the AC portion which is separated by FIDES_SEPARATOR - const [tcString] = cookieString.split(FIDES_SEPARATOR); - // We only want to return the first part of the tcString, which is separated by '.' - // This means Publisher TC is not sent either, which is okay for now since we do not set it. - // However, if we do one day set it, we would have to decode the string and encode it again - // without vendorsDisclosed - return tcString.split(".")[0]; - } - return cookieString; -}; - /** * Initializes the CMP API, including setting up listeners on FidesEvents to update * the CMP API accordingly. diff --git a/clients/fides-js/src/lib/tcf/events.ts b/clients/fides-js/src/lib/tcf/events.ts new file mode 100644 index 0000000000..9f16b584fd --- /dev/null +++ b/clients/fides-js/src/lib/tcf/events.ts @@ -0,0 +1,21 @@ +import { FidesEvent } from "../events"; +import { FIDES_SEPARATOR } from "./constants"; + +/** + * Extract just the TC string from a FidesEvent. This will also remove parts of the + * TC string that we do not want to surface with our CMP API events, such as + * `vendors_disclosed` and our own AC string addition. + */ +export const fidesEventToTcString = (event: FidesEvent) => { + const { fides_string: cookieString } = event.detail; + if (cookieString) { + // Remove the AC portion which is separated by FIDES_SEPARATOR + const [tcString] = cookieString.split(FIDES_SEPARATOR); + // We only want to return the first part of the tcString, which is separated by '.' + // This means Publisher TC is not sent either, which is okay for now since we do not set it. + // However, if we do one day set it, we would have to decode the string and encode it again + // without vendorsDisclosed + return tcString.split(".")[0]; + } + return cookieString; +}; From 94ebc9995b20ef888a4a64fd6353ae294f2f9d4b Mon Sep 17 00:00:00 2001 From: Allison King Date: Fri, 17 Nov 2023 13:15:02 -0500 Subject: [PATCH 11/14] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e93b5e6971..7a099ef26a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ The types of changes are: ### Added - Stub for initial GPP support [#4431](https://github.com/ethyca/fides/pull/4431) +- Fire GPP events per spec [#4433](https://github.com/ethyca/fides/pull/4433) ### Changed - Improved bulk vendor adding table UX [#4425](https://github.com/ethyca/fides/pull/4425) From 9af64df87faf0f7d8444a263b563ee2dba669bb3 Mon Sep 17 00:00:00 2001 From: Allison King Date: Fri, 17 Nov 2023 15:03:00 -0500 Subject: [PATCH 12/14] Check for resurface for initial signal status --- clients/fides-js/src/extensions/gpp.ts | 18 +++++++++++++----- clients/fides-js/src/lib/initialize.ts | 2 +- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/clients/fides-js/src/extensions/gpp.ts b/clients/fides-js/src/extensions/gpp.ts index a8d7352a7c..4aaa28e52a 100644 --- a/clients/fides-js/src/extensions/gpp.ts +++ b/clients/fides-js/src/extensions/gpp.ts @@ -14,6 +14,10 @@ import { } from "@iabgpp/cmpapi"; import { makeStub } from "../lib/gpp/stub"; import { fidesEventToTcString } from "../lib/tcf/events"; +import { + isPrivacyExperience, + shouldResurfaceConsent, +} from "../lib/consent-utils"; const CMP_ID = 407; // TODO: is this supposed to be the same as TCF, or is this separate? const CMP_VERSION = 1; @@ -27,11 +31,15 @@ export const initializeGppCmpApi = () => { cmpApi.setApplicableSections([TCF_SECTION_ID]); cmpApi.setCmpStatus(CmpStatus.LOADED); - window.addEventListener("FidesUIInitialized", () => { - // TODO: if we don't need to resurface consent, we can set - // cmpApi.setSignalStatus(SignalStatus.READY); - // But we don't have easy access to tell if we should resurface consent here yet - // (we need the experience too) + // If consent does not need to be resurfaced, then we can set the signal to Ready here + window.addEventListener("FidesInitialized", (event) => { + const { experience } = window.Fides; + if ( + isPrivacyExperience(experience) && + !shouldResurfaceConsent(experience, event.detail) + ) { + cmpApi.setSignalStatus(SignalStatus.READY); + } }); window.addEventListener("FidesUIShown", () => { diff --git a/clients/fides-js/src/lib/initialize.ts b/clients/fides-js/src/lib/initialize.ts index 07ba140e3d..52e0adf629 100644 --- a/clients/fides-js/src/lib/initialize.ts +++ b/clients/fides-js/src/lib/initialize.ts @@ -368,7 +368,7 @@ export const initialize = async ({ identity: cookie.identity, fides_string: cookie.fides_string, tcf_consent: cookie.tcf_consent, - experience, + experience: effectiveExperience, geolocation, options, initialized: true, From 31ae240cd679ed654da2ed395a5fc1637ab877e5 Mon Sep 17 00:00:00 2001 From: Allison King Date: Fri, 17 Nov 2023 16:47:05 -0500 Subject: [PATCH 13/14] Add if the user saved to FidesModalClosed event --- clients/fides-js/src/components/Overlay.tsx | 19 +++++++++++-------- clients/fides-js/src/extensions/gpp.ts | 9 ++++++++- clients/fides-js/src/lib/events.ts | 4 ++-- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/clients/fides-js/src/components/Overlay.tsx b/clients/fides-js/src/components/Overlay.tsx index 3f237fe50f..624d10fb5b 100644 --- a/clients/fides-js/src/components/Overlay.tsx +++ b/clients/fides-js/src/components/Overlay.tsx @@ -49,15 +49,18 @@ const Overlay: FunctionComponent = ({ const hasMounted = useHasMounted(); const [bannerIsOpen, setBannerIsOpen] = useState(false); - const dispatchCloseEvent = useCallback(() => { - dispatchFidesEvent("FidesModalClosed", cookie, options.debug); - }, [cookie, options.debug]); + const dispatchCloseEvent = useCallback( + ({ saved = false }: { saved?: boolean }) => { + dispatchFidesEvent("FidesModalClosed", cookie, options.debug, { saved }); + }, + [cookie, options.debug] + ); const { instance, attributes } = useA11yDialog({ id: "fides-modal", role: "alertdialog", title: experience?.experience_config?.title || "", - onClose: dispatchCloseEvent, + onClose: () => dispatchCloseEvent({ saved: false }), }); const handleOpenModal = useCallback(() => { @@ -67,10 +70,10 @@ const Overlay: FunctionComponent = ({ } }, [instance, onOpen]); - const handleCloseModal = useCallback(() => { + const handleCloseModalAfterSave = useCallback(() => { if (instance && !options.fidesEmbed) { instance.hide(); - dispatchCloseEvent(); + dispatchCloseEvent({ saved: true }); } }, [instance, dispatchCloseEvent, options.fidesEmbed]); @@ -156,7 +159,7 @@ const Overlay: FunctionComponent = ({ experience={experience.experience_config} renderModalFooter={() => renderModalFooter({ - onClose: handleCloseModal, + onClose: handleCloseModalAfterSave, isMobile: false, }) } @@ -170,7 +173,7 @@ const Overlay: FunctionComponent = ({ onVendorPageClick={onVendorPageClick} renderModalFooter={() => renderModalFooter({ - onClose: handleCloseModal, + onClose: handleCloseModalAfterSave, isMobile: false, }) } diff --git a/clients/fides-js/src/extensions/gpp.ts b/clients/fides-js/src/extensions/gpp.ts index 4aaa28e52a..7747b00ceb 100644 --- a/clients/fides-js/src/extensions/gpp.ts +++ b/clients/fides-js/src/extensions/gpp.ts @@ -47,8 +47,15 @@ export const initializeGppCmpApi = () => { cmpApi.setCmpDisplayStatus(CmpDisplayStatus.VISIBLE); }); - window.addEventListener("FidesModalClosed", () => { + window.addEventListener("FidesModalClosed", (event) => { cmpApi.setCmpDisplayStatus(CmpDisplayStatus.HIDDEN); + // If the modal was closed without the user saving, set signal status back to Ready + if ( + event.detail.extraDetails && + event.detail.extraDetails.saved === false + ) { + cmpApi.setSignalStatus(SignalStatus.READY); + } }); window.addEventListener("FidesUpdated", (event) => { diff --git a/clients/fides-js/src/lib/events.ts b/clients/fides-js/src/lib/events.ts index d0ff6b4512..b46f593bb0 100644 --- a/clients/fides-js/src/lib/events.ts +++ b/clients/fides-js/src/lib/events.ts @@ -34,7 +34,7 @@ declare global { */ export type FidesEventDetail = FidesCookie & { debug?: boolean; - extraDetails?: Record; + extraDetails?: Record; }; export type FidesEvent = CustomEvent; @@ -59,7 +59,7 @@ export const dispatchFidesEvent = ( type: FidesEventType, cookie: FidesCookie, debug: boolean, - extraDetails?: Record + extraDetails?: Record ) => { if (typeof window !== "undefined" && typeof CustomEvent !== "undefined") { const event = new CustomEvent(type, { From 6588867920a58b8ff178f8d1716c997846213f69 Mon Sep 17 00:00:00 2001 From: Allison King Date: Wed, 22 Nov 2023 11:40:26 -0500 Subject: [PATCH 14/14] Extract CMP ID to a constant shared by TCF and GPP --- clients/fides-js/src/extensions/gpp.ts | 4 ++-- clients/fides-js/src/lib/tcf.ts | 7 +++---- clients/fides-js/src/lib/tcf/constants.ts | 3 +++ 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/clients/fides-js/src/extensions/gpp.ts b/clients/fides-js/src/extensions/gpp.ts index 7747b00ceb..757d67483c 100644 --- a/clients/fides-js/src/extensions/gpp.ts +++ b/clients/fides-js/src/extensions/gpp.ts @@ -18,8 +18,8 @@ import { isPrivacyExperience, shouldResurfaceConsent, } from "../lib/consent-utils"; +import { ETHYCA_CMP_ID } from "../lib/tcf/constants"; -const CMP_ID = 407; // TODO: is this supposed to be the same as TCF, or is this separate? const CMP_VERSION = 1; const TCF_SECTION_ID = 2; @@ -27,7 +27,7 @@ const TCF_SECTION_ID = 2; export const initializeGppCmpApi = () => { makeStub(); - const cmpApi = new CmpApi(CMP_ID, CMP_VERSION); + const cmpApi = new CmpApi(ETHYCA_CMP_ID, CMP_VERSION); cmpApi.setApplicableSections([TCF_SECTION_ID]); cmpApi.setCmpStatus(CmpStatus.LOADED); diff --git a/clients/fides-js/src/lib/tcf.ts b/clients/fides-js/src/lib/tcf.ts index 68dd8973d0..ebae0bf0b5 100644 --- a/clients/fides-js/src/lib/tcf.ts +++ b/clients/fides-js/src/lib/tcf.ts @@ -17,11 +17,10 @@ import { uniqueGvlVendorIds, } from "./tcf/vendors"; import { PrivacyExperience } from "./consent-types"; -import { FIDES_SEPARATOR } from "./tcf/constants"; +import { ETHYCA_CMP_ID, FIDES_SEPARATOR } from "./tcf/constants"; import { fidesEventToTcString } from "./tcf/events"; // TCF -const CMP_ID = 407; const CMP_VERSION = 1; const FORBIDDEN_LEGITIMATE_INTEREST_PURPOSE_IDS = [1, 3, 4, 5, 6]; @@ -70,7 +69,7 @@ export const generateFidesString = async ({ // Some fields will not be populated until a GVL is loaded await tcModel.gvl.readyPromise; - tcModel.cmpId = CMP_ID; + tcModel.cmpId = ETHYCA_CMP_ID; tcModel.cmpVersion = CMP_VERSION; tcModel.consentScreen = 1; // todo- On which 'screen' consent was captured; this is a CMP proprietary number encoded into the TC string tcModel.isServiceSpecific = true; @@ -156,7 +155,7 @@ export const generateFidesString = async ({ export const initializeTcfCmpApi = () => { makeStub(); const isServiceSpecific = true; // TODO: determine this from the backend? - const cmpApi = new CmpApi(CMP_ID, CMP_VERSION, isServiceSpecific, { + const cmpApi = new CmpApi(ETHYCA_CMP_ID, CMP_VERSION, isServiceSpecific, { // Add custom command to support adding `addtlConsent` per AC spec getTCData: (next, tcData: TCData, status) => { /* diff --git a/clients/fides-js/src/lib/tcf/constants.ts b/clients/fides-js/src/lib/tcf/constants.ts index 3c8b63a905..6c77f6a56f 100644 --- a/clients/fides-js/src/lib/tcf/constants.ts +++ b/clients/fides-js/src/lib/tcf/constants.ts @@ -6,6 +6,9 @@ import { TcfModelType, } from "./types"; +/** CMP ID assigned to us by the IAB */ +export const ETHYCA_CMP_ID = 407; + /** * We store all of our preference strings (TC, AC, etc.) together as one string so that * we can have a single-source-of-truth for offline storage & syncing. The code responsible