diff --git a/CHANGELOG.md b/CHANGELOG.md index 72561687bc2..083d90af74d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,10 @@ The types of changes are: - Add Great Britain as a consent option [#4628](https://github.com/ethyca/fides/pull/4628) - Navbar update and new properties page [#4633](https://github.com/ethyca/fides/pull/4633) +### Changed +- Update when GPP API reports signal status: ready [#4635](https://github.com/ethyca/fides/pull/4635) + + ## [2.30.1](https://github.com/ethyca/fides/compare/2.30.0...2.30.1) ### Fixed diff --git a/clients/fides-js/src/fides-ext-gpp.ts b/clients/fides-js/src/fides-ext-gpp.ts index 800ab4648ce..71c6d8ae29f 100644 --- a/clients/fides-js/src/fides-ext-gpp.ts +++ b/clients/fides-js/src/fides-ext-gpp.ts @@ -22,12 +22,17 @@ import { import { makeStub } from "./lib/gpp/stub"; import { extractTCStringForCmpApi } from "./lib/tcf/events"; import { + allNoticesAreDefaultOptIn, isPrivacyExperience, shouldResurfaceConsent, } from "./lib/consent-utils"; import { ETHYCA_CMP_ID } from "./lib/tcf/constants"; import type { Fides } from "./lib/initialize"; -import type { OverrideOptions } from "./lib/consent-types"; +import type { + CookieKeyConsent, + OverrideOptions, + PrivacyNoticeWithPreference, +} from "./lib/consent-types"; import { GPPUSApproach, GppFunction } from "./lib/gpp/types"; import { FidesEvent } from "./fides"; import { @@ -49,6 +54,34 @@ declare global { } } +/** + * Special GPP util method to determine if user has existing prefs, including those on the cookie or fides string. + * Specifically, this method does not consider legacy consent has an existing pref, since they aren't relevant for GPP. + * @param consent: CookieKeyConsent | undefined + * @param fides_string: string | undefined + * @param notices: Array | undefined + * @return boolean + */ +const userHasExistingPrefs = ( + consent: CookieKeyConsent | undefined, + fides_string: string | undefined, + notices: Array | undefined +): boolean => { + if (!consent) { + return false; + } + if (fides_string) { + return true; + } + return Boolean( + notices && + Object.entries(consent).some( + ([key, val]) => + key in notices.map((i) => i.notice_key) && val !== undefined + ) + ); +}; + /** * Wrapper around setting a TC string on the CMP API object. * Returns whether or not the TC string was set. @@ -105,9 +138,18 @@ export const initializeGppCmpApi = () => { window.addEventListener("FidesInitialized", (event) => { const { experience } = window.Fides; cmpApi.setSupportedAPIs(getSupportedApis()); + // Set status to ready immediately upon initialization, if either: + // A. Consent should not be resurfaced + // B. User has no prefs and has all opt-in notices if ( isPrivacyExperience(experience) && - !shouldResurfaceConsent(experience, event.detail) + (!shouldResurfaceConsent(experience, event.detail) || + (allNoticesAreDefaultOptIn(experience.privacy_notices) && + !userHasExistingPrefs( + event.detail.consent, + event.detail.fides_string, + experience.privacy_notices + ))) ) { const tcSet = setTcString(event, cmpApi); if (tcSet) { @@ -126,13 +168,24 @@ export const initializeGppCmpApi = () => { } }); - window.addEventListener("FidesUIShown", () => { - cmpApi.setSignalStatus(SignalStatus.NOT_READY); - cmpApi.setCmpDisplayStatus(CmpDisplayStatus.VISIBLE); - + window.addEventListener("FidesUIShown", (event) => { // Set US GPP notice fields const { experience } = window.Fides; if (isPrivacyExperience(experience)) { + // set signal status to ready only for users with no existing prefs and if notices are all opt-in by default + if ( + allNoticesAreDefaultOptIn(experience.privacy_notices) && + !userHasExistingPrefs( + event.detail.consent, + event.detail.fides_string, + experience.privacy_notices + ) + ) { + cmpApi.setSignalStatus(SignalStatus.READY); + } else { + cmpApi.setSignalStatus(SignalStatus.NOT_READY); + } + cmpApi.setCmpDisplayStatus(CmpDisplayStatus.VISIBLE); const sectionsChanged = setGppNoticesProvidedFromExperience({ cmpApi, experience, diff --git a/clients/fides-js/src/lib/consent-utils.ts b/clients/fides-js/src/lib/consent-utils.ts index 256a43ea054..d5b379a0b48 100644 --- a/clients/fides-js/src/lib/consent-utils.ts +++ b/clients/fides-js/src/lib/consent-utils.ts @@ -9,6 +9,7 @@ import { OverrideOptions, PrivacyExperience, PrivacyNotice, + PrivacyNoticeWithPreference, UserConsentPreference, UserGeolocation, } from "./consent-types"; @@ -58,6 +59,16 @@ export const isPrivacyExperience = ( return false; }; +export const allNoticesAreDefaultOptIn = ( + notices: Array | undefined +): boolean => + Boolean( + notices && + notices.every( + (notice) => notice.default_preference === UserConsentPreference.OPT_IN + ) + ); + /** * Construct user location str to be ingested by Fides API * Returns null if geolocation cannot be constructed by provided params, e.g. us_ca diff --git a/clients/privacy-center/cypress/e2e/consent-banner-gpp.cy.ts b/clients/privacy-center/cypress/e2e/consent-banner-gpp.cy.ts index ad7bab79e97..df8d8194d2f 100644 --- a/clients/privacy-center/cypress/e2e/consent-banner-gpp.cy.ts +++ b/clients/privacy-center/cypress/e2e/consent-banner-gpp.cy.ts @@ -63,7 +63,7 @@ describe("Fides-js GPP extension", () => { .its("lastCall.args") .then(([data, success]) => { expect(success).to.eql(true); - expect(data.signalStatus).to.eql("not ready"); + expect(data.signalStatus).to.eql("ready"); }); }); }); @@ -98,7 +98,7 @@ describe("Fides-js GPP extension", () => { expect(data.eventName).to.eql("listenerRegistered"); const { cmpDisplayStatus, signalStatus, gppString } = data.pingData; expect(cmpDisplayStatus).to.eql("visible"); - expect(signalStatus).to.eql("not ready"); + expect(signalStatus).to.eql("ready"); expect(gppString).to.eql("DBAA"); // empty string, header only }); @@ -240,13 +240,13 @@ describe("Fides-js GPP extension", () => { * Expected flow for a returning user who opens but then closes the modal without making a change: * 1. listenerRegistered * 2. User opens the modal - * 3. signalStatus = not ready + * 3. signalStatus = ready * 4. cmpDisplayStatus = visible - * 5. User closes the modal without saving anything + * 5. User closes the modal which automatically triggers preference save * 6. cmpDisplayStatus = hidden - * 7. signalStatus = ready + * 7. signalStatus = not ready */ - it("can handle returning user closing the modal without a preference change", () => { + it("can handle returning user closing the modal", () => { const cookie = mockCookie({ tcf_version_hash: TCF_VERSION_HASH, }); @@ -265,12 +265,16 @@ describe("Fides-js GPP extension", () => { win.__gpp("addEventListener", cy.stub().as("gppListener")); }); cy.get("#fides-modal-link").click(); + cy.get(".fides-modal-content .fides-close-button").click(); const expected = [ { eventName: "listenerRegistered", data: true }, - { eventName: "signalStatus", data: "not ready" }, + { eventName: "signalStatus", data: "ready" }, { eventName: "cmpDisplayStatus", data: "visible" }, { eventName: "cmpDisplayStatus", data: "hidden" }, { eventName: "signalStatus", data: "ready" }, + { eventName: "cmpDisplayStatus", data: "hidden" }, + { eventName: "sectionChange", data: "tcfeuv2" }, + { eventName: "signalStatus", data: "ready" }, ]; cy.get("@gppListener") .its("args") @@ -319,7 +323,7 @@ describe("Fides-js GPP extension", () => { applicableSections, supportedAPIs, } = data.pingData; - expect(signalStatus).to.eql("not ready"); + expect(signalStatus).to.eql("ready"); expect(applicableSections).to.eql([]); expect(supportedAPIs).to.eql([]); expect(gppString).to.eql("DBAA"); @@ -355,7 +359,7 @@ describe("Fides-js GPP extension", () => { .its("lastCall.args") .then(([data, success]) => { expect(success).to.eql(true); - expect(data.signalStatus).to.eql("not ready"); + expect(data.signalStatus).to.eql("ready"); }); }); });