Skip to content

Commit f0400a6

Browse files
Merge fe632be into 3a93d78
2 parents 3a93d78 + fe632be commit f0400a6

File tree

16 files changed

+533
-72
lines changed

16 files changed

+533
-72
lines changed

Diff for: .changeset/breezy-drinks-live.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@guardian/libs': major
3+
---
4+
5+
Add the ability for consent or pay by adding two new params to cmp.init

Diff for: apps/github-pages/src/components/CmpTest.svelte

+47-4
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
import { cmp, onConsentChange, log } from '@guardian/libs';
44
import { onMount } from 'svelte';
55
6+
let useNonAdvertisedList = window.location.search.includes('CMP_MAIN');
7+
let isUserSignedIn = window.location.search.includes('CMP_SIGNED_IN');
8+
69
switch (window.location.hash) {
710
case '#tcfv2':
811
localStorage.setItem('framework', JSON.stringify('tcfv2'));
@@ -52,6 +55,29 @@
5255
window.location.reload();
5356
};
5457
58+
const toggleQueryParams = (param) => {
59+
let queryParams = new URLSearchParams(window.location.search);
60+
queryParams.has(param)
61+
? queryParams.delete(param)
62+
: queryParams.append(param, '');
63+
window.location.search = queryParams.toString();
64+
};
65+
66+
const toggleIsFeatureFlagEnabled = () => {
67+
isFeatureFlagEnabled = !isFeatureFlagEnabled;
68+
toggleQueryParams('CMP_COP');
69+
};
70+
71+
const toggleIsUserSignedIn = () => {
72+
isUserSignedIn = !isUserSignedIn;
73+
toggleQueryParams('CMP_SIGNED_IN');
74+
};
75+
76+
const toggleUseNonAdvertisedList = () => {
77+
useNonAdvertisedList = !useNonAdvertisedList;
78+
toggleQueryParams('CMP_MAIN');
79+
};
80+
5581
let framework = JSON.parse(localStorage.getItem('framework'));
5682
5783
let setLocation = () => {
@@ -91,10 +117,11 @@
91117
}
92118
93119
// do this loads to make sure that doesn't break things
94-
cmp.init({ country });
95-
cmp.init({ country });
96-
cmp.init({ country });
97-
cmp.init({ country });
120+
cmp.init({
121+
country,
122+
isUserSignedIn: isUserSignedIn ?? false,
123+
useNonAdvertisedList: useNonAdvertisedList ?? false,
124+
});
98125
});
99126
</script>
100127
@@ -133,6 +160,22 @@
133160
in Australia:
134161
<strong>CCPA-like</strong>
135162
</label>
163+
<label class={useNonAdvertisedList ? 'selected' : 'none'}>
164+
<input
165+
type="checkbox"
166+
on:change={toggleUseNonAdvertisedList}
167+
checked={useNonAdvertisedList}
168+
/>
169+
<strong>useNonAdvertisedList?</strong>
170+
</label>
171+
<label class={isUserSignedIn ? 'selected' : 'none'}>
172+
<input
173+
type="checkbox"
174+
on:change={toggleIsUserSignedIn}
175+
checked={isUserSignedIn}
176+
/>
177+
<strong>isUserSignedIn?</strong>
178+
</label>
136179
</nav>
137180
138181
<div id="consent-state">

Diff for: libs/@guardian/libs/src/consent-management-platform/cmp.ts

+22-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
import type { CountryCode } from '../index.test';
12
import { getCurrentFramework } from './getCurrentFramework';
3+
import { getIsConsentOrPay } from './isConsentOrPay';
24
import { mark } from './lib/mark';
35
import {
46
PRIVACY_MANAGER_AUSTRALIA,
57
PRIVACY_MANAGER_TCFV2,
8+
PRIVACY_MANAGER_TCFV2_CONSENT_OR_PAY,
69
PRIVACY_MANAGER_USNAT,
710
} from './lib/sourcepointConfig';
811
import {
@@ -16,9 +19,21 @@ import type {
1619
WillShowPrivacyMessage,
1720
} from './types';
1821

19-
const init = (framework: ConsentFramework, pubData?: PubData): void => {
22+
const init = (
23+
framework: ConsentFramework,
24+
countryCode: CountryCode,
25+
isUserSignedIn: boolean,
26+
useNonAdvertisedList: boolean,
27+
pubData?: PubData,
28+
): void => {
2029
mark('cmp-init');
21-
initSourcepoint(framework, pubData);
30+
initSourcepoint(
31+
framework,
32+
countryCode,
33+
isUserSignedIn,
34+
useNonAdvertisedList,
35+
pubData,
36+
);
2237
};
2338

2439
const willShowPrivacyMessage: WillShowPrivacyMessage = () =>
@@ -27,7 +42,11 @@ const willShowPrivacyMessage: WillShowPrivacyMessage = () =>
2742
function showPrivacyManager(): void {
2843
switch (getCurrentFramework()) {
2944
case 'tcfv2':
30-
window._sp_?.gdpr?.loadPrivacyManagerModal?.(PRIVACY_MANAGER_TCFV2);
45+
window._sp_?.gdpr?.loadPrivacyManagerModal?.(
46+
getIsConsentOrPay()
47+
? PRIVACY_MANAGER_TCFV2_CONSENT_OR_PAY
48+
: PRIVACY_MANAGER_TCFV2,
49+
);
3150
break;
3251
case 'usnat':
3352
window._sp_?.usnat?.loadPrivacyManagerModal?.(PRIVACY_MANAGER_USNAT);

Diff for: libs/@guardian/libs/src/consent-management-platform/index.ts

+13-2
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,12 @@ const initialised = new Promise((resolve) => {
3333
resolveInitialised = resolve;
3434
});
3535

36-
const init: InitCMP = ({ pubData, country }) => {
36+
const init: InitCMP = ({
37+
pubData,
38+
country,
39+
isUserSignedIn = false,
40+
useNonAdvertisedList = false,
41+
}) => {
3742
if (isDisabled() || isServerSide) {
3843
return;
3944
}
@@ -61,7 +66,13 @@ const init: InitCMP = ({ pubData, country }) => {
6166

6267
const framework = getFramework(country);
6368

64-
UnifiedCMP.init(framework, pubData ?? {});
69+
UnifiedCMP.init(
70+
framework,
71+
country,
72+
isUserSignedIn,
73+
useNonAdvertisedList,
74+
pubData ?? {},
75+
);
6576

6677
void UnifiedCMP.willShowPrivacyMessage().then((willShowValue) => {
6778
_willShowPrivacyMessage = willShowValue;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { isConsentOrPayCountry } from './isConsentOrPay.ts';
2+
3+
describe('isConsentOrPay', () => {
4+
test('should return false country code is FR', () => {
5+
expect(isConsentOrPayCountry('FR')).toBe(false);
6+
});
7+
8+
test('should return true country code is GB', () => {
9+
expect(isConsentOrPayCountry('GB')).toBe(true);
10+
});
11+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import type { CountryCode } from '../index.test';
2+
import { isObject } from '../isObject/isObject';
3+
import { storage } from '../storage/storage';
4+
import { consentOrPayCountries } from './lib/sourcepointConfig';
5+
import type { Participations } from './types';
6+
7+
let isConsentOrPay = false;
8+
9+
export const setIsConsentOrPay = (_isConsentOrPay: boolean) => {
10+
isConsentOrPay = _isConsentOrPay;
11+
};
12+
13+
export const getIsConsentOrPay = (): boolean => {
14+
return isConsentOrPay;
15+
};
16+
17+
export const isConsentOrPayCountry = (countryCode: CountryCode) => {
18+
return consentOrPayCountries.includes(countryCode);
19+
};
20+
21+
export const isInConsentOrPayABTest = (): boolean => {
22+
const participations: Participations = storage.local.get(
23+
'gu.ab.participations',
24+
) as Participations;
25+
return isObject(participations)
26+
? participations.ConsentOrPayBanner?.variant === 'activate'
27+
: false;
28+
};

Diff for: libs/@guardian/libs/src/consent-management-platform/lib/ophan.ts

+6-5
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type {
22
OphanComponentEvent,
33
OphanRecordFunction,
44
} from '../../ophan/@types';
5+
import { getIsConsentOrPay } from '../isConsentOrPay';
56
import { SourcePointChoiceTypes } from './sourcepointConfig';
67

78
export type SourcepointButtonActions =
@@ -11,7 +12,7 @@ export type SourcepointButtonActions =
1112
| 'dismiss'
1213
| undefined;
1314

14-
export type SourcepointMessageType = 'ACCEPT_REJECT';
15+
export type SourcepointMessageType = 'ACCEPT_REJECT' | 'CONSENT_OR_PAY_BANNER';
1516
const getOphanRecordFunction = (): OphanRecordFunction => {
1617
const record = window.guardian?.ophan?.record;
1718

@@ -22,10 +23,10 @@ const getOphanRecordFunction = (): OphanRecordFunction => {
2223
return () => {};
2324
};
2425

25-
export const constructBannerMessageId = (
26-
messageType: SourcepointMessageType,
27-
messageId: string,
28-
): string => {
26+
export const constructBannerMessageId = (messageId: string): string => {
27+
const messageType: SourcepointMessageType = getIsConsentOrPay()
28+
? 'CONSENT_OR_PAY_BANNER'
29+
: 'ACCEPT_REJECT';
2930
return `${messageType}-${messageId}`;
3031
};
3132

Diff for: libs/@guardian/libs/src/consent-management-platform/lib/sourcepointConfig.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,16 @@ import { isGuardianDomain } from './domain';
22

33
export const ACCOUNT_ID = 1257;
44
export const PRIVACY_MANAGER_USNAT = 1068329;
5-
export const PROPERTY_ID = 7417;
5+
6+
export const PROPERTY_ID_MAIN = 9398; // TO BE CHANGED to 7417
7+
export const PROPERTY_ID_SUBDOMAIN = 38161;
68
export const PROPERTY_ID_AUSTRALIA = 13348;
9+
10+
export const PROPERTY_HREF_SUBDOMAIN = 'http://subdomain.theguardian.com';
11+
export const PROPERTY_HREF_MAIN = 'http://ui-dev';
12+
713
export const PRIVACY_MANAGER_TCFV2 = 106842;
14+
export const PRIVACY_MANAGER_TCFV2_CONSENT_OR_PAY = 1251121;
815
export const PRIVACY_MANAGER_AUSTRALIA = 1178486;
916

1017
export const ENDPOINT = isGuardianDomain()
@@ -19,3 +26,5 @@ export const SourcePointChoiceTypes = {
1926
RejectAll: 13,
2027
Dismiss: 15,
2128
} as const;
29+
30+
export const consentOrPayCountries = ['GB'];
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import { getCookie } from '../cookies/getCookie';
2+
import {
3+
PROPERTY_ID_MAIN,
4+
PROPERTY_ID_SUBDOMAIN,
5+
} from './lib/sourcepointConfig';
6+
import { postCustomConsent } from './tcfv2/api';
7+
import type { SPUserConsent } from './types/tcfv2';
8+
9+
const purposeIdToNonAdvertisingPurposesMap = new Map<number, string>([
10+
[1, '677e3387b265dc07332909da'], // 1
11+
[2, '677e3386b265dc073328f686'], //2
12+
[3, '677e3386b265dc073328f96f'], // 3
13+
[4, '677e3386b265dc073328fc01'], //4
14+
[5, '677e3386b265dc073328fe72'], // 5
15+
[6, '677e3386b265dc073328ff70'], //6
16+
[7, '677e3386b265dc0733290050'], //7
17+
[8, '677e3386b265dc07332903c8'], //8
18+
[9, '677e3386b265dc0733290502'], //9
19+
[10, '677e3386b265dc073329071a'], //10
20+
[11, '677e3386b265dc07332909c2'], //11
21+
]);
22+
23+
const spBaseUrl = 'https://cdn.privacy-mgmt.com/consent/tcfv2/consent/v3';
24+
interface Vendors {
25+
_id: string;
26+
name: string;
27+
vendorType: string;
28+
googleId?: string;
29+
}
30+
31+
interface LegIntCategories {
32+
_id: string;
33+
name: string;
34+
iabPurposeRef: {
35+
iabId: number;
36+
name: string;
37+
};
38+
}
39+
interface UserConsentStatus {
40+
vendors: Vendors[];
41+
legIntCategories: LegIntCategories[];
42+
legIntVendors: Vendors[];
43+
categories: LegIntCategories[];
44+
}
45+
46+
/**
47+
* THis function gets the user's consent for the larger gdpr vendor list
48+
* then calls postUserConsent
49+
* https://sourcepoint-public-api.readme.io/reference/get_consent-v3-history-siteid-1
50+
*/
51+
export const mergeVendorList = async (): Promise<void> => {
52+
const userConsent = await getUserConsentOnAdvertisingList();
53+
await postUserConsent(userConsent);
54+
await mergeUserConsent();
55+
window.location.reload();
56+
};
57+
58+
/**
59+
*
60+
*
61+
* @return {*} {Promise<UserConsentStatus[]>}
62+
*/
63+
const getUserConsentOnAdvertisingList = async (): Promise<
64+
UserConsentStatus[]
65+
> => {
66+
const consentUUID = getCookie({ name: 'consentUUID' });
67+
const url = `${spBaseUrl}/history/${PROPERTY_ID_MAIN}?consentUUID=${consentUUID}`;
68+
69+
const getUserConsentOnAdvertisingListResponse = await fetch(url, {
70+
method: 'GET',
71+
headers: {
72+
Accept: 'application/json',
73+
'Content-Type': 'application/json',
74+
},
75+
});
76+
77+
return (await getUserConsentOnAdvertisingListResponse.json()) as UserConsentStatus[];
78+
};
79+
80+
/**
81+
*
82+
*
83+
* @param {UserConsentStatus[]} data
84+
*/
85+
const postUserConsent = async (data: UserConsentStatus[]): Promise<void> => {
86+
const vendorIds = data[0]?.vendors.map((vendor) => vendor._id);
87+
let purposeIds = data[0]?.categories.map(
88+
(category) =>
89+
purposeIdToNonAdvertisingPurposesMap.get(category.iabPurposeRef.iabId) ??
90+
'',
91+
);
92+
let legitimateInterestPurposeIds = data[0]?.legIntCategories.map(
93+
(category) =>
94+
purposeIdToNonAdvertisingPurposesMap.get(category.iabPurposeRef.iabId) ??
95+
'',
96+
);
97+
98+
purposeIds = purposeIds?.filter((id) => id !== '');
99+
legitimateInterestPurposeIds = legitimateInterestPurposeIds?.filter(
100+
(id) => id !== '',
101+
);
102+
103+
if (vendorIds && purposeIds && legitimateInterestPurposeIds) {
104+
await postCustomConsent(
105+
vendorIds,
106+
purposeIds,
107+
legitimateInterestPurposeIds,
108+
);
109+
}
110+
};
111+
112+
/**
113+
* This function merges the main vendor list with the sub-domain user consent status
114+
* https://sourcepoint-public-api.readme.io/reference/post_consent-v3-siteid-tcstring
115+
*/
116+
const mergeUserConsent = async (): Promise<void> => {
117+
const consentUUID = getCookie({ name: 'consentUUID' });
118+
const url = `${spBaseUrl}/${PROPERTY_ID_SUBDOMAIN}/tcstring?consentUUID=${consentUUID}`;
119+
const spUserConsentString = localStorage.getItem(
120+
`_sp_user_consent_${PROPERTY_ID_MAIN}`,
121+
);
122+
const userConsent = JSON.parse(spUserConsentString ?? '{}') as SPUserConsent;
123+
124+
await fetch(url, {
125+
method: 'POST',
126+
headers: {
127+
Accept: 'application/json',
128+
'Content-Type': 'application/json',
129+
},
130+
body: JSON.stringify({
131+
euconsent: userConsent.gdpr?.euconsent,
132+
}),
133+
});
134+
};

0 commit comments

Comments
 (0)