Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GPP serving TCF strings #4433

Merged
merged 19 commits into from
Nov 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
19 changes: 11 additions & 8 deletions clients/fides-js/src/components/Overlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,18 @@ const Overlay: FunctionComponent<Props> = ({
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 });
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we need to know if the modal was closed without saving. gpp signals that it is not ready when the modal opens. once it saves, it signals it is ready again. but if you close the modal without saving, we'd be stuck in a "not ready" state

},
[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(() => {
Expand All @@ -67,10 +70,10 @@ const Overlay: FunctionComponent<Props> = ({
}
}, [instance, onOpen]);

const handleCloseModal = useCallback(() => {
const handleCloseModalAfterSave = useCallback(() => {
if (instance && !options.fidesEmbed) {
instance.hide();
dispatchCloseEvent();
dispatchCloseEvent({ saved: true });
}
}, [instance, dispatchCloseEvent, options.fidesEmbed]);

Expand Down Expand Up @@ -156,7 +159,7 @@ const Overlay: FunctionComponent<Props> = ({
experience={experience.experience_config}
renderModalFooter={() =>
renderModalFooter({
onClose: handleCloseModal,
onClose: handleCloseModalAfterSave,
isMobile: false,
})
}
Expand All @@ -170,7 +173,7 @@ const Overlay: FunctionComponent<Props> = ({
onVendorPageClick={onVendorPageClick}
renderModalFooter={() =>
renderModalFooter({
onClose: handleCloseModal,
onClose: handleCloseModalAfterSave,
isMobile: false,
})
}
Expand Down
57 changes: 56 additions & 1 deletion clients/fides-js/src/extensions/gpp.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,71 @@
/* eslint-disable no-underscore-dangle */
/**
* Extension for GPP
*
* Usage:
* 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();
4 changes: 2 additions & 2 deletions clients/fides-js/src/lib/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ declare global {
*/
export type FidesEventDetail = FidesCookie & {
debug?: boolean;
extraDetails?: Record<string, string>;
extraDetails?: Record<string, string | boolean>;
};

export type FidesEvent = CustomEvent<FidesEventDetail>;
Expand All @@ -59,7 +59,7 @@ export const dispatchFidesEvent = (
type: FidesEventType,
cookie: FidesCookie,
debug: boolean,
extraDetails?: Record<string, string>
extraDetails?: Record<string, string | boolean>
) => {
if (typeof window !== "undefined" && typeof CustomEvent !== "undefined") {
const event = new CustomEvent(type, {
Expand Down
2 changes: 1 addition & 1 deletion clients/fides-js/src/lib/initialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ export const initialize = async ({
identity: cookie.identity,
fides_string: cookie.fides_string,
tcf_consent: cookie.tcf_consent,
experience,
experience: effectiveExperience,
Copy link
Contributor Author

@allisonking allisonking Nov 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a potentially consequential change—before, if the experience was not server side provided, then window.Fides.experience would be undefined. I don't think there was a reason for this, and I didn't see why we shouldn't always have the effective experience here. but let me know if I'm missing something!

This basically makes it possible for the "FidesInitialized" listener in gpp.ts to grab the experience easily, without it having to come through the event object.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good. The return obj built here is only used to update the window.Fides obj, which we don't rely on to make any functional decisions within or outside of Fides. Regardless, we should absolutely return the most up-to-date experience here. Thanks!

geolocation,
options,
initialized: true,
Expand Down
28 changes: 4 additions & 24 deletions clients/fides-js/src/lib/tcf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -149,33 +148,14 @@ export const generateFidesString = async ({
return Promise.resolve(encodedString);
};

/**
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved to a separate file so gpp can also use this function

* 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.
*/
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) => {
/*
Expand Down
3 changes: 3 additions & 0 deletions clients/fides-js/src/lib/tcf/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 21 additions & 0 deletions clients/fides-js/src/lib/tcf/events.ts
Original file line number Diff line number Diff line change
@@ -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;
};