diff --git a/package-lock.json b/package-lock.json index 48063d6..7d6ba1b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@phantomstudios/ft-lib", - "version": "0.5.1", + "version": "0.5.2-rc6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@phantomstudios/ft-lib", - "version": "0.5.1", + "version": "0.5.2-rc6", "license": "MIT", "dependencies": { "yup": "^1.0.2" diff --git a/package.json b/package.json index ed15946..432b57d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@phantomstudios/ft-lib", "description": "A collection of Javascript UI & tracking utils for FT sites", - "version": "0.5.1", + "version": "0.5.2-rc6", "main": "lib/index.js", "types": "lib/index.d.ts", "homepage": "https://github.com/phantomstudios/ft-lib#readme", @@ -78,4 +78,4 @@ "@financial-times/o-tracking": "^4.5.1", "@financial-times/o-viewport": "^5.1.2" } -} +} \ No newline at end of file diff --git a/src/cmp/loadFtCmp.ts b/src/cmp/loadFtCmp.ts index a98c30e..e416274 100644 --- a/src/cmp/loadFtCmp.ts +++ b/src/cmp/loadFtCmp.ts @@ -20,9 +20,12 @@ export function loadFtCmpScript(): Promise { }); } -export function enqueueCmpCallback(cb: () => void): void { - if (!window._sp_) window._sp_ = {}; - if (!window._sp_queue) window._sp_queue = []; +export function initCmpQueue(): void { + window._sp_ = window._sp_ || {}; + window._sp_queue = window._sp_queue || []; +} +export function enqueueCmpCallback(cb: () => void): void { + initCmpQueue(); window._sp_queue!.push(cb); } diff --git a/src/cmp/vendorConsent.ts b/src/cmp/vendorConsent.ts new file mode 100644 index 0000000..ca7a06e --- /dev/null +++ b/src/cmp/vendorConsent.ts @@ -0,0 +1,81 @@ +// 1. Declare Global Window Types for TCF API +declare global { + interface Window { + __tcfapi?: ( + command: string, + version: number, + callback: (tcData: TCFData | null, success: boolean) => void, + parameter?: any, + ) => void; + } +} + +interface TCFData { + eventStatus: "tcloaded" | "useractioncomplete" | "cmpuishown"; + listenerId: number; + purpose?: { + consents?: Record; + }; + vendor?: { + consents?: Record; + }; + gdprApplies?: boolean | undefined; +} + +export interface VendorConsentResults { + brandmetrics: boolean; + linkedIn: boolean; + purpose1: boolean; + gdprApplies: boolean; +} + +export function initVendorConsentListener( + onConsentUpdate: (results: VendorConsentResults) => void, +): void { + if (typeof window.__tcfapi !== "function") { + console.warn("[CMP] window.__tcfapi is not defined on this page yet."); + return; + } + + window.__tcfapi("addEventListener", 2, (tcData, success) => { + if (!success || !tcData) return; + + // Handles BOTH returning visitors ('tcloaded') AND live banner accepts ('useractioncomplete') + const isConsentReady = + tcData.eventStatus === "tcloaded" || + tcData.eventStatus === "useractioncomplete"; + + if (isConsentReady) { + // Brandmetrics (IAB Vendor ID: 422) + const brandmetrics = tcData.vendor?.consents?.[422] === true; + + // LinkedIn (IAB Vendor ID: 804) + const linkedIn = tcData.vendor?.consents?.[804] === true; + + // Purpose 1: Device storage/access (Cookies) + const purpose1 = tcData.purpose?.consents?.[1] === true; + + const gdprApplies = tcData.gdprApplies !== false ? true : false; + + const results: VendorConsentResults = { + brandmetrics, + linkedIn, + purpose1, + gdprApplies, + }; + + // Trigger your callback handler with the updated states + onConsentUpdate(results); + + // Clean up listener after handling the event + if (typeof tcData.listenerId === "number") { + window.__tcfapi?.( + "removeEventListener", + 2, + () => {}, + tcData.listenerId, + ); + } + } + }); +} diff --git a/src/consentMonitor/index.ts b/src/consentMonitor/index.ts index fd25438..47d9e1d 100644 --- a/src/consentMonitor/index.ts +++ b/src/consentMonitor/index.ts @@ -6,6 +6,10 @@ import { import Debug from "debug"; import { enqueueCmpCallback, loadFtCmpScript } from "../cmp/loadFtCmp"; +import { + initVendorConsentListener, + VendorConsentResults, +} from "../cmp/vendorConsent"; const debug = Debug("@phantomstudios/ft-lib/consentMonitor"); @@ -35,6 +39,11 @@ export class ConsentMonitor { private _isDevEnvironment = false; private _isInitialized = false; private _hostname: string; + private _vendorConsents = { + brandmetrics: false, + linkedIn: false, + purpose1: false, + } as VendorConsentResults; public get consent(): boolean { return this._consent; @@ -48,10 +57,12 @@ export class ConsentMonitor { public get isInitialized(): boolean { return this._isInitialized; } - public get userHasConsented(): boolean { return this._consent; } + public get vendorConsents(): VendorConsentResults { + return this._vendorConsents; + } getCookieValue = (name: string) => document.cookie.match("(^|;)\\s*" + name + "\\s*=\\s*([^;]+)")?.pop() || ""; @@ -71,10 +82,12 @@ export class ConsentMonitor { this._hostname.includes(h), ); + this.attachVendorConsentListeners(); // listen to existing consent data fetch + this.attachCmpListeners(); // listen to banner interaction + + // load banner loadFtCmpScript() .then(() => { - this.attachCmpListeners(); - const propertyConfig = window.location.hostname.endsWith(".ft.com") ? properties["FT_DOTCOM_PROD"] : properties["FT_DOTCOM_TEST"]; @@ -89,6 +102,20 @@ export class ConsentMonitor { .catch((err) => console.error(err)); } + // Added for required brandmetrics image pixels and linkedin script consent (CMP specific vendor consents integration) + private attachVendorConsentListeners(): void { + enqueueCmpCallback(() => { + initVendorConsentListener((vendorConsents: VendorConsentResults) => { + const vendorConsentEvent = new CustomEvent("cmp_vendorConsent", { + detail: vendorConsents, + }); + + window.dispatchEvent(vendorConsentEvent); + debug("[CMP Consent lookup Event", vendorConsents); + }); + }); + } + private attachCmpListeners(): void { enqueueCmpCallback(() => { const onReady: ConsentReadyHandler = (_l, _u, _t, info) => { @@ -98,6 +125,15 @@ export class ConsentMonitor { } else { this.disablePermutive(); } + + initVendorConsentListener((vendorConsents: VendorConsentResults) => { + const vendorConsentEvent = new CustomEvent("cmp_vendorConsent", { + detail: vendorConsents, + }); + + window.dispatchEvent(vendorConsentEvent); + debug("[CMP Consent lookup Event", vendorConsents); + }); }; const onChoice: MessageChoiceHandler = (_l, _c, typeId) => {