Skip to content

Commit 10c7e68

Browse files
Feat/update cookie cmp (#58)
* feat: update cookie logic to introduce cmp script * feat: update cookie logic to introduce cmp script * published npm v0.5.0-rc2 - swapped referrerPolicy on CMP Script * published npm v0.5.0-rc3 - updated window._sp_queue - enqueueCallbacks now firing * publish v0.5.0-rc5 - add cmp client as peer dependency - add interceptManageCookiesLinks for specific link listener for new cooke modal * add additional initSourcePointCMP call, expose consentMonitor in FTTracking * Update consentMonitor to fire consent_update custom event - published v0.5.0-rc14 * Update copilot security alert - published v0.5.0-rc15 * bump node version * fix test * bump release publish job version * bump version --------- Co-authored-by: Dhia Shakiry <dhia@phntms.com>
1 parent d7bc882 commit 10c7e68

11 files changed

Lines changed: 4768 additions & 6966 deletions

File tree

.github/workflows/pr.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414

1515
strategy:
1616
matrix:
17-
node-version: [18.x, 20.x, 22.x]
17+
node-version: [20.x, 22.x, 24.x]
1818

1919
steps:
2020
- uses: actions/checkout@v1

.github/workflows/release.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313

1414
strategy:
1515
matrix:
16-
node-version: [18.x, 20.x, 22.x]
16+
node-version: [20.x, 22.x, 24.x]
1717

1818
steps:
1919
- uses: actions/checkout@v1
@@ -43,7 +43,7 @@ jobs:
4343
- uses: actions/checkout@v2
4444
- uses: actions/setup-node@v1
4545
with:
46-
node-version: 22
46+
node-version: 24
4747
registry-url: https://registry.npmjs.org/
4848
- run: npm ci
4949
- run: npm publish --access=public

.nvmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
v24

package-lock.json

Lines changed: 4538 additions & 6846 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@phantomstudios/ft-lib",
33
"description": "A collection of Javascript UI & tracking utils for FT sites",
4-
"version": "0.4.1-rc1",
4+
"version": "0.5.1",
55
"main": "lib/index.js",
66
"types": "lib/index.d.ts",
77
"homepage": "https://github.com/phantomstudios/ft-lib#readme",
@@ -41,6 +41,11 @@
4141
"@eslint/compat": "^1.2.9",
4242
"@eslint/eslintrc": "^3.3.1",
4343
"@eslint/js": "^9.25.1",
44+
"@financial-times/cmp-client": "^6.2.0",
45+
"@financial-times/o-footer": "^9.2.10",
46+
"@financial-times/o-header": "^11.2.0",
47+
"@financial-times/o-tracking": "^4.9.0",
48+
"@financial-times/o-viewport": "^5.1.2",
4449
"@types/debug": "^4.1.7",
4550
"@types/jest": "^30.0.0",
4651
"@types/youtube": "^0.1.0",
@@ -69,6 +74,7 @@
6974
"yup": "^1.0.2"
7075
},
7176
"peerDependencies": {
77+
"@financial-times/cmp-client": "^6.1.2",
7278
"@financial-times/o-tracking": "^4.5.1",
7379
"@financial-times/o-viewport": "^5.1.2"
7480
}

src/FTTracking/index.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1-
import { consentMonitor } from "../consentMonitor";
1+
import {
2+
ConsentMonitor,
3+
ConsentMonitor as consentMonitor,
4+
} from "../consentMonitor";
25
import { gaTracker } from "../gaTracker";
36
import { oTracker } from "../oTracker";
47
import { ScrollTracker } from "../utils/scroll";
58
import {
6-
validateConfig,
79
ConfigType,
810
OrigamiEventType,
11+
validateConfig,
912
} from "../utils/yupValidator";
1013

1114
export interface TrackingOptions {
@@ -32,6 +35,7 @@ export class FTTracking {
3235
scrollTracker: ScrollTracker;
3336
disableAppFormatTransform: boolean;
3437
logValidationErrors: boolean;
38+
consentMonitor: ConsentMonitor | undefined;
3539
oEvent: (detail: OrigamiEventType) => void;
3640
gaEvent: (category: string, action: string, label: string) => void;
3741
gtmEvent: (category: string, action: string, label: string) => void;
@@ -57,7 +61,10 @@ export class FTTracking {
5761

5862
//cookie consent monitor for permutive tracking
5963
window.addEventListener("load", () => {
60-
new consentMonitor(window.location.hostname, [".app", "preview"]);
64+
this.consentMonitor = new consentMonitor(window.location.hostname, [
65+
".app",
66+
"preview",
67+
]);
6168
});
6269
}
6370
set config(c: ConfigType) {
@@ -68,6 +75,16 @@ export class FTTracking {
6875
return this._config;
6976
}
7077

78+
initializeConsentMonitor = () => {
79+
if (!this.consentMonitor) {
80+
this.consentMonitor = new consentMonitor(window.location.hostname, [
81+
".app",
82+
"preview",
83+
]);
84+
}
85+
return this.consentMonitor;
86+
};
87+
7188
public newPageView(config: ConfigType) {
7289
//Update passed config to otracker,send pageview events and reset scrollTracker
7390
validateConfig(

src/cmp/loadFtCmp.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
export function loadFtCmpScript(): Promise<void> {
2+
if (document.getElementById("ft-cmp-loader")) {
3+
return Promise.resolve();
4+
}
5+
6+
return new Promise<void>((resolve, reject) => {
7+
const script: HTMLScriptElement = document.createElement("script");
8+
script.id = "ft-cmp-loader";
9+
script.async = true;
10+
script.src = "https://consent-notice.ft.com/cmp.js";
11+
script.referrerPolicy = window.location.hostname.endsWith(".ft.com")
12+
? "" // production hosts
13+
: "no-referrer-when-downgrade"; // localhost / preview
14+
15+
script.onload = () => resolve();
16+
script.onerror = () =>
17+
reject(new Error("Sourcepoint CMP script failed to load"));
18+
19+
document.head.appendChild(script);
20+
});
21+
}
22+
23+
export function enqueueCmpCallback(cb: () => void): void {
24+
if (!window._sp_) window._sp_ = {};
25+
if (!window._sp_queue) window._sp_queue = [];
26+
27+
window._sp_queue!.push(cb);
28+
}

src/consentMonitor/index.ts

Lines changed: 132 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,120 +1,168 @@
1+
import {
2+
initSourcepointCmp,
3+
interceptManageCookiesLinks,
4+
properties,
5+
} from "@financial-times/cmp-client";
16
import Debug from "debug";
2-
const debug = Debug("@phantomstudios/ft-lib");
7+
8+
import { enqueueCmpCallback, loadFtCmpScript } from "../cmp/loadFtCmp";
9+
10+
const debug = Debug("@phantomstudios/ft-lib/consentMonitor");
311

412
const DEFAULT_DEV_HOSTS = ["localhost", "phq", ".app", "preview"];
513

6-
export class consentMonitor {
7-
protected _consent = false;
8-
protected _devHosts: string[];
9-
protected _isDevEnvironment = false;
10-
protected _hostname: string;
11-
protected _isInitialized = false;
14+
interface ConsentReadyInfo {
15+
consentedToAll: boolean;
16+
}
17+
type ConsentReadyHandler = (
18+
legislation: string,
19+
uuid: string,
20+
tcData: unknown,
21+
info: ConsentReadyInfo,
22+
) => void;
23+
type MessageChoiceHandler = (
24+
legislation: string,
25+
choiceId: number,
26+
choiceTypeId: number,
27+
) => void;
1228

13-
constructor(hostname?: string, devHosts?: string[] | string) {
14-
if (Array.isArray(devHosts)) {
15-
this._devHosts = devHosts.concat(DEFAULT_DEV_HOSTS);
16-
} else if (devHosts === undefined) {
17-
this._devHosts = DEFAULT_DEV_HOSTS;
18-
} else {
19-
this._devHosts = DEFAULT_DEV_HOSTS;
20-
this._devHosts.push(devHosts);
21-
}
29+
const CMP_CHOICE_ACCEPT_ALL = 11;
30+
const CMP_CHOICE_REJECT_ALL = 13;
2231

23-
this._hostname = hostname || window.location.hostname;
24-
this.init();
25-
}
32+
export class ConsentMonitor {
33+
private _consent = false;
34+
private _devHosts: string[];
35+
private _isDevEnvironment = false;
36+
private _isInitialized = false;
37+
private _hostname: string;
2638

27-
get consent(): boolean {
39+
public get consent(): boolean {
2840
return this._consent;
2941
}
30-
31-
get devHosts(): string[] | string {
42+
public get devHosts(): string[] {
3243
return this._devHosts;
3344
}
34-
35-
get isDevEnvironment(): boolean {
45+
public get isDevEnvironment(): boolean {
3646
return this._isDevEnvironment;
3747
}
38-
39-
get isInitialized(): boolean {
48+
public get isInitialized(): boolean {
4049
return this._isInitialized;
4150
}
4251

52+
public get userHasConsented(): boolean {
53+
return this._consent;
54+
}
55+
4356
getCookieValue = (name: string) =>
4457
document.cookie.match("(^|;)\\s*" + name + "\\s*=\\s*([^;]+)")?.pop() || "";
4558

46-
init = () => {
47-
this.cookieConsentTest();
48-
setInterval(this.cookieConsentTest, 3000);
59+
constructor(hostname?: string, devHosts?: string[] | string) {
60+
if (Array.isArray(devHosts)) {
61+
this._devHosts = [...devHosts, ...DEFAULT_DEV_HOSTS];
62+
} else if (devHosts === undefined) {
63+
this._devHosts = [...DEFAULT_DEV_HOSTS];
64+
} else {
65+
this._devHosts = [...DEFAULT_DEV_HOSTS, devHosts];
66+
}
4967

50-
//Simulate cookie consent behaviour in non-prod environments
51-
this._devHosts.map(
52-
(devHost) =>
53-
this._hostname.includes(devHost) && this.setDevCookieHandler(),
68+
this._hostname = hostname || window.location.hostname;
69+
70+
this._isDevEnvironment = this._devHosts.some((h) =>
71+
this._hostname.includes(h),
5472
);
55-
};
5673

57-
cookieConsentTest = () => {
58-
if (window.permutive) {
59-
if (!this._isInitialized) {
60-
if (
61-
this.getCookieValue("FTConsent").includes("behaviouraladsOnsite%3Aon")
62-
) {
63-
this.permutiveConsentOn();
74+
loadFtCmpScript()
75+
.then(() => {
76+
this.attachCmpListeners();
77+
78+
const propertyConfig = window.location.hostname.endsWith(".ft.com")
79+
? properties["FT_DOTCOM_PROD"]
80+
: properties["FT_DOTCOM_TEST"];
81+
82+
// initialize CMP
83+
initSourcepointCmp({ propertyConfig });
84+
// use cmp client lib to intercept footer 'Manage Cookies' links (opens privacy modal)
85+
// Note, function requires very specific link: text = 'Manage Cookies' and href = 'https://ft.com/preferences/manage-cookies'
86+
interceptManageCookiesLinks();
87+
this._isInitialized = true;
88+
})
89+
.catch((err) => console.error(err));
90+
}
91+
92+
private attachCmpListeners(): void {
93+
enqueueCmpCallback(() => {
94+
const onReady: ConsentReadyHandler = (_l, _u, _t, info) => {
95+
debug("onConsentReady:", info);
96+
if (info.consentedToAll) {
97+
this.enablePermutive();
6498
} else {
65-
this.permutiveConsentOff();
99+
this.disablePermutive();
66100
}
67-
this._isInitialized = true;
68-
} else if (
69-
this.getCookieValue("FTConsent").includes(
70-
"behaviouraladsOnsite%3Aon",
71-
) &&
72-
!this.consent
73-
) {
74-
debug("setting permutive tracking consent: on");
75-
this.permutiveConsentOn();
76-
} else if (
77-
!this.getCookieValue("FTConsent").includes(
78-
"behaviouraladsOnsite%3Aon",
79-
) &&
80-
this.consent
81-
) {
82-
debug("setting permutive tracking consent: off");
83-
this.permutiveConsentOff();
84-
}
85-
}
86-
};
101+
};
87102

88-
setDevCookieHandler = () => {
89-
this._isDevEnvironment = true;
90-
debug("setting development environment from host match");
91-
const oCookieMessage =
92-
document.getElementsByClassName("o-cookie-message")[0];
93-
if (oCookieMessage) {
94-
const onCookieMessageAct = () => {
95-
debug("setting development FT consent cookies");
96-
document.cookie = "FTConsent=behaviouraladsOnsite%3Aon";
97-
document.cookie = "FTCookieConsentGDPR=true";
98-
oCookieMessage.removeEventListener(
99-
"oCookieMessage.act",
100-
onCookieMessageAct,
101-
false,
103+
const onChoice: MessageChoiceHandler = (_l, _c, typeId) => {
104+
debug("onMessageChoiceSelect:", typeId);
105+
if (typeId === CMP_CHOICE_ACCEPT_ALL) this.enablePermutive();
106+
else if (typeId === CMP_CHOICE_REJECT_ALL) this.disablePermutive();
107+
108+
//Simulate cookie consent behaviour in non-prod environments as banner does not set cookies in non .ft.com domains
109+
this._devHosts.map(
110+
(devHost) =>
111+
this._hostname.includes(devHost) &&
112+
typeId === CMP_CHOICE_ACCEPT_ALL &&
113+
this.setDevConsentCookies(),
102114
);
115+
116+
// banner updated - check new cookie value to fire consent_update event
117+
setTimeout(this.cookieConsentTest, 3000);
103118
};
104-
oCookieMessage.addEventListener("oCookieMessage.act", onCookieMessageAct);
105-
}
106-
};
107119

108-
permutiveConsentOn = () => {
109-
window.permutive.consent({
120+
window._sp_.addEventListener?.("onConsentReady", onReady);
121+
window._sp_.addEventListener?.("onMessageChoiceSelect", onChoice);
122+
});
123+
}
124+
125+
private enablePermutive(): void {
126+
if (this._consent) return;
127+
debug("Permutive consent: ON");
128+
window.permutive?.consent({
110129
opt_in: true,
111130
token: "behaviouraladsOnsite:on",
112131
});
113132
this._consent = true;
114-
};
133+
}
115134

116-
permutiveConsentOff = () => {
117-
window.permutive.consent({ opt_in: false });
135+
private disablePermutive(): void {
136+
if (!this._consent) return;
137+
debug("Permutive consent: OFF");
138+
window.permutive?.consent({ opt_in: false });
118139
this._consent = false;
140+
}
141+
142+
//check for FTConsent - cookiesOnSite to trigger custom consent_update event for GTM tags (banner updated)
143+
cookieConsentTest = () => {
144+
if (!this._isInitialized || !window || !window.dataLayer) return;
145+
if (this.getCookieValue("FTConsent").includes("cookiesOnsite%3Aon")) {
146+
//send consent_update event
147+
window.dataLayer.push({
148+
event: "consent_update",
149+
consent: true,
150+
});
151+
} else {
152+
window.dataLayer.push({
153+
event: "consent_update",
154+
consent: false,
155+
});
156+
}
157+
};
158+
159+
setDevConsentCookies = () => {
160+
this._isDevEnvironment = true;
161+
debug("setting development FT consent cookies");
162+
document.cookie =
163+
"FTConsent=behaviouraladsOnsite%3Aon%2CcookiesOnsite%3Aon%2CpermutiveadsOnsite%3Aon";
164+
document.cookie = "FTCookieConsentGDPR=true";
119165
};
120166
}
167+
168+
export { ConsentMonitor as consentMonitor };

0 commit comments

Comments
 (0)