Skip to content
Open
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
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -78,4 +78,4 @@
"@financial-times/o-tracking": "^4.5.1",
"@financial-times/o-viewport": "^5.1.2"
}
}
}
9 changes: 6 additions & 3 deletions src/cmp/loadFtCmp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,12 @@ export function loadFtCmpScript(): Promise<void> {
});
}

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);
}
81 changes: 81 additions & 0 deletions src/cmp/vendorConsent.ts
Original file line number Diff line number Diff line change
@@ -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<number, boolean>;
};
vendor?: {
consents?: Record<number, boolean>;
};
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,
);
}
}
});
}
42 changes: 39 additions & 3 deletions src/consentMonitor/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down Expand Up @@ -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;
Expand All @@ -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() || "";
Expand All @@ -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"];
Expand All @@ -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) => {
Expand All @@ -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) => {
Expand Down
Loading