From ea84dc29bfd1b5e51b41995a9ec4e52cc311aeab Mon Sep 17 00:00:00 2001 From: Tom Van Dort Date: Fri, 8 Nov 2024 20:11:39 -0500 Subject: [PATCH] HJ-166: Support BlueConic objectives (#5479) Co-authored-by: Neville Samuell --- .../__tests__/integrations/blueconic.test.ts | 106 ++++++++++++++++++ clients/fides-js/src/docs/fides.ts | 5 + clients/fides-js/src/fides-tcf.ts | 2 + clients/fides-js/src/fides.ts | 2 + .../fides-js/src/integrations/blueconic.ts | 64 +++++++++++ clients/fides-js/src/integrations/shopify.ts | 10 +- clients/fides-js/src/lib/consent-constants.ts | 10 ++ clients/fides-js/src/lib/consent-types.ts | 2 + 8 files changed, 193 insertions(+), 8 deletions(-) create mode 100644 clients/fides-js/__tests__/integrations/blueconic.test.ts create mode 100644 clients/fides-js/src/integrations/blueconic.ts diff --git a/clients/fides-js/__tests__/integrations/blueconic.test.ts b/clients/fides-js/__tests__/integrations/blueconic.test.ts new file mode 100644 index 0000000000..52e3240474 --- /dev/null +++ b/clients/fides-js/__tests__/integrations/blueconic.test.ts @@ -0,0 +1,106 @@ +import { FidesGlobal } from "../../src/fides"; +import { blueconic } from "../../src/integrations/blueconic"; +import { MARKETING_CONSENT_KEYS } from "../../src/lib/consent-constants"; + +const getBlueConicEvent = () => + ({ + subscribe: () => {}, + }) as const; + +const setupBlueConicClient = ( + initialized: "initialized" | "uinitialized" = "initialized", +) => { + const client = { + profile: { + setConsentedObjectives: jest.fn(), + setRefusedObjectives: jest.fn(), + updateProfile: jest.fn(), + }, + event: initialized === "initialized" ? getBlueConicEvent() : undefined, + } as const satisfies typeof window.blueConicClient; + + window.blueConicClient = client; + + return client; +}; + +const setupFidesWithConsent = (key: string, optInStatus: boolean) => { + window.Fides = { + consent: { + [key]: optInStatus, + }, + } as any as FidesGlobal; +}; + +describe("blueconic", () => { + afterEach(() => { + window.blueConicClient = undefined; + window.Fides = undefined as any; + jest.resetAllMocks(); + }); + + test("that other modes are not supported", () => { + expect(() => blueconic({ approach: "other mode" as "onetrust" })).toThrow(); + }); + + test("that nothing happens when blueconic and fides are not initialized", () => { + setupBlueConicClient("uinitialized"); + + blueconic(); + + expect( + window.blueConicClient?.profile?.setConsentedObjectives, + ).not.toHaveBeenCalled(); + expect( + window.blueConicClient?.profile?.setConsentedObjectives, + ).not.toHaveBeenCalled(); + expect( + window.blueConicClient?.profile?.updateProfile, + ).not.toHaveBeenCalled(); + }); + + describe.each(MARKETING_CONSENT_KEYS)( + "when consent is set via the %s key", + (key) => { + test.each([ + [ + "opted in", + true, + ["iab_purpose_1", "iab_purpose_2", "iab_purpose_3", "iab_purpose_4"], + [], + ], + [ + "opted out", + false, + ["iab_purpose_1"], + ["iab_purpose_2", "iab_purpose_3", "iab_purpose_4"], + ], + ])( + "that a user who has %s gets the correct consented and refused objectives", + (_, optInStatus, consented, refused) => { + const blueConicClient = setupBlueConicClient(); + setupFidesWithConsent(key, optInStatus); + + blueconic(); + + expect( + blueConicClient.profile.setConsentedObjectives, + ).toHaveBeenCalledWith(consented); + expect( + blueConicClient.profile.setRefusedObjectives, + ).toHaveBeenCalledWith(refused); + expect(blueConicClient.profile.updateProfile).toHaveBeenCalled(); + }, + ); + }, + ); + + test.each(["FidesInitialized", "FidesUpdated", "onBlueConicLoaded"])( + "that %s event can cause objectives to be set", + (eventName) => { + const spy = jest.spyOn(window, "addEventListener"); + blueconic(); + expect(spy).toHaveBeenCalledWith(eventName, expect.any(Function)); + }, + ); +}); diff --git a/clients/fides-js/src/docs/fides.ts b/clients/fides-js/src/docs/fides.ts index 00f49fd5e5..fe4c1baebc 100644 --- a/clients/fides-js/src/docs/fides.ts +++ b/clients/fides-js/src/docs/fides.ts @@ -328,4 +328,9 @@ export interface Fides { * @internal */ shopify: (options: any) => void; + + /** + * @internal + */ + blueconic: (options?: { approach: "onetrust" }) => void; } diff --git a/clients/fides-js/src/fides-tcf.ts b/clients/fides-js/src/fides-tcf.ts index e8c95fcfd0..e0c9508c2b 100644 --- a/clients/fides-js/src/fides-tcf.ts +++ b/clients/fides-js/src/fides-tcf.ts @@ -16,6 +16,7 @@ import { isPrivacyExperience, shouldResurfaceConsent, } from "./fides"; +import { blueconic } from "./integrations/blueconic"; import { gtm } from "./integrations/gtm"; import { meta } from "./integrations/meta"; import { shopify } from "./integrations/shopify"; @@ -264,6 +265,7 @@ const _Fides: FidesGlobal = { identity: {}, tcf_consent: {}, saved_consent: {}, + blueconic, gtm, init, config: undefined, diff --git a/clients/fides-js/src/fides.ts b/clients/fides-js/src/fides.ts index 6d44eb446c..4e45275268 100644 --- a/clients/fides-js/src/fides.ts +++ b/clients/fides-js/src/fides.ts @@ -5,6 +5,7 @@ * * See the overall package docs in ./docs/README.md for more! */ +import { blueconic } from "./integrations/blueconic"; import { gtm } from "./integrations/gtm"; import { meta } from "./integrations/meta"; import { shopify } from "./integrations/shopify"; @@ -204,6 +205,7 @@ const _Fides: FidesGlobal = { identity: {}, tcf_consent: {}, saved_consent: {}, + blueconic, gtm, init, config: undefined, diff --git a/clients/fides-js/src/integrations/blueconic.ts b/clients/fides-js/src/integrations/blueconic.ts new file mode 100644 index 0000000000..f0e219b301 --- /dev/null +++ b/clients/fides-js/src/integrations/blueconic.ts @@ -0,0 +1,64 @@ +import { MARKETING_CONSENT_KEYS } from "../lib/consent-constants"; + +declare global { + interface Window { + blueConicClient?: { + profile?: { + setConsentedObjectives: (objectives: string[]) => void; + setRefusedObjectives: (objectives: string[]) => void; + updateProfile: () => void; + }; + event?: { + subscribe: any; + }; + }; + } +} + +// https://support.blueconic.com/hc/en-us/articles/202605221-JavaScript-front-end-API +const blueConicLoaded = () => + typeof window.blueConicClient !== "undefined" && + typeof window.blueConicClient.event !== "undefined" && + typeof window.blueConicClient.event.subscribe !== "undefined"; + +const configureObjectives = () => { + if (!blueConicLoaded() || !window.blueConicClient?.profile) { + return; + } + + const profile = window.blueConicClient?.profile; + const { consent } = window.Fides; + const optedIn = MARKETING_CONSENT_KEYS.some((key) => consent[key]); + if (optedIn) { + profile.setConsentedObjectives([ + "iab_purpose_1", + "iab_purpose_2", + "iab_purpose_3", + "iab_purpose_4", + ]); + profile.setRefusedObjectives([]); + } else { + profile.setConsentedObjectives(["iab_purpose_1"]); + profile.setRefusedObjectives([ + "iab_purpose_2", + "iab_purpose_3", + "iab_purpose_4", + ]); + } + + profile.updateProfile(); +}; + +export const blueconic = ( + { approach }: { approach: "onetrust" } = { approach: "onetrust" }, +) => { + if (approach !== "onetrust") { + throw new Error("Unsupported approach"); + } + + window.addEventListener("FidesInitialized", configureObjectives); + window.addEventListener("FidesUpdated", configureObjectives); + window.addEventListener("onBlueConicLoaded", configureObjectives); + + configureObjectives(); +}; diff --git a/clients/fides-js/src/integrations/shopify.ts b/clients/fides-js/src/integrations/shopify.ts index 9d9f992e15..260e616fd9 100644 --- a/clients/fides-js/src/integrations/shopify.ts +++ b/clients/fides-js/src/integrations/shopify.ts @@ -1,3 +1,4 @@ +import { MARKETING_CONSENT_KEYS } from "../lib/consent-constants"; import { NoticeConsent } from "../lib/consent-types"; declare global { @@ -18,14 +19,7 @@ declare global { } const CONSENT_MAP = { - marketing: [ - "marketing", - "data_sales_and_sharing", - "data_sales_sharing_gpp_us_state", - "data_sharing_gpp_us_state", - "data_sales_gpp_us_state", - "targeted_advertising_gpp_us_state", - ], + marketing: MARKETING_CONSENT_KEYS, sale_of_data: [ "marketing", "data_sales_and_sharing", diff --git a/clients/fides-js/src/lib/consent-constants.ts b/clients/fides-js/src/lib/consent-constants.ts index 7f8b1ea8b3..6702771e79 100644 --- a/clients/fides-js/src/lib/consent-constants.ts +++ b/clients/fides-js/src/lib/consent-constants.ts @@ -125,3 +125,13 @@ export const FIDES_OVERRIDE_EXPERIENCE_LANGUAGE_VALIDATOR_MAP: { export const FIDES_OVERLAY_WRAPPER = "fides-overlay-wrapper"; export const FIDES_I18N_ICON = "fides-i18n-icon"; + +export const MARKETING_CONSENT_KEYS = [ + "marketing", + "data_sales_and_sharing", + "data_sales_sharing_gpp_us_state", + "data_sharing_gpp_us_state", + "data_sales_gpp_us_state", + "targeted_advertising_gpp_us_state", + "sales_sharing_targeted_advertising_gpp_us_national", +]; diff --git a/clients/fides-js/src/lib/consent-types.ts b/clients/fides-js/src/lib/consent-types.ts index 5a9003a52d..d1a6350c70 100644 --- a/clients/fides-js/src/lib/consent-types.ts +++ b/clients/fides-js/src/lib/consent-types.ts @@ -4,6 +4,7 @@ import type { FidesExperienceConfig, FidesOptions, } from "../docs"; +import { blueconic } from "../integrations/blueconic"; import type { gtm } from "../integrations/gtm"; import type { meta } from "../integrations/meta"; import type { shopify } from "../integrations/shopify"; @@ -161,6 +162,7 @@ export interface FidesGlobal extends Fides { options: FidesInitOptions; saved_consent: NoticeConsent; tcf_consent: TcfOtherConsent; + blueconic: typeof blueconic; gtm: typeof gtm; init: (config?: FidesConfig) => Promise; meta: typeof meta;