Skip to content

Commit

Permalink
Merge 6319e06 into cb19d46
Browse files Browse the repository at this point in the history
  • Loading branch information
akinsola-guardian authored Sep 17, 2024
2 parents cb19d46 + 6319e06 commit fb2f818
Show file tree
Hide file tree
Showing 41 changed files with 531 additions and 306 deletions.
14 changes: 14 additions & 0 deletions .changeset/slimy-lobsters-jump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
'@guardian/libs': major
---

CMP: Implement Multi-State Privacy Agreement for US Compliance

This release introduces support for the Global Privacy Platform (GPP) for third-party vendors who have completed migration. The legacy \*\*uspapi will remain available temporarily for vendors still in transition.

Key updates:

Added window.__gpp stub function for the US region.
Updated the US framework to use "usnat" instead of "ccpa".
Migrated CCPA-related types and functions to the "aus" namespace.
The ConsentState type, returned by getConsentFor, onConsentChange, and onConsent, now includes a usnat property, replacing the previous ccpa property.
20 changes: 10 additions & 10 deletions apps/github-pages/src/components/CmpTest.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
case '#tcfv2':
localStorage.setItem('framework', JSON.stringify('tcfv2'));
break;
case '#ccpa':
localStorage.setItem('framework', JSON.stringify('ccpa'));
case '#usnat':
localStorage.setItem('framework', JSON.stringify('usnat'));
break;
case '#aus':
localStorage.setItem('framework', JSON.stringify('aus'));
Expand Down Expand Up @@ -81,7 +81,7 @@
country = 'GB';
break;
case 'ccpa':
case 'usnat':
country = 'US';
break;
Expand Down Expand Up @@ -113,15 +113,15 @@
/>
in RoW:<strong>TCFv2</strong>
</label>
<label class={framework == 'ccpa' ? 'selected' : 'none'}>
<label class={framework == 'usnat' ? 'selected' : 'none'}>
<input
type="radio"
value="ccpa"
value="usnat"
bind:group={framework}
on:change={setLocation}
/>
in USA:
<strong>CCPA</strong>
<strong>USNAT</strong>
</label>
<label class={framework == 'aus' ? 'selected' : 'none'}>
<input
Expand Down Expand Up @@ -153,10 +153,10 @@
{#each Object.entries(consentState.tcfv2.vendorConsents) as [consent, state]}
<span class={JSON.parse(state) ? 'yes' : 'no'}>{consent}</span>
{/each}
{:else if consentState.ccpa}
<h2>ccpa.doNotSell</h2>
<span class="label" data-donotsell={consentState.ccpa.doNotSell}
>{consentState.ccpa.doNotSell}</span
{:else if consentState.usnat}
<h2>usnat.doNotSell</h2>
<span class="label" data-donotsell={consentState.usnat.doNotSell}
>{consentState.usnat.doNotSell}</span
>
{:else if consentState.aus}
<h2>aus.personalisedAdvertising</h2>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,20 @@ import { expect, test } from '@playwright/test';
import {
ACCOUNT_ID,
ENDPOINT,
PRIVACY_MANAGER_CCPA,
PRIVACY_MANAGER_USNAT,
} from '../fixtures/sourcepointConfig';

const iframePrivacyManager = `#sp_message_iframe_${PRIVACY_MANAGER_CCPA}`;
const iframeMessage = `[id^="sp_message_iframe_"]`;
const iframePrivacyManager = `#sp_message_iframe_${PRIVACY_MANAGER_USNAT}`;
const doNotSellButton = 'div.message-component > button.sp_choice_type_13';
const closeButton = 'div.message-component > button.sp_choice_type_15';
const saveAndExitButton = '.sp_choice_type_SE';

const url = `http://localhost:4321/csnx/cmp-test-page#ccpa`;
const url = `http://localhost:4321/csnx/cmp-test-page#usnat`;

async function doNotSellIs(page, expectedValue) {
await page.waitForLoadState('networkidle');

await page.locator('[data-donotsell]').waitFor({
state: 'attached',
timeout: 2000,
Expand All @@ -21,7 +27,7 @@ async function doNotSellIs(page, expectedValue) {
.querySelector('[data-donotsell]')
.getAttribute('data-donotsell') === expectedValue.toString(),
expectedValue,
{ timeout: 2000 },
{ timeout: 5000 },
);
const attributeValue = await page
.locator('[data-donotsell]')
Expand Down Expand Up @@ -63,6 +69,7 @@ test.describe('Window', () => {
test.describe('Document', () => {
test('should have the SP iframe', async ({ page }) => {
await page.goto(url);
await page.waitForLoadState('networkidle');
const iframe = page
.frameLocator('iframe[title="SP Consent Message"]')
.getByLabel('Do not sell my personal');
Expand All @@ -86,33 +93,62 @@ test.describe('Interaction', () => {
await doNotSellIs(page, false);
});

test(`should retract consent when clicking "${buttonTitle}"`, async ({
test(`should retract consent banner after selecting do not sell button "${buttonTitle}"`, async ({
page,
}) => {
await page.goto(url);

await page
.frameLocator('iframe[title="SP Consent Message"]')
.getByLabel('Do not sell my personal')
.click();
await doNotSellIs(page, false);

await page.frameLocator(iframeMessage).locator(doNotSellButton).click();

await page.waitForLoadState('networkidle');

await doNotSellIs(page, true);
await expect(
page.frameLocator(iframeMessage).locator(doNotSellButton),
).toBeHidden();
});

test(`should be able to retract consent`, async ({ page }) => {
test(`should retract consent banner after selecting close button "${buttonTitle}"`, async ({
page,
}) => {
await page.goto(url);

await doNotSellIs(page, false);

await page.frameLocator(iframeMessage).locator(closeButton).click();

await page.waitForLoadState('networkidle');

await expect(
page.frameLocator(iframeMessage).locator(closeButton),
).toBeHidden();
});

test(`should be able to interact with the toggle privacy manager`, async ({
page,
}) => {
await page.goto(url);

await doNotSellIs(page, false);

await page.frameLocator(iframeMessage).locator(closeButton).click();

await doNotSellIs(page, false);

await page.click('[data-cy=pm]');

const privacyManagerIframe = await getIframeBody(
page,
iframePrivacyManager,
);
await privacyManagerIframe.click('.pm-toggle .off');
await privacyManagerIframe.click('.sp_choice_type_SAVE_AND_EXIT');
await privacyManagerIframe.click('.pm-toggle .on');
await privacyManagerIframe.click(saveAndExitButton);

await page.waitForLoadState('networkidle');

await doNotSellIs(page, true);
await expect(
page.frameLocator(iframeMessage).locator(saveAndExitButton),
).toBeHidden();
});
});
3 changes: 2 additions & 1 deletion libs/@guardian/libs/playwright/fixtures/sourcepointConfig.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export const ACCOUNT_ID = 1257;
export const PRIVACY_MANAGER_CCPA = 540252;
export const PRIVACY_MANAGER_AUSTRALIA = 1178486;

export const PRIVACY_MANAGER_USNAT = 1068329;
export const PRIVACY_MANAGER_USNAT_SECOND_LAYER = 1129533;
export const ENDPOINT = 'https://cdn.privacy-mgmt.com';
15 changes: 8 additions & 7 deletions libs/@guardian/libs/src/consent-management-platform/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

> Consent management for `*.theguardian.com`.
The Guardian CMP handles applying the CCPA to users in the USA,
The Guardian CMP handles applying the USNAT to users in the USA, CCPA to Aus
and TCFv2 to everyone else.

## Table of Contents
Expand Down Expand Up @@ -206,7 +206,7 @@ boolean value, defaulting to `false` where no explicit consent was given.
}
```

##### `consentState.ccpa`
##### `consentState.usnat`

type: `Object` or `undefined`

Expand All @@ -217,6 +217,7 @@ If the user is not in the USA, it will be `undefined`.
```js
{
doNotSell: Boolean;
signalStatus: 'ready';
}
```

Expand All @@ -243,28 +244,28 @@ If the user can be targeted for personalisation according to the active consent

For example `canTarget` would be `true` in the following scenarios:

- for CCPA if the user has _not_ clicked "do not sell",
- for USNAT if the user has _not_ clicked "do not sell",
- for AUS if the user has _not_ opted out of personalised advertising
- for TCFv2 if the user has given consent for all purposes

##### `consentState.framework`

type: `string` | null

The active consent framework e.g. `"ccpa"`, `"aus"`, `"tcfv2"` or `null`.
The active consent framework e.g. `"usnat"`, `"aus"`, `"tcfv2"` or `null`.

#### Example

```js
import { onConsentChange } from '@guardian/consent-management-platform';

onConsentChange(({ tcfv2, ccpa, aus }) => {
onConsentChange(({ tcfv2, usnat, aus }) => {
if (tcfv2) {
console.log(tcfv2); // { 1: true || false, 1: true || false, ... }
}

if (ccpa) {
console.log(ccpa); // { doNotSell: true || false }
if (usnat) {
console.log(usnat); // { doNotSell: true || false }
}

if (aus) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { CCPAData } from '../types/ccpa';
import type { AUSData } from '../types/aus';

type Command = 'getUSPData';

Expand All @@ -16,5 +16,5 @@ const api = (command: Command) =>
}
});

export const getUSPData = (): Promise<CCPAData> =>
api('getUSPData') as Promise<CCPAData>;
export const getUSPData = (): Promise<AUSData> =>
api('getUSPData') as Promise<AUSData>;

This file was deleted.

20 changes: 0 additions & 20 deletions libs/@guardian/libs/src/consent-management-platform/ccpa/api.ts

This file was deleted.

This file was deleted.

This file was deleted.

6 changes: 3 additions & 3 deletions libs/@guardian/libs/src/consent-management-platform/cmp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { getCurrentFramework } from './getCurrentFramework';
import { mark } from './lib/mark';
import {
PRIVACY_MANAGER_AUSTRALIA,
PRIVACY_MANAGER_CCPA,
PRIVACY_MANAGER_TCFV2,
PRIVACY_MANAGER_USNAT,
} from './lib/sourcepointConfig';
import {
init as initSourcepoint,
Expand All @@ -29,8 +29,8 @@ function showPrivacyManager(): void {
case 'tcfv2':
window._sp_?.gdpr?.loadPrivacyManagerModal?.(PRIVACY_MANAGER_TCFV2);
break;
case 'ccpa':
window._sp_?.ccpa?.loadPrivacyManagerModal?.(PRIVACY_MANAGER_CCPA);
case 'usnat':
window._sp_?.usnat?.loadPrivacyManagerModal?.(PRIVACY_MANAGER_USNAT);
break;
case 'aus':
window._sp_?.ccpa?.loadPrivacyManagerModal?.(PRIVACY_MANAGER_AUSTRALIA);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ const tcfv2ConsentFoundTrue = {
const tcfv2ConsentFoundFalse = {
tcfv2: { vendorConsents: { [vendorOne]: false } },
};
const ccpaWithConsent = { ccpa: { doNotSell: false } };
const ccpaWithoutConsent = { ccpa: { doNotSell: true } };
const usnatWithConsent = { usnat: { doNotSell: false } };
const usnatWithoutConsent = { usnat: { doNotSell: true } };
const ausWithConsent = { aus: { personalisedAdvertising: true } };
const ausWithoutConsent = { aus: { personalisedAdvertising: false } };

Expand All @@ -41,8 +41,8 @@ test.each([
['tcfv2', true, 'vendorOne', tcfv2ConsentFoundTrueAlt],
['tcfv2', true, 'vendorOne', tcfv2ConsentFoundTrue],
['tcfv2', false, 'vendorOne', tcfv2ConsentFoundFalse],
['ccpa', true, 'vendorOne', ccpaWithConsent],
['ccpa', false, 'vendorOne', ccpaWithoutConsent],
['usnat', true, 'vendorOne', usnatWithConsent],
['usnat', false, 'vendorOne', usnatWithoutConsent],
['aus', true, 'vendorOne', ausWithConsent],
['aus', false, 'vendorOne', ausWithoutConsent],
])(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ export const getConsentFor: GetConsentFor = (
);
}

if (consent.ccpa) {
if (consent.usnat) {
// doNotSell = false means we have consent
return !consent.ccpa.doNotSell;
return !consent.usnat.doNotSell;
}

if (consent.aus) {
Expand Down
Loading

0 comments on commit fb2f818

Please sign in to comment.