diff --git a/.changeset/breezy-drinks-live.md b/.changeset/breezy-drinks-live.md
new file mode 100644
index 000000000..cdd8d06f3
--- /dev/null
+++ b/.changeset/breezy-drinks-live.md
@@ -0,0 +1,9 @@
+---
+'@guardian/libs': major
+---
+
+- Add the ability for consent or pay by adding two new params, useNonAdvertisingList and isUsedSignedIn to cmp.init
+- Using Sourcepoint Subdomain property if useNonAdvertisedList is true
+- Merging user consent from Advertising to Non-Advertising vendor list
+- Redirecting user to Support when clicking Reject for Consent or Pay users
+- Extending \_\_tcfapi to postCustomConsent as part of merging user consent process.
diff --git a/apps/github-pages/src/components/CmpTest.svelte b/apps/github-pages/src/components/CmpTest.svelte
index 2186fce56..d8a065b7c 100644
--- a/apps/github-pages/src/components/CmpTest.svelte
+++ b/apps/github-pages/src/components/CmpTest.svelte
@@ -3,6 +3,9 @@
import { cmp, onConsentChange, log } from '@guardian/libs';
import { onMount } from 'svelte';
+ let useNonAdvertisedList = window.location.search.includes('NON_ADV');
+ let isUserSignedIn = window.location.search.includes('SIGNED_IN');
+
switch (window.location.hash) {
case '#tcfv2':
localStorage.setItem('framework', JSON.stringify('tcfv2'));
@@ -37,11 +40,22 @@
log('cmp', event);
}
+ let setABTest = () => {
+ localStorage.setItem('gu.ab.participations', JSON.stringify({
+ value: {
+ ConsentOrPayBanner: {
+ variant: 'activate',
+ },
+ }
+ }));
+ }
+
let clearPreferences = () => {
// clear local storage
// https://documentation.sourcepoint.com/web-implementation/general/cookies-and-local-storage#cmp-local-storage
localStorage.clear();
+ setABTest();
// clear cookies
// https://documentation.sourcepoint.com/web-implementation/general/cookies-and-local-storage#cmp-cookies
document.cookie.split(';').forEach((cookie) => {
@@ -52,6 +66,29 @@
window.location.reload();
};
+ const toggleQueryParams = (param) => {
+ let queryParams = new URLSearchParams(window.location.search);
+ queryParams.has(param)
+ ? queryParams.delete(param)
+ : queryParams.append(param, '');
+ window.location.search = queryParams.toString();
+ };
+
+ const toggleIsFeatureFlagEnabled = () => {
+ isFeatureFlagEnabled = !isFeatureFlagEnabled;
+ toggleQueryParams('CMP_COP');
+ };
+
+ const toggleIsUserSignedIn = () => {
+ isUserSignedIn = !isUserSignedIn;
+ toggleQueryParams('SIGNED_IN');
+ };
+
+ const toggleUseNonAdvertisedList = () => {
+ useNonAdvertisedList = !useNonAdvertisedList;
+ toggleQueryParams('NON_ADV');
+ };
+
let framework = JSON.parse(localStorage.getItem('framework'));
let setLocation = () => {
@@ -90,11 +127,11 @@
break;
}
- // do this loads to make sure that doesn't break things
- cmp.init({ country });
- cmp.init({ country });
- cmp.init({ country });
- cmp.init({ country });
+ cmp.init({
+ country,
+ isUserSignedIn: isUserSignedIn,
+ useNonAdvertisedList: useNonAdvertisedList,
+ });
});
@@ -104,6 +141,7 @@
>open privacy manager
+
+
+
diff --git a/libs/@guardian/libs/playwright/e2e/sourcepoint-tcfv2.spec.js.disabled b/libs/@guardian/libs/playwright/e2e/sourcepoint-tcfv2.spec.js.disabled
index 90008aad0..41048f942 100644
--- a/libs/@guardian/libs/playwright/e2e/sourcepoint-tcfv2.spec.js.disabled
+++ b/libs/@guardian/libs/playwright/e2e/sourcepoint-tcfv2.spec.js.disabled
@@ -3,9 +3,15 @@ import { expect, test } from '@playwright/test';
import { ACCOUNT_ID, ENDPOINT } from '../fixtures/sourcepointConfig';
const iframeMessage = `[id^="sp_message_iframe_"]`;
-const iframePrivacyManager = '#sp_message_iframe_106842';
+// const iframePrivacyManager = '#sp_message_iframe_106842';
+const iframePrivacyManager = '#sp_message_iframe_1251121';
+
+const acceptAllButton = 'sp_choice_type_11';
+const rejectAllButton = 'sp_choice_type_13';
const url = `http://localhost:4321/csnx/cmp-test-page#tcfv2`;
+const nonAdvertisingBannerUrl = `http://localhost:4321/csnx/cmp-test-page?CMP_MAIN#tcfv2`
+
async function getIframeBody(page, selector) {
const iframeElement = await page.locator(`iframe${selector}`).elementHandle();
@@ -59,120 +65,201 @@ test.describe('Document', () => {
});
});
test.describe('Interaction', () => {
- const buttonTitle = 'Yes, I accept';
- test(`should give all consents when clicking "${buttonTitle}"`, async ({
- page,
- }) => {
- await page.goto(url);
- await page.context().addCookies([
- { name: 'ccpaApplies', value: 'false', url },
- { name: 'gdprApplies', value: 'true', url },
- ]);
+ test.describe('Consent or Pay banner', () => {
+ test(`should give all consents when clicking accept all`, async ({
+ page,
+ }) => {
+ await page.goto(url);
+ await page.context().addCookies([
+ { name: 'ccpaApplies', value: 'false', url },
+ { name: 'gdprApplies', value: 'true', url },
+ ]);
+
+ const iframe = await getIframeBody(page, iframeMessage);
+ await iframe.click(`button.${acceptAllButton}`);
+
+ await page.waitForTimeout(2000);
+
+ for (const purpose of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]) {
+ const consent = await page.getAttribute(
+ `[data-purpose="${purpose}"]`,
+ 'data-consent',
+ );
+ expect(consent).toBe('true');
+ }
+ });
+
+ test(`should have no consents when clicking reject all`, async ({
+ page,
+ }) => {
+ await page.goto(url);
+ await page.context().addCookies([
+ { name: 'ccpaApplies', value: 'false', url },
+ { name: 'gdprApplies', value: 'true', url },
+ ]);
+
+ const iframe = await getIframeBody(page, iframeMessage);
+ await iframe.click(`button.${rejectAllButton}`);
+
+ await page.waitForTimeout(2000);
+
+ for (const purpose of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]) {
+ const consent = await page.getAttribute(
+ `[data-purpose="${purpose}"]`,
+ 'data-consent',
+ );
+ expect(consent).toBe('false');
+ }
+ });
+
+ test(`should deactivate purpose 5, 6, 8, 11 if rejecting 'Personalised content and content measurement' in privacy manager`, async ({ page }) => {
+ await page.goto(url);
+ await page.context().addCookies([
+ { name: 'ccpaApplies', value: 'false', url },
+ { name: 'gdprApplies', value: 'true', url },
+ ]);
- const iframe = await getIframeBody(page, iframeMessage);
- await iframe.click(`button[title="${buttonTitle}"]`);
+ const iframe = await getIframeBody(page, iframeMessage);
+ await iframe.click(`button.${acceptAllButton}`);
- await page.waitForTimeout(2000);
+ await page.waitForTimeout(2000);
- for (const purpose of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]) {
- const consent = await page.getAttribute(
- `[data-purpose="${purpose}"]`,
- 'data-consent',
+ await page.click('[data-cy=pm]');
+
+ const privacyManagerIframe = await getIframeBody(
+ page,
+ iframePrivacyManager,
);
- expect(consent).toBe('true');
- }
- });
+ await privacyManagerIframe.locator('div.pur-buttons-container').nth(2).locator('button').nth(1).click();
+ await privacyManagerIframe.click('button[title="Save and close"]');
- test(`should deactivate purpose 1 only`, async ({ page }) => {
- await page.goto(url);
- await page.context().addCookies([
- { name: 'ccpaApplies', value: 'false', url },
- { name: 'gdprApplies', value: 'true', url },
- ]);
+ await page.waitForTimeout(2000);
- const iframe = await getIframeBody(page, iframeMessage);
- await iframe.click(`button[title="${buttonTitle}"]`);
+ for (const purpose of [1, 2, 3, 4, 7, 9, 10]) {
+ const consent = await page.getAttribute(
+ `[data-purpose="${purpose}"]`,
+ 'data-consent',
+ );
+ expect(consent).toBe('true');
+ }
- await page.waitForTimeout(2000);
+ for (const purpose of [5, 6, 8, 11]) {
+ const consent = await page.getAttribute(
+ `[data-purpose="${purpose}"]`,
+ 'data-consent',
+ );
+ expect(consent).toBe('false');
+ }
+ });
- await page.click('[data-cy=pm]');
+ })
- const privacyManagerIframe = await getIframeBody(
+ test.describe('Non Advertising Banner', () => {
+ test(`should give all consents when clicking accept all`, async ({
page,
- iframePrivacyManager,
- );
- await privacyManagerIframe.click(
- 'div[title="Store and/or access information on a device"] span.off',
- );
- await privacyManagerIframe.click('button[title="Save and close"]');
+ }) => {
+ await page.goto(nonAdvertisingBannerUrl);
+ await page.context().addCookies([
+ { name: 'ccpaApplies', value: 'false', url: nonAdvertisingBannerUrl },
+ { name: 'gdprApplies', value: 'true', url: nonAdvertisingBannerUrl },
+ ]);
- await page.waitForFunction(
- () =>
- document
- .querySelector(`[data-purpose="1"]`)
- .getAttribute('data-consent') === 'false',
- );
+ const iframe = await getIframeBody(page, iframeMessage);
+ await iframe.click(`button.${acceptAllButton}`);
- const consent = await page.getAttribute(
- `[data-purpose="1"]`,
- 'data-consent',
- );
- expect(consent).toBe('false');
+ await page.waitForTimeout(2000);
- for (const purpose of [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]) {
- const consent = await page.getAttribute(
- `[data-purpose="${purpose}"]`,
- 'data-consent',
- );
- expect(consent).toBe('true');
- }
- });
+ for (const purpose of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) {
+ const consent = await page.getAttribute(
+ `[data-purpose="${purpose}"]`,
+ 'data-consent',
+ );
+ expect(consent).toBe('true');
+ }
+ });
- test(`should deactivate all purposes except purpose 1`, async ({ page }) => {
- await page.goto(url);
- await page.context().addCookies([
- { name: 'ccpaApplies', value: 'false', url },
- { name: 'gdprApplies', value: 'true', url },
- ]);
+ test(`should have no consents when clicking reject all`, async ({
+ page,
+ }) => {
+ await page.goto(nonAdvertisingBannerUrl);
+ await page.context().addCookies([
+ { name: 'ccpaApplies', value: 'false', url: nonAdvertisingBannerUrl },
+ { name: 'gdprApplies', value: 'true', url: nonAdvertisingBannerUrl },
+ ]);
- const iframe = await getIframeBody(page, iframeMessage);
- await iframe.click(`button[title="${buttonTitle}"]`);
+ const iframe = await getIframeBody(page, iframeMessage);
+ await iframe.click(`button.${rejectAllButton}`);
- await page.click('[data-cy=pm]');
+ await page.waitForTimeout(2000);
+
+ for (const purpose of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]) {
+ const consent = await page.getAttribute(
+ `[data-purpose="${purpose}"]`,
+ 'data-consent',
+ );
+ expect(consent).toBe('false');
+ }
+ });
+ })
- const privacyManagerIframe = await getIframeBody(
+ test.describe('Moving from Consent or Pay to Non Advertising Banner', () => {
+ test(`should give all consents in non advertised banner when clicking accept all in Consent or Pay`, async ({
page,
- iframePrivacyManager,
- );
- await privacyManagerIframe.click(
- 'div[title="Store and/or access information on a device"] span.on',
- );
- await page
- .frameLocator('iframe[title="SP Consent Message"]')
- .getByRole('button', { name: 'Off' })
- .click();
- await privacyManagerIframe.click('button[title="Save and close"]');
-
- await page.waitForFunction(
- () =>
- document
- .querySelector(`[data-purpose="2"]`)
- .getAttribute('data-consent') === 'false',
- );
+ }) => {
+ await page.goto(url);
+ await page.context().addCookies([
+ { name: 'ccpaApplies', value: 'false', url },
+ { name: 'gdprApplies', value: 'true', url },
+ ]);
- const consent = await page.getAttribute(
- `[data-purpose="1"]`,
- 'data-consent',
- );
- expect(consent).toBe('true');
+ const iframe = await getIframeBody(page, iframeMessage);
+ await iframe.click(`button.${acceptAllButton}`);
- for (const purpose of [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]) {
- const consent = await page.getAttribute(
- `[data-purpose="${purpose}"]`,
- 'data-consent',
- );
- expect(consent).toBe('false');
- }
+ await page.waitForTimeout(2000);
+
+ await page.goto(nonAdvertisingBannerUrl);
+
+ await page.waitForTimeout(2000);
+
+ for (const purpose of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]) {
+ const consent = await page.getAttribute(
+ `[data-purpose="${purpose}"]`,
+ 'data-consent',
+ );
+ expect(consent).toBe('true');
+ }
+
+ });
+
+ test(`should give no consents in non advertised banner when clicking reject all in Consent or Pay`, async ({
+ page,
+ }) => {
+ await page.goto(url);
+ await page.context().addCookies([
+ { name: 'ccpaApplies', value: 'false', url },
+ { name: 'gdprApplies', value: 'true', url },
+ ]);
+
+ const iframe = await getIframeBody(page, iframeMessage);
+ await iframe.click(`button.${rejectAllButton}`);
+
+ await page.waitForTimeout(2000);
+
+ await page.goto(nonAdvertisingBannerUrl);
+
+ await page.waitForTimeout(2000);
+
+ for (const purpose of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]) {
+ const consent = await page.getAttribute(
+ `[data-purpose="${purpose}"]`,
+ 'data-consent',
+ );
+ expect(consent).toBe('false');
+ }
+
+ });
});
+
+
});
diff --git a/libs/@guardian/libs/src/consent-management-platform/cmp.ts b/libs/@guardian/libs/src/consent-management-platform/cmp.ts
index 2b4faba55..7f9cbb1f6 100644
--- a/libs/@guardian/libs/src/consent-management-platform/cmp.ts
+++ b/libs/@guardian/libs/src/consent-management-platform/cmp.ts
@@ -1,8 +1,11 @@
+import type { CountryCode } from '../index.test';
import { getCurrentFramework } from './getCurrentFramework';
+import { getIsConsentOrPay } from './isConsentOrPay';
import { mark } from './lib/mark';
import {
PRIVACY_MANAGER_AUSTRALIA,
PRIVACY_MANAGER_TCFV2,
+ PRIVACY_MANAGER_TCFV2_CONSENT_OR_PAY,
PRIVACY_MANAGER_USNAT,
} from './lib/sourcepointConfig';
import {
@@ -16,9 +19,21 @@ import type {
WillShowPrivacyMessage,
} from './types';
-const init = (framework: ConsentFramework, pubData?: PubData): void => {
+const init = (
+ framework: ConsentFramework,
+ countryCode: CountryCode,
+ isUserSignedIn: boolean,
+ useNonAdvertisedList: boolean,
+ pubData?: PubData,
+): void => {
mark('cmp-init');
- initSourcepoint(framework, pubData);
+ initSourcepoint(
+ framework,
+ countryCode,
+ isUserSignedIn,
+ useNonAdvertisedList,
+ pubData,
+ );
};
const willShowPrivacyMessage: WillShowPrivacyMessage = () =>
@@ -27,7 +42,11 @@ const willShowPrivacyMessage: WillShowPrivacyMessage = () =>
function showPrivacyManager(): void {
switch (getCurrentFramework()) {
case 'tcfv2':
- window._sp_?.gdpr?.loadPrivacyManagerModal?.(PRIVACY_MANAGER_TCFV2);
+ window._sp_?.gdpr?.loadPrivacyManagerModal?.(
+ getIsConsentOrPay()
+ ? PRIVACY_MANAGER_TCFV2_CONSENT_OR_PAY
+ : PRIVACY_MANAGER_TCFV2,
+ );
break;
case 'usnat':
window._sp_?.usnat?.loadPrivacyManagerModal?.(PRIVACY_MANAGER_USNAT);
diff --git a/libs/@guardian/libs/src/consent-management-platform/index.ts b/libs/@guardian/libs/src/consent-management-platform/index.ts
index 96c5eab68..4da6f1c6f 100644
--- a/libs/@guardian/libs/src/consent-management-platform/index.ts
+++ b/libs/@guardian/libs/src/consent-management-platform/index.ts
@@ -33,7 +33,12 @@ const initialised = new Promise((resolve) => {
resolveInitialised = resolve;
});
-const init: InitCMP = ({ pubData, country }) => {
+const init: InitCMP = ({
+ pubData,
+ country,
+ isUserSignedIn = false,
+ useNonAdvertisedList = false,
+}) => {
if (isDisabled() || isServerSide) {
return;
}
@@ -61,7 +66,13 @@ const init: InitCMP = ({ pubData, country }) => {
const framework = getFramework(country);
- UnifiedCMP.init(framework, pubData ?? {});
+ UnifiedCMP.init(
+ framework,
+ country,
+ isUserSignedIn,
+ useNonAdvertisedList,
+ pubData ?? {},
+ );
void UnifiedCMP.willShowPrivacyMessage().then((willShowValue) => {
_willShowPrivacyMessage = willShowValue;
diff --git a/libs/@guardian/libs/src/consent-management-platform/isConsentOrPay.test.js b/libs/@guardian/libs/src/consent-management-platform/isConsentOrPay.test.js
new file mode 100644
index 000000000..7182611be
--- /dev/null
+++ b/libs/@guardian/libs/src/consent-management-platform/isConsentOrPay.test.js
@@ -0,0 +1,11 @@
+import { isConsentOrPayCountry } from './isConsentOrPay.ts';
+
+describe('isConsentOrPay', () => {
+ test('should return false country code is FR', () => {
+ expect(isConsentOrPayCountry('FR')).toBe(false);
+ });
+
+ test('should return true country code is GB', () => {
+ expect(isConsentOrPayCountry('GB')).toBe(true);
+ });
+});
diff --git a/libs/@guardian/libs/src/consent-management-platform/isConsentOrPay.ts b/libs/@guardian/libs/src/consent-management-platform/isConsentOrPay.ts
new file mode 100644
index 000000000..4d8974d14
--- /dev/null
+++ b/libs/@guardian/libs/src/consent-management-platform/isConsentOrPay.ts
@@ -0,0 +1,28 @@
+import type { CountryCode } from '../index.test';
+import { isObject } from '../isObject/isObject';
+import { storage } from '../storage/storage';
+import { consentOrPayCountries } from './lib/sourcepointConfig';
+import type { Participations } from './types';
+
+let _isConsentOrPay = false;
+
+export const setIsConsentOrPay = (isConsentOrPay: boolean) => {
+ _isConsentOrPay = isConsentOrPay;
+};
+
+export const getIsConsentOrPay = (): boolean => {
+ return _isConsentOrPay;
+};
+
+export const isConsentOrPayCountry = (countryCode: CountryCode) => {
+ return consentOrPayCountries.includes(countryCode);
+};
+
+export const isInConsentOrPayABTest = (): boolean => {
+ const participations: Participations = storage.local.get(
+ 'gu.ab.participations',
+ ) as Participations;
+ return isObject(participations)
+ ? participations.ConsentOrPayBanner?.variant === 'activate'
+ : false;
+};
diff --git a/libs/@guardian/libs/src/consent-management-platform/lib/ophan.ts b/libs/@guardian/libs/src/consent-management-platform/lib/ophan.ts
index 214e30677..c118955a0 100644
--- a/libs/@guardian/libs/src/consent-management-platform/lib/ophan.ts
+++ b/libs/@guardian/libs/src/consent-management-platform/lib/ophan.ts
@@ -2,6 +2,7 @@ import type {
OphanComponentEvent,
OphanRecordFunction,
} from '../../ophan/@types';
+import { getIsConsentOrPay } from '../isConsentOrPay';
import { SourcePointChoiceTypes } from './sourcepointConfig';
export type SourcepointButtonActions =
@@ -11,7 +12,7 @@ export type SourcepointButtonActions =
| 'dismiss'
| undefined;
-export type SourcepointMessageType = 'ACCEPT_REJECT';
+export type SourcepointMessageType = 'ACCEPT_REJECT' | 'CONSENT_OR_PAY_BANNER';
const getOphanRecordFunction = (): OphanRecordFunction => {
const record = window.guardian?.ophan?.record;
@@ -22,10 +23,10 @@ const getOphanRecordFunction = (): OphanRecordFunction => {
return () => {};
};
-export const constructBannerMessageId = (
- messageType: SourcepointMessageType,
- messageId: string,
-): string => {
+export const constructBannerMessageId = (messageId: string): string => {
+ const messageType: SourcepointMessageType = getIsConsentOrPay()
+ ? 'CONSENT_OR_PAY_BANNER'
+ : 'ACCEPT_REJECT';
return `${messageType}-${messageId}`;
};
diff --git a/libs/@guardian/libs/src/consent-management-platform/lib/sourcepointConfig.ts b/libs/@guardian/libs/src/consent-management-platform/lib/sourcepointConfig.ts
index 4c45db1cf..9926e6de0 100644
--- a/libs/@guardian/libs/src/consent-management-platform/lib/sourcepointConfig.ts
+++ b/libs/@guardian/libs/src/consent-management-platform/lib/sourcepointConfig.ts
@@ -2,9 +2,16 @@ import { isGuardianDomain } from './domain';
export const ACCOUNT_ID = 1257;
export const PRIVACY_MANAGER_USNAT = 1068329;
-export const PROPERTY_ID = 7417;
+
+export const PROPERTY_ID_MAIN = 9398; // TO BE CHANGED to 7417
+export const PROPERTY_ID_SUBDOMAIN = 38161;
export const PROPERTY_ID_AUSTRALIA = 13348;
+
+export const PROPERTY_HREF_SUBDOMAIN = 'http://subdomain.theguardian.com';
+export const PROPERTY_HREF_MAIN = 'http://ui-dev';
+
export const PRIVACY_MANAGER_TCFV2 = 106842;
+export const PRIVACY_MANAGER_TCFV2_CONSENT_OR_PAY = 1251121;
export const PRIVACY_MANAGER_AUSTRALIA = 1178486;
export const ENDPOINT = isGuardianDomain()
@@ -19,3 +26,5 @@ export const SourcePointChoiceTypes = {
RejectAll: 13,
Dismiss: 15,
} as const;
+
+export const consentOrPayCountries = ['GB'];
diff --git a/libs/@guardian/libs/src/consent-management-platform/mergeUserConsent.ts b/libs/@guardian/libs/src/consent-management-platform/mergeUserConsent.ts
new file mode 100644
index 000000000..e0e07b2df
--- /dev/null
+++ b/libs/@guardian/libs/src/consent-management-platform/mergeUserConsent.ts
@@ -0,0 +1,134 @@
+import { getCookie } from '../cookies/getCookie';
+import {
+ PROPERTY_ID_MAIN,
+ PROPERTY_ID_SUBDOMAIN,
+} from './lib/sourcepointConfig';
+import { postCustomConsent } from './tcfv2/api';
+import type { SPUserConsent } from './types/tcfv2';
+
+const purposeIdToNonAdvertisingPurposesMap = new Map([
+ [1, '677e3387b265dc07332909da'], // 1
+ [2, '677e3386b265dc073328f686'], //2
+ [3, '677e3386b265dc073328f96f'], // 3
+ [4, '677e3386b265dc073328fc01'], //4
+ [5, '677e3386b265dc073328fe72'], // 5
+ [6, '677e3386b265dc073328ff70'], //6
+ [7, '677e3386b265dc0733290050'], //7
+ [8, '677e3386b265dc07332903c8'], //8
+ [9, '677e3386b265dc0733290502'], //9
+ [10, '677e3386b265dc073329071a'], //10
+ [11, '677e3386b265dc07332909c2'], //11
+]);
+
+const spBaseUrl = 'https://cdn.privacy-mgmt.com/consent/tcfv2/consent/v3';
+interface Vendors {
+ _id: string;
+ name: string;
+ vendorType: string;
+ googleId?: string;
+}
+
+interface LegIntCategories {
+ _id: string;
+ name: string;
+ iabPurposeRef: {
+ iabId: number;
+ name: string;
+ };
+}
+interface UserConsentStatus {
+ vendors: Vendors[];
+ legIntCategories: LegIntCategories[];
+ legIntVendors: Vendors[];
+ categories: LegIntCategories[];
+}
+
+/**
+ * THis function gets the user's consent for the larger gdpr vendor list
+ * then calls postUserConsent
+ * https://sourcepoint-public-api.readme.io/reference/get_consent-v3-history-siteid-1
+ */
+export const mergeVendorList = async (): Promise => {
+ const userConsent = await getUserConsentOnAdvertisingList();
+ await postUserConsent(userConsent);
+ await mergeUserConsent();
+ window.location.reload();
+};
+
+/**
+ *
+ *
+ * @return {*} {Promise}
+ */
+const getUserConsentOnAdvertisingList = async (): Promise<
+ UserConsentStatus[]
+> => {
+ const consentUUID = getCookie({ name: 'consentUUID' });
+ const url = `${spBaseUrl}/history/${PROPERTY_ID_MAIN}?consentUUID=${consentUUID}`;
+
+ const getUserConsentOnAdvertisingListResponse = await fetch(url, {
+ method: 'GET',
+ headers: {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ return (await getUserConsentOnAdvertisingListResponse.json()) as UserConsentStatus[];
+};
+
+/**
+ *
+ *
+ * @param {UserConsentStatus[]} data
+ */
+const postUserConsent = async (data: UserConsentStatus[]): Promise => {
+ const vendorIds = data[0]?.vendors.map((vendor) => vendor._id);
+ let purposeIds = data[0]?.categories.map(
+ (category) =>
+ purposeIdToNonAdvertisingPurposesMap.get(category.iabPurposeRef.iabId) ??
+ '',
+ );
+ let legitimateInterestPurposeIds = data[0]?.legIntCategories.map(
+ (category) =>
+ purposeIdToNonAdvertisingPurposesMap.get(category.iabPurposeRef.iabId) ??
+ '',
+ );
+
+ purposeIds = purposeIds?.filter((id) => id !== '');
+ legitimateInterestPurposeIds = legitimateInterestPurposeIds?.filter(
+ (id) => id !== '',
+ );
+
+ if (vendorIds && purposeIds && legitimateInterestPurposeIds) {
+ await postCustomConsent(
+ vendorIds,
+ purposeIds,
+ legitimateInterestPurposeIds,
+ );
+ }
+};
+
+/**
+ * This function merges the main vendor list with the sub-domain user consent status
+ * https://sourcepoint-public-api.readme.io/reference/post_consent-v3-siteid-tcstring
+ */
+const mergeUserConsent = async (): Promise => {
+ const consentUUID = getCookie({ name: 'consentUUID' });
+ const url = `${spBaseUrl}/${PROPERTY_ID_SUBDOMAIN}/tcstring?consentUUID=${consentUUID}`;
+ const spUserConsentString = localStorage.getItem(
+ `_sp_user_consent_${PROPERTY_ID_MAIN}`,
+ );
+ const userConsent = JSON.parse(spUserConsentString ?? '{}') as SPUserConsent;
+
+ await fetch(url, {
+ method: 'POST',
+ headers: {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ euconsent: userConsent.gdpr?.euconsent,
+ }),
+ });
+};
diff --git a/libs/@guardian/libs/src/consent-management-platform/sourcepoint.test.js b/libs/@guardian/libs/src/consent-management-platform/sourcepoint.test.js
index 0e50f1976..6cba16f53 100644
--- a/libs/@guardian/libs/src/consent-management-platform/sourcepoint.test.js
+++ b/libs/@guardian/libs/src/consent-management-platform/sourcepoint.test.js
@@ -1,7 +1,12 @@
import { ACCOUNT_ID, ENDPOINT } from './lib/sourcepointConfig.ts';
import { init } from './sourcepoint.ts';
-const frameworks = ['tcfv2', 'usnat', 'aus'];
+// const frameworks = ['tcfv2', 'usnat', 'aus'];
+const frameworksAndCountryCodes = [
+ { framework: 'tcfv2', countryCode: 'FR' },
+ { framework: 'usnat', countryCode: 'US' },
+ { framework: 'aus', countryCode: 'AU' },
+];
describe('Sourcepoint unified', () => {
beforeEach(() => {
@@ -18,10 +23,13 @@ describe('Sourcepoint unified', () => {
expect(init).toThrow();
});
- it.each(frameworks)(
+ it.each(frameworksAndCountryCodes)(
"should initialize window._sp_ with the correct config if it doesn't exist",
- (framework) => {
- init(framework);
+ (frameworkAndCountryCode) => {
+ init(
+ frameworkAndCountryCode.framework,
+ frameworkAndCountryCode.countryCode,
+ );
expect(window._sp_).toBeDefined();
expect(window._sp_.config).toBeDefined();
expect(window._sp_.config.baseEndpoint).toEqual(ENDPOINT);
@@ -32,27 +40,27 @@ describe('Sourcepoint unified', () => {
'function',
);
- if (framework == 'tcfv2') {
+ if (frameworkAndCountryCode.framework == 'tcfv2') {
expect(window._sp_.config.gdpr.targetingParams.framework).toEqual(
- framework,
+ frameworkAndCountryCode.framework,
);
expect(window._sp_.config.usnat).toBeUndefined();
expect(window.__tcfapi).toBeDefined();
expect(window.__uspapi).toBeUndefined();
expect(window.__gpp).toBeUndefined();
- } else if (framework == 'usnat') {
+ } else if (frameworkAndCountryCode.framework == 'usnat') {
expect(window._sp_.config.usnat.includeUspApi).toBeTruthy();
expect(window._sp_.config.usnat.transitionCCPAAuth).toBeTruthy();
expect(window._sp_.config.usnat.targetingParams.framework).toEqual(
- framework,
+ frameworkAndCountryCode.framework,
);
expect(window._sp_.config.gdpr).toBeUndefined;
expect(window.__uspapi).toBeDefined();
expect(window.__tcfapi).toBeUndefined();
expect(window.__gpp).toBeDefined();
- } else if (framework == 'aus') {
+ } else if (frameworkAndCountryCode.framework == 'aus') {
expect(window._sp_.config.ccpa.targetingParams.framework).toEqual(
- framework,
+ frameworkAndCountryCode.framework,
);
expect(window._sp_.config.gdpr).toBeUndefined;
expect(window.__uspapi).toBeDefined();
@@ -62,34 +70,59 @@ describe('Sourcepoint unified', () => {
},
);
- it.each(frameworks)('points at a real file', async (framework) => {
- init(framework);
- expect(document.getElementById('sourcepoint-lib')).toBeTruthy();
- const src = document.getElementById('sourcepoint-lib')?.getAttribute('src');
+ it.each(frameworksAndCountryCodes)(
+ 'points at a real file',
+ async (frameworkAndCountryCode) => {
+ init(
+ frameworkAndCountryCode.framework,
+ frameworkAndCountryCode.countryCode,
+ );
+ expect(document.getElementById('sourcepoint-lib')).toBeTruthy();
+ const src = document
+ .getElementById('sourcepoint-lib')
+ ?.getAttribute('src');
- const response = await fetch(src);
- expect(response.ok).toBe(true);
- });
+ const response = await fetch(src);
+ expect(response.ok).toBe(true);
+ },
+ );
- it.each(frameworks)('should accept pubData', (framework) => {
- const now = new Date().getTime();
- init(framework, {
- browserId: 'abc123',
- pageViewId: 'abcdef',
- cmpInitTimeUtc: 1601511014537,
- });
- expect(window._sp_.config.pubData.browserId).toEqual('abc123');
- expect(window._sp_.config.pubData.pageViewId).toEqual('abcdef');
- expect(window._sp_.config.pubData.cmpInitTimeUtc).toBeGreaterThanOrEqual(
- now,
- );
- });
+ it.each(frameworksAndCountryCodes)(
+ 'should accept pubData',
+ (frameworkAndCountryCode) => {
+ const now = new Date().getTime();
+ init(
+ frameworkAndCountryCode.framework,
+ frameworkAndCountryCode.countryCode,
+ true,
+ true,
+ {
+ browserId: 'abc123',
+ pageViewId: 'abcdef',
+ cmpInitTimeUtc: 1601511014537,
+ },
+ );
+ expect(window._sp_.config.pubData.browserId).toEqual('abc123');
+ expect(window._sp_.config.pubData.pageViewId).toEqual('abcdef');
+ expect(window._sp_.config.pubData.cmpInitTimeUtc).toBeGreaterThanOrEqual(
+ now,
+ );
+ },
+ );
- it.each(frameworks)('should handle no pubData', (framework) => {
- const now = new Date().getTime();
- init(framework);
- expect(window._sp_.config.pubData.cmpInitTimeUtc).toBeGreaterThanOrEqual(
- now,
- );
- });
+ it.each(frameworksAndCountryCodes)(
+ 'should handle no pubData',
+ (frameworkAndCountryCode) => {
+ const now = new Date().getTime();
+ init(
+ frameworkAndCountryCode.framework,
+ frameworkAndCountryCode.countryCode,
+ true,
+ true,
+ );
+ expect(window._sp_.config.pubData.cmpInitTimeUtc).toBeGreaterThanOrEqual(
+ now,
+ );
+ },
+ );
});
diff --git a/libs/@guardian/libs/src/consent-management-platform/sourcepoint.ts b/libs/@guardian/libs/src/consent-management-platform/sourcepoint.ts
index 355e9229a..8197a3132 100644
--- a/libs/@guardian/libs/src/consent-management-platform/sourcepoint.ts
+++ b/libs/@guardian/libs/src/consent-management-platform/sourcepoint.ts
@@ -1,6 +1,12 @@
+import type { CountryCode } from '../index.test';
import { log } from '../logger/logger';
import { isExcludedFromCMP } from './exclusionList';
import { setCurrentFramework } from './getCurrentFramework';
+import {
+ isConsentOrPayCountry,
+ isInConsentOrPayABTest,
+ setIsConsentOrPay,
+} from './isConsentOrPay';
import { isGuardianDomain } from './lib/domain';
import { mark } from './lib/mark';
import {
@@ -12,13 +18,18 @@ import type { Property } from './lib/property';
import {
ACCOUNT_ID,
ENDPOINT,
- PROPERTY_ID,
+ PROPERTY_HREF_MAIN,
+ PROPERTY_HREF_SUBDOMAIN,
PROPERTY_ID_AUSTRALIA,
+ PROPERTY_ID_MAIN,
+ PROPERTY_ID_SUBDOMAIN,
SourcePointChoiceTypes,
} from './lib/sourcepointConfig';
+import { mergeVendorList } from './mergeUserConsent';
import { invokeCallbacks } from './onConsentChange';
import { stub } from './stub';
import type { ConsentFramework } from './types';
+import type { SPUserConsent } from './types/tcfv2';
let resolveWillShowPrivacyMessage: typeof Promise.resolve;
export const willShowPrivacyMessage = new Promise((resolve) => {
@@ -32,21 +43,57 @@ export const willShowPrivacyMessage = new Promise((resolve) => {
* Australia has a single property while the rest of the world has a test and prod property.
* TODO: incorporate au.theguardian into *.theguardian.com
*/
-const getPropertyHref = (framework: ConsentFramework): Property => {
+const getPropertyHref = (
+ framework: ConsentFramework,
+ useNonAdvertisedList: boolean,
+): Property => {
if (framework == 'aus') {
return 'https://au.theguardian.com';
}
- return isGuardianDomain() ? null : 'https://test.theguardian.com';
+
+ return isGuardianDomain()
+ ? null
+ : useNonAdvertisedList
+ ? PROPERTY_HREF_SUBDOMAIN
+ : PROPERTY_HREF_MAIN;
};
-const getPropertyId = (framework: ConsentFramework): number => {
+const getPropertyId = (
+ framework: ConsentFramework,
+ useNonAdvertisedList: boolean,
+): number => {
if (framework == 'aus') {
return PROPERTY_ID_AUSTRALIA;
}
- return PROPERTY_ID;
+
+ if (framework == 'usnat') {
+ return PROPERTY_ID_MAIN;
+ }
+
+ return useNonAdvertisedList ? PROPERTY_ID_SUBDOMAIN : PROPERTY_ID_MAIN;
+};
+
+/**
+ * This function checks the hasConsentData in the localStorage
+ * It returns false if it can't find the key to ensure it doesn't get stuck in a loop
+ *
+ * @return {*} {boolean}
+ */
+const hasConsentedToNonAdvertisedList = (): boolean => {
+ const spUserConsentString = localStorage.getItem(
+ `_sp_user_consent_${PROPERTY_ID_SUBDOMAIN}`,
+ );
+ const userConsent = JSON.parse(spUserConsentString ?? '{}') as SPUserConsent;
+ return userConsent.gdpr?.consentStatus.hasConsentData ?? true;
};
-export const init = (framework: ConsentFramework, pubData = {}): void => {
+export const init = (
+ framework: ConsentFramework,
+ countryCode: CountryCode,
+ isUserSignedIn: boolean,
+ useNonAdvertisedList: boolean,
+ pubData = {},
+): void => {
stub(framework);
// make sure nothing else on the page has accidentally
@@ -57,7 +104,30 @@ export const init = (framework: ConsentFramework, pubData = {}): void => {
setCurrentFramework(framework);
- // invoke callbacks before we receive Sourcepoint events
+ const isCorpABTest = isInConsentOrPayABTest();
+
+ // To esnure users who are not part
+ if (!isCorpABTest) {
+ useNonAdvertisedList = false;
+ }
+
+ console.log('participations', isCorpABTest);
+
+ setIsConsentOrPay(
+ isConsentOrPayCountry(countryCode) && !useNonAdvertisedList,
+ );
+
+ if (
+ isConsentOrPayCountry(countryCode) &&
+ useNonAdvertisedList &&
+ !hasConsentedToNonAdvertisedList()
+ ) {
+ mergeVendorList().catch((error) => {
+ console.log('Failed to merge vendor list', error);
+ });
+ }
+
+ // invoke callbacks before we receive Sourcepoint
invokeCallbacks();
let frameworkMessageType: string;
@@ -91,7 +161,8 @@ export const init = (framework: ConsentFramework, pubData = {}): void => {
config: {
baseEndpoint: ENDPOINT,
accountId: ACCOUNT_ID,
- propertyHref: getPropertyHref(framework),
+ propertyId: getPropertyId(framework, useNonAdvertisedList),
+ propertyHref: getPropertyHref(framework, useNonAdvertisedList),
targetingParams: {
framework,
excludePage: isExcludedFromCMP(pageSection),
@@ -137,7 +208,7 @@ export const init = (framework: ConsentFramework, pubData = {}): void => {
if (data.messageId !== 0) {
messageId = data.messageId;
sendMessageReadyToOphan(
- constructBannerMessageId('ACCEPT_REJECT', messageId.toString()),
+ constructBannerMessageId(messageId.toString()),
);
}
@@ -157,7 +228,7 @@ export const init = (framework: ConsentFramework, pubData = {}): void => {
sendConsentChoicesToOphan(
choiceTypeID,
- constructBannerMessageId('ACCEPT_REJECT', messageId.toString()),
+ constructBannerMessageId(messageId.toString()),
);
// https://documentation.sourcepoint.com/web-implementation/web-implementation/multi-campaign-web-implementation/event-callbacks#choice-type-id-descriptions
@@ -167,6 +238,19 @@ export const init = (framework: ConsentFramework, pubData = {}): void => {
choiceTypeID === SourcePointChoiceTypes.Dismiss
) {
setTimeout(invokeCallbacks, 0);
+
+ if (
+ choiceTypeID === SourcePointChoiceTypes.RejectAll &&
+ message_type === 'gdpr' &&
+ isConsentOrPayCountry(countryCode) &&
+ !useNonAdvertisedList &&
+ isCorpABTest
+ ) {
+ window.open(
+ `https://support.theguardian.com/uk/contribute?redirectUrl=${window.location.href}`,
+ '_blank',
+ );
+ }
}
},
onPrivacyManagerAction: function (message_type, pmData) {
@@ -209,7 +293,10 @@ export const init = (framework: ConsentFramework, pubData = {}): void => {
};
if (isInPropertyIdABTest) {
- window._sp_.config.propertyId = getPropertyId(framework);
+ window._sp_.config.propertyId = getPropertyId(
+ framework,
+ useNonAdvertisedList,
+ );
}
// NOTE - Contrary to the SourcePoint documentation, it's important that we add EITHER gdpr OR ccpa
@@ -224,6 +311,9 @@ export const init = (framework: ConsentFramework, pubData = {}): void => {
targetingParams: {
framework,
excludePage: isExcludedFromCMP(pageSection),
+ isCorP: isConsentOrPayCountry(countryCode),
+ isUserSignedIn,
+ isCorpABTest,
},
};
break;
diff --git a/libs/@guardian/libs/src/consent-management-platform/tcfv2/api.test.js b/libs/@guardian/libs/src/consent-management-platform/tcfv2/api.test.js
index f449d8fdb..2eb18f8b8 100644
--- a/libs/@guardian/libs/src/consent-management-platform/tcfv2/api.test.js
+++ b/libs/@guardian/libs/src/consent-management-platform/tcfv2/api.test.js
@@ -1,8 +1,13 @@
-import { getCustomVendorConsents, getTCData } from './api.ts';
+import {
+ getCustomVendorConsents,
+ getTCData,
+ postCustomConsent,
+} from './api.ts';
it('calls the correct IAB api with the correct methods', async () => {
expect(getTCData()).rejects.toThrow();
expect(getCustomVendorConsents()).rejects.toThrow();
+ expect(postCustomConsent()).rejects.toThrow();
window.__tcfapi = jest.fn((a, b, cb) => {
cb({}, true);
@@ -10,12 +15,20 @@ it('calls the correct IAB api with the correct methods', async () => {
await getTCData();
await getCustomVendorConsents();
+ await postCustomConsent(
+ ['vendor_id'],
+ ['purpose_id'],
+ ['leg_int_purpose_id'],
+ );
expect(window.__tcfapi).toHaveBeenNthCalledWith(
1,
'addEventListener',
expect.any(Number),
expect.any(Function),
+ undefined,
+ undefined,
+ undefined,
);
expect(window.__tcfapi).toHaveBeenNthCalledWith(
@@ -23,5 +36,18 @@ it('calls the correct IAB api with the correct methods', async () => {
'getCustomVendorConsents',
expect.any(Number),
expect.any(Function),
+ undefined,
+ undefined,
+ undefined,
+ );
+
+ expect(window.__tcfapi).toHaveBeenNthCalledWith(
+ 3,
+ 'postCustomConsent',
+ expect.any(Number),
+ expect.any(Function),
+ ['vendor_id'],
+ ['purpose_id'],
+ ['leg_int_purpose_id'],
);
});
diff --git a/libs/@guardian/libs/src/consent-management-platform/tcfv2/api.ts b/libs/@guardian/libs/src/consent-management-platform/tcfv2/api.ts
index 0b8d8c996..fd2e306e9 100644
--- a/libs/@guardian/libs/src/consent-management-platform/tcfv2/api.ts
+++ b/libs/@guardian/libs/src/consent-management-platform/tcfv2/api.ts
@@ -6,16 +6,28 @@ type Command =
| 'ping'
| 'addEventListener'
| 'removeEventListener'
+ | 'postCustomConsent'
| 'getCustomVendorConsents'; // Sourcepoint addition https://documentation.sourcepoint.com/web-implementation/sourcepoint-gdpr-and-tcf-v2-support/__tcfapi-getcustomvendorconsents-api
-const api = (command: Command) =>
+const api = (
+ command: Command,
+ vendorIds?: string[],
+ purposeIds?: string[],
+ legitimateInterestPurposeIds?: string[],
+) =>
new Promise((resolve, reject) => {
if (window.__tcfapi) {
- window.__tcfapi(command, 2, (result, success) =>
- success
- ? resolve(result)
- : /* istanbul ignore next */
- reject(new Error(`Unable to get ${command} data`)),
+ window.__tcfapi(
+ command,
+ 2,
+ (result, success) =>
+ success
+ ? resolve(result)
+ : /* istanbul ignore next */
+ reject(new Error(`Unable to get ${command} data`)),
+ vendorIds,
+ purposeIds,
+ legitimateInterestPurposeIds,
);
} else {
reject(new Error('No __tcfapi found on window'));
@@ -33,3 +45,15 @@ export const getTCData = (): Promise =>
export const getCustomVendorConsents = (): Promise =>
api('getCustomVendorConsents') as Promise;
+
+export const postCustomConsent = (
+ vendorIds: string[],
+ purposeIds: string[],
+ legitimateInterestPurposeIds: string[],
+): Promise =>
+ api(
+ 'postCustomConsent',
+ vendorIds,
+ purposeIds,
+ legitimateInterestPurposeIds,
+ ) as Promise;
diff --git a/libs/@guardian/libs/src/consent-management-platform/types/index.ts b/libs/@guardian/libs/src/consent-management-platform/types/index.ts
index 0b5bbb11c..941f71c52 100644
--- a/libs/@guardian/libs/src/consent-management-platform/types/index.ts
+++ b/libs/@guardian/libs/src/consent-management-platform/types/index.ts
@@ -21,6 +21,8 @@ export type CMP = {
export type InitCMP = (arg0: {
pubData?: PubData;
country?: CountryCode;
+ isUserSignedIn?: boolean;
+ useNonAdvertisedList?: boolean;
}) => void;
export type OnConsentChange = (
@@ -46,7 +48,13 @@ export interface PubData {
[propName: string]: unknown;
}
export interface SourcepointImplementation {
- init: (framework: ConsentFramework, pubData?: PubData) => void;
+ init: (
+ framework: ConsentFramework,
+ countryCode: CountryCode,
+ isUserSignedIn: boolean,
+ useNonAdvertisedList: boolean,
+ pubData?: PubData,
+ ) => void;
willShowPrivacyMessage: WillShowPrivacyMessage;
showPrivacyManager: () => void;
}
@@ -70,3 +78,5 @@ export interface VendorConsents {
}
export type { VendorName };
+
+export type Participations = Record;
diff --git a/libs/@guardian/libs/src/consent-management-platform/types/tcfv2/index.ts b/libs/@guardian/libs/src/consent-management-platform/types/tcfv2/index.ts
index efb8ed173..7b3692f77 100644
--- a/libs/@guardian/libs/src/consent-management-platform/types/tcfv2/index.ts
+++ b/libs/@guardian/libs/src/consent-management-platform/types/tcfv2/index.ts
@@ -26,3 +26,12 @@ export type TCPingStatusCode =
| 'visible'
| 'hidden'
| 'disabled';
+
+export interface SPUserConsent {
+ gdpr?: {
+ euconsent?: string;
+ consentStatus: {
+ hasConsentData: boolean;
+ };
+ };
+}
diff --git a/libs/@guardian/libs/src/consent-management-platform/types/window.d.ts b/libs/@guardian/libs/src/consent-management-platform/types/window.d.ts
index dd43b4b6a..178908812 100644
--- a/libs/@guardian/libs/src/consent-management-platform/types/window.d.ts
+++ b/libs/@guardian/libs/src/consent-management-platform/types/window.d.ts
@@ -34,6 +34,7 @@ declare global {
accountId: number;
propertyHref?: Property;
propertyId?: number;
+ campaignEnv?: 'prod' | 'stage';
targetingParams: {
framework: ConsentFramework;
excludePage: boolean;
@@ -47,6 +48,9 @@ declare global {
targetingParams?: {
framework: ConsentFramework;
excludePage: boolean;
+ isCorP: boolean;
+ isUserSignedIn: boolean;
+ isCorpABTest: boolean;
};
};
usnat?: {
@@ -107,7 +111,9 @@ declare global {
command: string,
version: number,
callback: (tcData: TCData, success: boolean) => void,
- vendorIDs?: number[],
+ vendorIDs?: string[],
+ purposeIds?: string[],
+ legitimateInterestPurposeIds?: string[],
) => void;
__gpp?: (
command: string,