From e40b6aa64dd450c0a2d28a1b67871fcfba2b951e Mon Sep 17 00:00:00 2001 From: Allison King Date: Wed, 22 Nov 2023 14:21:39 -0500 Subject: [PATCH] GPP serving TCF strings (#4433) --- CHANGELOG.md | 1 + clients/fides-js/src/components/Overlay.tsx | 19 ++++--- clients/fides-js/src/extensions/gpp.ts | 57 ++++++++++++++++++++- clients/fides-js/src/lib/events.ts | 4 +- clients/fides-js/src/lib/initialize.ts | 2 +- clients/fides-js/src/lib/tcf.ts | 28 ++-------- clients/fides-js/src/lib/tcf/constants.ts | 3 ++ clients/fides-js/src/lib/tcf/events.ts | 21 ++++++++ 8 files changed, 99 insertions(+), 36 deletions(-) create mode 100644 clients/fides-js/src/lib/tcf/events.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f797b2334..d7720d50f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ The types of changes are: - Stub for initial GPP support [#4431](https://github.com/ethyca/fides/pull/4431) - Added confirmation modal on deleting a data use declaration [#4439](https://github.com/ethyca/fides/pull/4439) - Added feature flag for separating system name and Compass vendor selector [#4437](https://github.com/ethyca/fides/pull/4437) +- 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) 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 ee5cc1279e..757d67483c 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,66 @@ * 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"; +import { fidesEventToTcString } from "../lib/tcf/events"; +import { + isPrivacyExperience, + shouldResurfaceConsent, +} from "../lib/consent-utils"; +import { ETHYCA_CMP_ID } from "../lib/tcf/constants"; + +const CMP_VERSION = 1; + +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(ETHYCA_CMP_ID, CMP_VERSION); + cmpApi.setApplicableSections([TCF_SECTION_ID]); + cmpApi.setCmpStatus(CmpStatus.LOADED); + + // 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", () => { + cmpApi.setSignalStatus(SignalStatus.NOT_READY); + cmpApi.setCmpDisplayStatus(CmpDisplayStatus.VISIBLE); + }); + + 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) => { + 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/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, { 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, diff --git a/clients/fides-js/src/lib/tcf.ts b/clients/fides-js/src/lib/tcf.ts index 2f29c9f4d2..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 { FidesEvent } from "./events"; +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; @@ -149,25 +148,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. @@ -175,7 +155,7 @@ const fidesEventToTcString = (event: FidesEvent) => { 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 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; +};