From b499ea809c7b24c8951330b991e70a918223180b Mon Sep 17 00:00:00 2001 From: Dhia Shakiry Date: Tue, 21 Jul 2026 12:34:24 +0100 Subject: [PATCH 1/3] Add specific TCF API lookup to return Brandmetrics and Linkedin cookie banner consents - send returned consent (fires on both fresh and returning sessions) --- package-lock.json | 4 +- package.json | 4 +- src/cmp/loadFtCmp.ts | 9 +++-- src/cmp/vendorConsent.ts | 77 +++++++++++++++++++++++++++++++++++++ src/consentMonitor/index.ts | 31 +++++++++++++-- 5 files changed, 115 insertions(+), 10 deletions(-) create mode 100644 src/cmp/vendorConsent.ts diff --git a/package-lock.json b/package-lock.json index 48063d6..2105336 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-rc2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@phantomstudios/ft-lib", - "version": "0.5.1", + "version": "0.5.2-rc2", "license": "MIT", "dependencies": { "yup": "^1.0.2" diff --git a/package.json b/package.json index ed15946..06d3368 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-rc2", "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..e765d61 --- /dev/null +++ b/src/cmp/vendorConsent.ts @@ -0,0 +1,77 @@ +// 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; + }; +} + +export interface VendorConsentResults { + brandmetrics: boolean; + linkedIn: boolean; + purpose1: boolean; +} + +// 2. Main Implementation Function +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 results: VendorConsentResults = { + brandmetrics, + linkedIn, + purpose1, + }; + + // 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..f1225a8 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,18 @@ 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 { + 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) => { From 685af92824da15170a12ab227b6b7ea9e69796d2 Mon Sep 17 00:00:00 2001 From: Dhia Shakiry Date: Tue, 21 Jul 2026 17:04:06 +0100 Subject: [PATCH 2/3] initVendorConsentListener - wait for cmpCallback - retrigger listener on consentReady (manage cookies updated) --- package-lock.json | 4 ++-- package.json | 2 +- src/cmp/vendorConsent.ts | 1 - src/consentMonitor/index.ts | 23 +++++++++++++++++------ 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2105336..db58e10 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@phantomstudios/ft-lib", - "version": "0.5.2-rc2", + "version": "0.5.2-rc5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@phantomstudios/ft-lib", - "version": "0.5.2-rc2", + "version": "0.5.2-rc5", "license": "MIT", "dependencies": { "yup": "^1.0.2" diff --git a/package.json b/package.json index 06d3368..ba77c90 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.2-rc2", + "version": "0.5.2-rc5", "main": "lib/index.js", "types": "lib/index.d.ts", "homepage": "https://github.com/phantomstudios/ft-lib#readme", diff --git a/src/cmp/vendorConsent.ts b/src/cmp/vendorConsent.ts index e765d61..19412d3 100644 --- a/src/cmp/vendorConsent.ts +++ b/src/cmp/vendorConsent.ts @@ -27,7 +27,6 @@ export interface VendorConsentResults { purpose1: boolean; } -// 2. Main Implementation Function export function initVendorConsentListener( onConsentUpdate: (results: VendorConsentResults) => void, ): void { diff --git a/src/consentMonitor/index.ts b/src/consentMonitor/index.ts index f1225a8..47d9e1d 100644 --- a/src/consentMonitor/index.ts +++ b/src/consentMonitor/index.ts @@ -104,13 +104,15 @@ export class ConsentMonitor { // Added for required brandmetrics image pixels and linkedin script consent (CMP specific vendor consents integration) private attachVendorConsentListeners(): void { - initVendorConsentListener((vendorConsents: VendorConsentResults) => { - const vendorConsentEvent = new CustomEvent("cmp_vendorConsent", { - detail: vendorConsents, - }); + enqueueCmpCallback(() => { + initVendorConsentListener((vendorConsents: VendorConsentResults) => { + const vendorConsentEvent = new CustomEvent("cmp_vendorConsent", { + detail: vendorConsents, + }); - window.dispatchEvent(vendorConsentEvent); - debug("[CMP Consent lookup Event", vendorConsents); + window.dispatchEvent(vendorConsentEvent); + debug("[CMP Consent lookup Event", vendorConsents); + }); }); } @@ -123,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) => { From 77f0bfa5b9a71f4cbb2b91d6f46bee851cafd155 Mon Sep 17 00:00:00 2001 From: Dhia Shakiry Date: Thu, 23 Jul 2026 12:38:45 +0100 Subject: [PATCH 3/3] pass TCData - gdprApplies --- package-lock.json | 4 ++-- package.json | 2 +- src/cmp/vendorConsent.ts | 5 +++++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index db58e10..7d6ba1b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@phantomstudios/ft-lib", - "version": "0.5.2-rc5", + "version": "0.5.2-rc6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@phantomstudios/ft-lib", - "version": "0.5.2-rc5", + "version": "0.5.2-rc6", "license": "MIT", "dependencies": { "yup": "^1.0.2" diff --git a/package.json b/package.json index ba77c90..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.2-rc5", + "version": "0.5.2-rc6", "main": "lib/index.js", "types": "lib/index.d.ts", "homepage": "https://github.com/phantomstudios/ft-lib#readme", diff --git a/src/cmp/vendorConsent.ts b/src/cmp/vendorConsent.ts index 19412d3..ca7a06e 100644 --- a/src/cmp/vendorConsent.ts +++ b/src/cmp/vendorConsent.ts @@ -19,12 +19,14 @@ interface TCFData { vendor?: { consents?: Record; }; + gdprApplies?: boolean | undefined; } export interface VendorConsentResults { brandmetrics: boolean; linkedIn: boolean; purpose1: boolean; + gdprApplies: boolean; } export function initVendorConsentListener( @@ -53,10 +55,13 @@ export function initVendorConsentListener( // 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