From e41d6752f2c39e0ce8d9458288338cc5835b5c1f Mon Sep 17 00:00:00 2001 From: Anna Voelker Date: Fri, 15 Dec 2023 15:14:31 +0000 Subject: [PATCH 01/17] cmpGetLocalStorageItem for first discussion --- src/cmpGetLocalStorageItem.ts | 37 +++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/cmpGetLocalStorageItem.ts diff --git a/src/cmpGetLocalStorageItem.ts b/src/cmpGetLocalStorageItem.ts new file mode 100644 index 000000000..915400d73 --- /dev/null +++ b/src/cmpGetLocalStorageItem.ts @@ -0,0 +1,37 @@ +import { storage } from '@guardian/libs'; +import { onConsent } from '.'; + +export const UseCaseOptions = [ + "Targeted advertising", + "Essential" +] as const; +export type UseCases = typeof UseCaseOptions[number]; + +export const cmpGetLocalStorageItem = async (localStorageItem: string, useCase: UseCases): Promise => +{ + console.log('in cmpGetLocalStorageItem'); + const consentState = await onConsent(); + switch(useCase) { + case "Targeted advertising": { + if ( + (consentState.tcfv2?.consents['1'] && consentState.tcfv2.consents['3']) + || (!consentState.ccpa?.doNotSell) + || (consentState.aus?.personalisedAdvertising) ) + { + console.log(`localStorage.getItem(localStorageItem): ${localStorage.getItem(localStorageItem)}`); + return storage.local.get(localStorageItem) + } + else + { + console.log('in else') + return(null) + } + } + case "Essential": return(localStorage.getItem(localStorageItem)) + default: return(null) + } +}; + + +//await cmpGetLocalStorageItem("dep", "Targeted advertising") +//await cmpGetLocalStorageItem("dep", "invalid") From 8df767a11e3860af376398a273de79188bd6b9db Mon Sep 17 00:00:00 2001 From: Anna Voelker Date: Fri, 15 Dec 2023 15:36:17 +0000 Subject: [PATCH 02/17] add simple index.ts --- src/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/index.ts b/src/index.ts index 8262e7574..13753a54e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,6 +12,7 @@ import { onConsent as serverOnConsent, onConsentChange as serverOnConsentChange, } from './server'; +import { cmpGetLocalStorageItem as cmpGetLocalStorageItemLocal } from './cmpGetLocalStorageItem'; import type { CMP, InitCMP, WillShowPrivacyMessage } from './types'; import { initVendorDataManager } from './vendorDataManager'; @@ -111,3 +112,4 @@ export const onConsentChange = isServerSide export const getConsentFor = isServerSide ? serverGetConsentFor : (window.guCmpHotFix.getConsentFor ||= clientGetConsentFor); +export const cmpGetLocalStorageItem = cmpGetLocalStorageItemLocal; From 1a8d0e83140959a1b0f39b2aeba4304a038c0e8f Mon Sep 17 00:00:00 2001 From: Anna Voelker Date: Mon, 18 Dec 2023 11:33:13 +0000 Subject: [PATCH 03/17] add cmpSetLocalStorageItem some tidy up --- src/cmpGetLocalStorageItem.ts | 37 --------------- src/cmpLocalStorage.ts | 86 +++++++++++++++++++++++++++++++++++ src/index.ts | 3 +- 3 files changed, 88 insertions(+), 38 deletions(-) delete mode 100644 src/cmpGetLocalStorageItem.ts create mode 100644 src/cmpLocalStorage.ts diff --git a/src/cmpGetLocalStorageItem.ts b/src/cmpGetLocalStorageItem.ts deleted file mode 100644 index 915400d73..000000000 --- a/src/cmpGetLocalStorageItem.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { storage } from '@guardian/libs'; -import { onConsent } from '.'; - -export const UseCaseOptions = [ - "Targeted advertising", - "Essential" -] as const; -export type UseCases = typeof UseCaseOptions[number]; - -export const cmpGetLocalStorageItem = async (localStorageItem: string, useCase: UseCases): Promise => -{ - console.log('in cmpGetLocalStorageItem'); - const consentState = await onConsent(); - switch(useCase) { - case "Targeted advertising": { - if ( - (consentState.tcfv2?.consents['1'] && consentState.tcfv2.consents['3']) - || (!consentState.ccpa?.doNotSell) - || (consentState.aus?.personalisedAdvertising) ) - { - console.log(`localStorage.getItem(localStorageItem): ${localStorage.getItem(localStorageItem)}`); - return storage.local.get(localStorageItem) - } - else - { - console.log('in else') - return(null) - } - } - case "Essential": return(localStorage.getItem(localStorageItem)) - default: return(null) - } -}; - - -//await cmpGetLocalStorageItem("dep", "Targeted advertising") -//await cmpGetLocalStorageItem("dep", "invalid") diff --git a/src/cmpLocalStorage.ts b/src/cmpLocalStorage.ts new file mode 100644 index 000000000..ce23a9ee3 --- /dev/null +++ b/src/cmpLocalStorage.ts @@ -0,0 +1,86 @@ +import { storage } from '@guardian/libs'; +import { onConsent } from '.'; + +export const UseCaseOptions = [ + "Targeted advertising", + "Essential" +] as const; +export type UseCases = typeof UseCaseOptions[number]; + +export const cmpGetLocalStorageItem = async (useCase: UseCases, localStorageItem: string): Promise => +{ + //console.log('in cmpGetLocalStorageItem'); + const consentState = await onConsent(); + + /*console.log(`consentState.tcfv2?.consents['1']: ${consentState.tcfv2?.consents['1']}`); + console.log(`consentState.tcfv2?.consents['2']: ${consentState.tcfv2?.consents['2']}`); + console.log(`consentState.tcfv2?.consents['3']: ${consentState.tcfv2?.consents['3']}`); + console.log(`consentState.tcfv2?.consents['4']: ${consentState.tcfv2?.consents['4']}`); + console.log(`consentState.tcfv2?.consents['5']: ${consentState.tcfv2?.consents['5']}`); + console.log(`consentState.tcfv2?.consents['6']: ${consentState.tcfv2?.consents['6']}`); + console.log(`consentState.tcfv2?.consents['7']: ${consentState.tcfv2?.consents['7']}`); + console.log(`consentState.tcfv2?.consents['8']: ${consentState.tcfv2?.consents['8']}`); + console.log(`consentState.tcfv2?.consents['9']: ${consentState.tcfv2?.consents['9']}`); + console.log(`consentState.tcfv2?.consents['10']: ${consentState.tcfv2?.consents['10']}`); + console.log(`consentState.tcfv2?.consents['11']: ${consentState.tcfv2?.consents['11']}`); + console.log(`consentState.canTarget: ${consentState.canTarget}`); + */ + + switch(useCase) { + case "Targeted advertising": { + if ( + consentState.canTarget //could be more granular than this, for example by using explicit tcf purposes: + //(consentState.tcfv2?.consents['1'] + // && consentState.tcfv2.consents['2'] + // && consentState.tcfv2.consents['3'])//Need the correct list of consents, this is just an example + //|| (!consentState.ccpa?.doNotSell) + //|| (consentState.aus?.personalisedAdvertising) + ) + { + return storage.local.get(localStorageItem) + } + else + { + console.error('cmp', `Cannot get local storage item ${localStorageItem} due to missing consent for use-case ${useCase}`) + return(null) + } + } + case "Essential": return(storage.local.get(localStorageItem)) + default: { + console.error('cmp', `Cannot get local storage item ${localStorageItem} due to unknown use-case ${useCase}`) + return(null) + } + } +}; + +export const cmpSetLocalStorageItem = async (useCase: UseCases, localStorageItem: string, value:unknown, expires?: string | number | Date): Promise => +{ + //console.log('in cmpSetLocalStorageItem'); + const consentState = await onConsent(); + + switch(useCase) { + case "Targeted advertising": { + if ( + consentState.canTarget //could be more granular than this, for example by using explicit tcf purposes: + //(consentState.tcfv2?.consents['1'] + // && consentState.tcfv2.consents['2'] + // && consentState.tcfv2.consents['3'])//Need the correct list of consents, this is just an example + //|| (!consentState.ccpa?.doNotSell) + //|| (consentState.aus?.personalisedAdvertising) + ) + { + storage.local.set(localStorageItem, value, expires) + } + else + { + console.error('cmp', `Cannot set local storage item ${localStorageItem} due to missing consent for use-case ${useCase}`) + } + } + case "Essential": storage.local.set(localStorageItem, value, expires) + default: console.error('cmp', `Cannot set local storage item ${localStorageItem} due to unknown use-case ${useCase}`) + }; +}; + + +//await cmpGetLocalStorageItem("Targeted advertising", "dep") +//await cmpGetLocalStorageItem("invalid", "dep") diff --git a/src/index.ts b/src/index.ts index 13753a54e..fea963b8b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,7 +12,7 @@ import { onConsent as serverOnConsent, onConsentChange as serverOnConsentChange, } from './server'; -import { cmpGetLocalStorageItem as cmpGetLocalStorageItemLocal } from './cmpGetLocalStorageItem'; +import { cmpGetLocalStorageItem as cmpGetLocalStorageItemLocal, cmpSetLocalStorageItem as cmpSetLocalStorageItemLocal } from './cmpLocalStorage'; import type { CMP, InitCMP, WillShowPrivacyMessage } from './types'; import { initVendorDataManager } from './vendorDataManager'; @@ -113,3 +113,4 @@ export const getConsentFor = isServerSide ? serverGetConsentFor : (window.guCmpHotFix.getConsentFor ||= clientGetConsentFor); export const cmpGetLocalStorageItem = cmpGetLocalStorageItemLocal; +export const cmpSetLocalStorageItem = cmpSetLocalStorageItemLocal; From 55bf627142b01fd5a2ddb60323fced5115c17dd0 Mon Sep 17 00:00:00 2001 From: Anna Voelker Date: Mon, 18 Dec 2023 20:33:33 +0000 Subject: [PATCH 04/17] add sessionStorage and cookies, some refactoring --- src/cmpLocalStorage.ts | 86 ------------------------- src/cmpStorage.ts | 141 +++++++++++++++++++++++++++++++++++++++++ src/index.ts | 5 +- 3 files changed, 143 insertions(+), 89 deletions(-) delete mode 100644 src/cmpLocalStorage.ts create mode 100644 src/cmpStorage.ts diff --git a/src/cmpLocalStorage.ts b/src/cmpLocalStorage.ts deleted file mode 100644 index ce23a9ee3..000000000 --- a/src/cmpLocalStorage.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { storage } from '@guardian/libs'; -import { onConsent } from '.'; - -export const UseCaseOptions = [ - "Targeted advertising", - "Essential" -] as const; -export type UseCases = typeof UseCaseOptions[number]; - -export const cmpGetLocalStorageItem = async (useCase: UseCases, localStorageItem: string): Promise => -{ - //console.log('in cmpGetLocalStorageItem'); - const consentState = await onConsent(); - - /*console.log(`consentState.tcfv2?.consents['1']: ${consentState.tcfv2?.consents['1']}`); - console.log(`consentState.tcfv2?.consents['2']: ${consentState.tcfv2?.consents['2']}`); - console.log(`consentState.tcfv2?.consents['3']: ${consentState.tcfv2?.consents['3']}`); - console.log(`consentState.tcfv2?.consents['4']: ${consentState.tcfv2?.consents['4']}`); - console.log(`consentState.tcfv2?.consents['5']: ${consentState.tcfv2?.consents['5']}`); - console.log(`consentState.tcfv2?.consents['6']: ${consentState.tcfv2?.consents['6']}`); - console.log(`consentState.tcfv2?.consents['7']: ${consentState.tcfv2?.consents['7']}`); - console.log(`consentState.tcfv2?.consents['8']: ${consentState.tcfv2?.consents['8']}`); - console.log(`consentState.tcfv2?.consents['9']: ${consentState.tcfv2?.consents['9']}`); - console.log(`consentState.tcfv2?.consents['10']: ${consentState.tcfv2?.consents['10']}`); - console.log(`consentState.tcfv2?.consents['11']: ${consentState.tcfv2?.consents['11']}`); - console.log(`consentState.canTarget: ${consentState.canTarget}`); - */ - - switch(useCase) { - case "Targeted advertising": { - if ( - consentState.canTarget //could be more granular than this, for example by using explicit tcf purposes: - //(consentState.tcfv2?.consents['1'] - // && consentState.tcfv2.consents['2'] - // && consentState.tcfv2.consents['3'])//Need the correct list of consents, this is just an example - //|| (!consentState.ccpa?.doNotSell) - //|| (consentState.aus?.personalisedAdvertising) - ) - { - return storage.local.get(localStorageItem) - } - else - { - console.error('cmp', `Cannot get local storage item ${localStorageItem} due to missing consent for use-case ${useCase}`) - return(null) - } - } - case "Essential": return(storage.local.get(localStorageItem)) - default: { - console.error('cmp', `Cannot get local storage item ${localStorageItem} due to unknown use-case ${useCase}`) - return(null) - } - } -}; - -export const cmpSetLocalStorageItem = async (useCase: UseCases, localStorageItem: string, value:unknown, expires?: string | number | Date): Promise => -{ - //console.log('in cmpSetLocalStorageItem'); - const consentState = await onConsent(); - - switch(useCase) { - case "Targeted advertising": { - if ( - consentState.canTarget //could be more granular than this, for example by using explicit tcf purposes: - //(consentState.tcfv2?.consents['1'] - // && consentState.tcfv2.consents['2'] - // && consentState.tcfv2.consents['3'])//Need the correct list of consents, this is just an example - //|| (!consentState.ccpa?.doNotSell) - //|| (consentState.aus?.personalisedAdvertising) - ) - { - storage.local.set(localStorageItem, value, expires) - } - else - { - console.error('cmp', `Cannot set local storage item ${localStorageItem} due to missing consent for use-case ${useCase}`) - } - } - case "Essential": storage.local.set(localStorageItem, value, expires) - default: console.error('cmp', `Cannot set local storage item ${localStorageItem} due to unknown use-case ${useCase}`) - }; -}; - - -//await cmpGetLocalStorageItem("Targeted advertising", "dep") -//await cmpGetLocalStorageItem("invalid", "dep") diff --git a/src/cmpStorage.ts b/src/cmpStorage.ts new file mode 100644 index 000000000..9812b6821 --- /dev/null +++ b/src/cmpStorage.ts @@ -0,0 +1,141 @@ +import { storage, getCookie, setCookie } from '@guardian/libs'; +import { onConsent } from '.'; + +export const UseCaseOptions = [ + "Targeted advertising", + "Essential" +] as const; +export type UseCases = typeof UseCaseOptions[number]; + +const hasConsentForUseCase = async (useCase: UseCases): Promise => +{ + /*console.log(`consentState.tcfv2?.consents['1']: ${consentState.tcfv2?.consents['1']}`); + console.log(`consentState.tcfv2?.consents['2']: ${consentState.tcfv2?.consents['2']}`); + console.log(`consentState.tcfv2?.consents['3']: ${consentState.tcfv2?.consents['3']}`); + console.log(`consentState.tcfv2?.consents['4']: ${consentState.tcfv2?.consents['4']}`); + console.log(`consentState.tcfv2?.consents['5']: ${consentState.tcfv2?.consents['5']}`); + console.log(`consentState.tcfv2?.consents['6']: ${consentState.tcfv2?.consents['6']}`); + console.log(`consentState.tcfv2?.consents['7']: ${consentState.tcfv2?.consents['7']}`); + console.log(`consentState.tcfv2?.consents['8']: ${consentState.tcfv2?.consents['8']}`); + console.log(`consentState.tcfv2?.consents['9']: ${consentState.tcfv2?.consents['9']}`); + console.log(`consentState.tcfv2?.consents['10']: ${consentState.tcfv2?.consents['10']}`); + console.log(`consentState.tcfv2?.consents['11']: ${consentState.tcfv2?.consents['11']}`); + console.log(`consentState.canTarget: ${consentState.canTarget}`); + */ + + const consentState = await onConsent(); + + switch(useCase) { + case "Targeted advertising": return(consentState.canTarget) + //could be more granular than this, for example by using explicit tcf purposes: + //(consentState.tcfv2?.consents['1'] + // && consentState.tcfv2.consents['2'] + // && consentState.tcfv2.consents['3'])//Need the correct list of consents, this is just an example + //|| (!consentState.ccpa?.doNotSell) + //|| (consentState.aus?.personalisedAdvertising) + case "Essential": return(true) + default: return(false) + } + +} + +export const cmpGetLocalStorageItem = async (useCase: UseCases, storageItem: string): Promise => +{ + console.log('in cmpGetLocalStorageItem'); + + if(await hasConsentForUseCase(useCase)) + { + return storage.local.get(storageItem) + } + else + { + console.error('cmp', `Cannot get local storage item ${storageItem} due to missing consent for use-case ${useCase}`) + return(null) + } +}; + +export const cmpSetLocalStorageItem = async (useCase: UseCases, storageItem: string, value:unknown, expires?: string | number | Date): Promise => +{ + console.log('in cmpSetLocalStorageItem'); + + if(await hasConsentForUseCase(useCase)) + { + return storage.local.set(storageItem, expires) + } + else + { + console.error('cmp', `Cannot set local storage item ${storageItem} due to missing consent for use-case ${useCase}`) + } +}; + +export const cmpGetSessionStorageItem = async (useCase: UseCases, storageItem: string): Promise => +{ + console.log('in cmpGetSessionStorageItem'); + + if(await hasConsentForUseCase(useCase)) + { + return storage.session.get(storageItem) + } + else + { + console.error('cmp', `Cannot get session storage item ${storageItem} due to missing consent for use-case ${useCase}`) + return(null) + } +}; + +export const cmpSetSessionStorageItem = async (useCase: UseCases, storageItem: string, value:unknown, expires?: string | number | Date): Promise => +{ + console.log('in cmpSetSessionStorageItem'); + + if(await hasConsentForUseCase(useCase)) + { + return storage.session.set(storageItem, expires) + } + else + { + console.error('cmp', `Cannot set session storage item ${storageItem} due to missing consent for use-case ${useCase}`) + } +}; + +export const cmpGetCookie = async({ useCase, name, shouldMemoize, }: { + useCase: UseCases, + name: string; + shouldMemoize?: boolean | undefined; +}): Promise => +{ + console.log('in cmpGetCookie'); + + if(await hasConsentForUseCase(useCase)) + { + return getCookie({name: name, shouldMemoize: shouldMemoize}) + } + else + { + console.error('cmp', `Cannot get cookie ${name} due to missing consent for use-case ${useCase}`) + return(null) + } +}; + +export const cmpSetCookie = async ({ useCase, name, value, daysToLive, isCrossSubdomain, }: { + useCase: UseCases, + name: string; + value: string; + daysToLive?: number | undefined; + isCrossSubdomain?: boolean | undefined; +}): Promise => +{ + console.log('in cmpSetCookie'); + + if(await hasConsentForUseCase(useCase)) + { + setCookie({name:name, value:value, daysToLive:daysToLive, isCrossSubdomain:isCrossSubdomain}) + } + else + { + console.error('cmp', `Cannot set cookie ${name} due to missing consent for use-case ${useCase}`) + } +}; + + +//await cmpGetLocalStorageItem("Targeted advertising", "dep") +//await cmpGetLocalStorageItem("invalid", "dep") diff --git a/src/index.ts b/src/index.ts index fea963b8b..4b2a3b8d4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,10 +12,11 @@ import { onConsent as serverOnConsent, onConsentChange as serverOnConsentChange, } from './server'; -import { cmpGetLocalStorageItem as cmpGetLocalStorageItemLocal, cmpSetLocalStorageItem as cmpSetLocalStorageItemLocal } from './cmpLocalStorage'; import type { CMP, InitCMP, WillShowPrivacyMessage } from './types'; import { initVendorDataManager } from './vendorDataManager'; +export {cmpGetLocalStorageItem, cmpSetLocalStorageItem, cmpGetSessionStorageItem, cmpSetSessionStorageItem, cmpGetCookie, cmpSetCookie} from './cmpStorage'; + // Store some bits in the global scope for reuse, in case there's more // than one instance of the CMP on the page in different scopes. if (!isServerSide) { @@ -112,5 +113,3 @@ export const onConsentChange = isServerSide export const getConsentFor = isServerSide ? serverGetConsentFor : (window.guCmpHotFix.getConsentFor ||= clientGetConsentFor); -export const cmpGetLocalStorageItem = cmpGetLocalStorageItemLocal; -export const cmpSetLocalStorageItem = cmpSetLocalStorageItemLocal; From f9c03f6904ddd8752bf00487b3db89058b905b2d Mon Sep 17 00:00:00 2001 From: Anna Voelker Date: Mon, 18 Dec 2023 22:07:57 +0000 Subject: [PATCH 05/17] add some tests --- src/cmpStorage.test.ts | 74 ++++++++++++++++++++++++++++++++++++++++++ src/cmpStorage.ts | 6 ++-- 2 files changed, 77 insertions(+), 3 deletions(-) create mode 100644 src/cmpStorage.test.ts diff --git a/src/cmpStorage.test.ts b/src/cmpStorage.test.ts new file mode 100644 index 000000000..30be0721d --- /dev/null +++ b/src/cmpStorage.test.ts @@ -0,0 +1,74 @@ +import { onConsentChange } from './onConsentChange'; +import type { Callback, ConsentState } from './types'; +import type { TCFv2ConsentState } from './types/tcfv2'; +import {hasConsentForUseCase} from './cmpStorage'; + +jest.mock('./onConsentChange'); + +const tcfv2ConsentState: TCFv2ConsentState = { + consents: { 1: true }, + eventStatus: 'tcloaded', + vendorConsents: { + ['5efefe25b8e05c06542b2a77']: true, + }, + addtlConsent: 'xyz', + gdprApplies: true, + tcString: 'YAAA', +}; + +const tcfv2ConsentStateNoConsent: TCFv2ConsentState = { + consents: { 1: false }, + eventStatus: 'tcloaded', + vendorConsents: {}, + addtlConsent: 'xyz', + gdprApplies: true, + tcString: 'YAAA', +}; + +const mockOnConsentChange = (consentState: ConsentState) => + (onConsentChange as jest.Mock).mockImplementation((cb: Callback) => + cb(consentState), + ); + +describe('cmpStorage.hasConsentForUseCase returns the expected consent', () => { + test('Targeted advertising when canTarget is true', async () => { + const consentState: ConsentState = { + tcfv2: tcfv2ConsentState, + canTarget: true, + framework: 'tcfv2', + }; + mockOnConsentChange(consentState); + const hasConsent = await hasConsentForUseCase('Targeted advertising'); + expect(hasConsent).toEqual(true); + }); + test('Targeted advertising when canTarget is false', async () => { + const consentState: ConsentState = { + tcfv2: tcfv2ConsentState, + canTarget: false, + framework: 'tcfv2', + }; + mockOnConsentChange(consentState); + const hasConsent = await hasConsentForUseCase('Targeted advertising'); + expect(hasConsent).toEqual(false); + }); + test('Targeted advertising when canTarget is true', async () => { + const consentState: ConsentState = { + tcfv2: tcfv2ConsentState, + canTarget: true, + framework: 'tcfv2', + }; + mockOnConsentChange(consentState); + const hasConsent = await hasConsentForUseCase('Targeted advertising'); + expect(hasConsent).toEqual(true); + }); + test('Essential when no consents', async () => { + const consentState: ConsentState = { + tcfv2: tcfv2ConsentStateNoConsent, + canTarget: false, + framework: 'tcfv2', + }; + mockOnConsentChange(consentState); + const hasConsent = await hasConsentForUseCase('Essential'); + expect(hasConsent).toEqual(true); + }); +}); diff --git a/src/cmpStorage.ts b/src/cmpStorage.ts index 9812b6821..5d5551723 100644 --- a/src/cmpStorage.ts +++ b/src/cmpStorage.ts @@ -7,8 +7,10 @@ export const UseCaseOptions = [ ] as const; export type UseCases = typeof UseCaseOptions[number]; -const hasConsentForUseCase = async (useCase: UseCases): Promise => +export const hasConsentForUseCase = async (useCase: UseCases): Promise => { + const consentState = await onConsent(); + /*console.log(`consentState.tcfv2?.consents['1']: ${consentState.tcfv2?.consents['1']}`); console.log(`consentState.tcfv2?.consents['2']: ${consentState.tcfv2?.consents['2']}`); console.log(`consentState.tcfv2?.consents['3']: ${consentState.tcfv2?.consents['3']}`); @@ -23,8 +25,6 @@ const hasConsentForUseCase = async (useCase: UseCases): Promise => console.log(`consentState.canTarget: ${consentState.canTarget}`); */ - const consentState = await onConsent(); - switch(useCase) { case "Targeted advertising": return(consentState.canTarget) //could be more granular than this, for example by using explicit tcf purposes: From 7071341b2ea0492a71daaab7ceef131987610e55 Mon Sep 17 00:00:00 2001 From: Anna Voelker Date: Tue, 19 Dec 2023 09:19:47 +0000 Subject: [PATCH 06/17] add more tests --- src/cmpStorage.test.ts | 136 +++++++++++++++++++++++------------------ src/cmpStorage.ts | 6 +- 2 files changed, 83 insertions(+), 59 deletions(-) diff --git a/src/cmpStorage.test.ts b/src/cmpStorage.test.ts index 30be0721d..e13a70ce6 100644 --- a/src/cmpStorage.test.ts +++ b/src/cmpStorage.test.ts @@ -1,74 +1,94 @@ import { onConsentChange } from './onConsentChange'; import type { Callback, ConsentState } from './types'; import type { TCFv2ConsentState } from './types/tcfv2'; -import {hasConsentForUseCase} from './cmpStorage'; +import {cmpGetLocalStorageItem, _private} from './cmpStorage'; jest.mock('./onConsentChange'); const tcfv2ConsentState: TCFv2ConsentState = { - consents: { 1: true }, - eventStatus: 'tcloaded', - vendorConsents: { - ['5efefe25b8e05c06542b2a77']: true, - }, - addtlConsent: 'xyz', - gdprApplies: true, - tcString: 'YAAA', + consents: { 1: true }, + eventStatus: 'tcloaded', + vendorConsents: { + ['5efefe25b8e05c06542b2a77']: true, + }, + addtlConsent: 'xyz', + gdprApplies: true, + tcString: 'YAAA', }; const tcfv2ConsentStateNoConsent: TCFv2ConsentState = { - consents: { 1: false }, - eventStatus: 'tcloaded', - vendorConsents: {}, - addtlConsent: 'xyz', - gdprApplies: true, - tcString: 'YAAA', + consents: { 1: false }, + eventStatus: 'tcloaded', + vendorConsents: {}, + addtlConsent: 'xyz', + gdprApplies: true, + tcString: 'YAAA', }; const mockOnConsentChange = (consentState: ConsentState) => - (onConsentChange as jest.Mock).mockImplementation((cb: Callback) => - cb(consentState), - ); + (onConsentChange as jest.Mock).mockImplementation((cb: Callback) => + cb(consentState), + ); describe('cmpStorage.hasConsentForUseCase returns the expected consent', () => { - test('Targeted advertising when canTarget is true', async () => { - const consentState: ConsentState = { - tcfv2: tcfv2ConsentState, - canTarget: true, - framework: 'tcfv2', - }; - mockOnConsentChange(consentState); - const hasConsent = await hasConsentForUseCase('Targeted advertising'); - expect(hasConsent).toEqual(true); - }); - test('Targeted advertising when canTarget is false', async () => { - const consentState: ConsentState = { - tcfv2: tcfv2ConsentState, - canTarget: false, - framework: 'tcfv2', - }; - mockOnConsentChange(consentState); - const hasConsent = await hasConsentForUseCase('Targeted advertising'); - expect(hasConsent).toEqual(false); - }); - test('Targeted advertising when canTarget is true', async () => { - const consentState: ConsentState = { - tcfv2: tcfv2ConsentState, - canTarget: true, - framework: 'tcfv2', - }; - mockOnConsentChange(consentState); - const hasConsent = await hasConsentForUseCase('Targeted advertising'); - expect(hasConsent).toEqual(true); - }); - test('Essential when no consents', async () => { - const consentState: ConsentState = { - tcfv2: tcfv2ConsentStateNoConsent, - canTarget: false, - framework: 'tcfv2', - }; - mockOnConsentChange(consentState); - const hasConsent = await hasConsentForUseCase('Essential'); - expect(hasConsent).toEqual(true); - }); + test('Targeted advertising when canTarget is true', async () => { + const consentState: ConsentState = { + tcfv2: tcfv2ConsentState, + canTarget: true, + framework: 'tcfv2', + }; + mockOnConsentChange(consentState); + const hasConsent = await _private.hasConsentForUseCase('Targeted advertising'); + expect(hasConsent).toEqual(true); + }); + test('Targeted advertising when canTarget is false', async () => { + const consentState: ConsentState = { + tcfv2: tcfv2ConsentState, + canTarget: false, + framework: 'tcfv2', + }; + mockOnConsentChange(consentState); + const hasConsent = await _private.hasConsentForUseCase('Targeted advertising'); + expect(hasConsent).toEqual(false); + }); + test('Targeted advertising when canTarget is true', async () => { + const consentState: ConsentState = { + tcfv2: tcfv2ConsentState, + canTarget: true, + framework: 'tcfv2', + }; + mockOnConsentChange(consentState); + const hasConsent = await _private.hasConsentForUseCase('Targeted advertising'); + expect(hasConsent).toEqual(true); + }); + test('Essential when no consents', async () => { + const consentState: ConsentState = { + tcfv2: tcfv2ConsentStateNoConsent, + canTarget: false, + framework: 'tcfv2', + }; + mockOnConsentChange(consentState); + const hasConsent = await _private.hasConsentForUseCase('Essential'); + expect(hasConsent).toEqual(true); + }); + test('Targeted advertising local storage when canTarget is false', async () => { + const consentState: ConsentState = { + tcfv2: tcfv2ConsentState, + canTarget: false, + framework: 'tcfv2', + }; + mockOnConsentChange(consentState); + const localStorageValue = await cmpGetLocalStorageItem('Targeted advertising', 'mockLocal'); + expect(localStorageValue).toEqual(null); + }); + test('Essential local storage when no consents', async () => { + const consentState: ConsentState = { + tcfv2: tcfv2ConsentStateNoConsent, + canTarget: false, + framework: 'tcfv2', + }; + mockOnConsentChange(consentState); + const localStorageValue = await cmpGetLocalStorageItem('Essential', 'mockLocal'); + expect(localStorageValue).toEqual(null); //TODO: need to mock the call to localStorage + }); }); diff --git a/src/cmpStorage.ts b/src/cmpStorage.ts index 5d5551723..3f7a8efb3 100644 --- a/src/cmpStorage.ts +++ b/src/cmpStorage.ts @@ -7,7 +7,7 @@ export const UseCaseOptions = [ ] as const; export type UseCases = typeof UseCaseOptions[number]; -export const hasConsentForUseCase = async (useCase: UseCases): Promise => +const hasConsentForUseCase = async (useCase: UseCases): Promise => { const consentState = await onConsent(); @@ -136,6 +136,10 @@ export const cmpSetCookie = async ({ useCase, name, value, daysToLive, isCrossSu } }; +export const _private = { + hasConsentForUseCase +}; + //await cmpGetLocalStorageItem("Targeted advertising", "dep") //await cmpGetLocalStorageItem("invalid", "dep") From 1c79bbc0e773457d93c3e1de3c56d94ee4f9a80a Mon Sep 17 00:00:00 2001 From: Anna Voelker Date: Tue, 19 Dec 2023 11:06:13 +0000 Subject: [PATCH 07/17] mock tests --- src/cmpStorage.test.ts | 170 ++++++++++++++++++++++++++++++++++------- src/cmpStorage.ts | 4 +- 2 files changed, 146 insertions(+), 28 deletions(-) diff --git a/src/cmpStorage.test.ts b/src/cmpStorage.test.ts index e13a70ce6..6e84caf17 100644 --- a/src/cmpStorage.test.ts +++ b/src/cmpStorage.test.ts @@ -1,9 +1,25 @@ import { onConsentChange } from './onConsentChange'; import type { Callback, ConsentState } from './types'; import type { TCFv2ConsentState } from './types/tcfv2'; -import {cmpGetLocalStorageItem, _private} from './cmpStorage'; +import {cmpGetLocalStorageItem, cmpSetLocalStorageItem, cmpGetSessionStorageItem, cmpSetSessionStorageItem, _private} from './cmpStorage'; +//import { getCookie as getCookie_ } from 'lib/cookies'; +import { storage as storageStub } from '@guardian/libs'; jest.mock('./onConsentChange'); +jest.mock('@guardian/libs', () => ({ + getCookie: jest.fn(), + setCookie: jest.fn(), + storage: { + local: { + get: jest.fn(), + set: jest.fn(), + }, + session: { + get: jest.fn(), + set: jest.fn(), + }, + } +})); const tcfv2ConsentState: TCFv2ConsentState = { consents: { 1: true }, @@ -26,69 +42,171 @@ const tcfv2ConsentStateNoConsent: TCFv2ConsentState = { }; const mockOnConsentChange = (consentState: ConsentState) => - (onConsentChange as jest.Mock).mockImplementation((cb: Callback) => - cb(consentState), - ); + (onConsentChange as jest.Mock).mockImplementation((cb: Callback) => cb(consentState)); describe('cmpStorage.hasConsentForUseCase returns the expected consent', () => { - test('Targeted advertising when canTarget is true', async () => { + test('Targeted advertising has consent when canTarget is true', async () => { + const consentState: ConsentState = { + tcfv2: tcfv2ConsentState, + canTarget: true, + framework: 'tcfv2', + }; + mockOnConsentChange(consentState); + const hasConsent = await _private.hasConsentForUseCase('Targeted advertising'); + expect(hasConsent).toEqual(true); + }); + test('Targeted advertising has no consent when canTarget is false', async () => { + const consentState: ConsentState = { + tcfv2: tcfv2ConsentState, + canTarget: false, + framework: 'tcfv2', + }; + mockOnConsentChange(consentState); + const hasConsent = await _private.hasConsentForUseCase('Targeted advertising'); + expect(hasConsent).toEqual(false); + }); + test('Essential has consent even when ConsentState has no consents', async () => { + const consentState: ConsentState = { + tcfv2: tcfv2ConsentStateNoConsent, + canTarget: false, + framework: 'tcfv2', + }; + mockOnConsentChange(consentState); + const hasConsent = await _private.hasConsentForUseCase('Essential'); + expect(hasConsent).toEqual(true); + }); +}); + + +describe('local storage returns the expected consent', () => { + let mockContains:any; + + beforeEach(() => { + mockContains = 'someTestData'; + + (storageStub.local.get as jest.Mock).mockImplementation((key:string) => { + if (key === 'gu.mock') {return mockContains} + else {return(null)} + }); + + (storageStub.local.set as jest.Mock).mockImplementation((key:string, data:unknown) => { + if (key === 'gu.mock') {mockContains = data;} + }); + }); + + test('Targeted advertising get local storage returns null when canTarget is false', async () => { const consentState: ConsentState = { tcfv2: tcfv2ConsentState, - canTarget: true, + canTarget: false, framework: 'tcfv2', }; mockOnConsentChange(consentState); - const hasConsent = await _private.hasConsentForUseCase('Targeted advertising'); - expect(hasConsent).toEqual(true); + const localStorageValue = await cmpGetLocalStorageItem('Targeted advertising', 'gu.mock'); + expect(localStorageValue).toEqual(null); }); - test('Targeted advertising when canTarget is false', async () => { + test('Targeted advertising can set and get local storage value when canTarget is true', async () => { const consentState: ConsentState = { tcfv2: tcfv2ConsentState, - canTarget: false, + canTarget: true, framework: 'tcfv2', }; mockOnConsentChange(consentState); - const hasConsent = await _private.hasConsentForUseCase('Targeted advertising'); - expect(hasConsent).toEqual(false); + const localStorageValueDefault = await cmpGetLocalStorageItem('Targeted advertising', 'gu.mock'); + expect(localStorageValueDefault).toEqual('someTestData'); + await cmpSetLocalStorageItem('Essential', 'gu.mock', 'testdataAd'); + const localStorageValue = await cmpGetLocalStorageItem('Targeted advertising', 'gu.mock'); + expect(localStorageValue).toEqual('testdataAd'); }); - test('Targeted advertising when canTarget is true', async () => { + test('Essential can set and get local storage when no consents', async () => { const consentState: ConsentState = { - tcfv2: tcfv2ConsentState, - canTarget: true, + tcfv2: tcfv2ConsentStateNoConsent, + canTarget: false, framework: 'tcfv2', }; mockOnConsentChange(consentState); - const hasConsent = await _private.hasConsentForUseCase('Targeted advertising'); - expect(hasConsent).toEqual(true); + const localStorageValueDefault = await cmpGetLocalStorageItem('Essential', 'gu.mock'); + expect(localStorageValueDefault).toEqual('someTestData'); + await cmpSetLocalStorageItem('Essential', 'gu.mock', 'testdata'); + const localStorageValue = await cmpGetLocalStorageItem('Essential', 'gu.mock'); + expect(localStorageValue).toEqual('testdata'); }); - test('Essential when no consents', async () => { + test('get null if local storage item does not exist', async () => { const consentState: ConsentState = { tcfv2: tcfv2ConsentStateNoConsent, canTarget: false, framework: 'tcfv2', }; mockOnConsentChange(consentState); - const hasConsent = await _private.hasConsentForUseCase('Essential'); - expect(hasConsent).toEqual(true); + const localStorageValue = await cmpGetLocalStorageItem('Essential', 'gu.does_not_exist'); + expect(localStorageValue).toEqual(null); + }); +}); + + +describe('session storage returns the expected consent', () => { + let mockContains:any; + + beforeEach(() => { + mockContains = 'someTestData'; + + (storageStub.session.get as jest.Mock).mockImplementation((key:string) => { + if (key === 'gu.mock') {return mockContains} + else {return(null)} + }); + + (storageStub.session.set as jest.Mock).mockImplementation((key:string, data:unknown) => { + if (key === 'gu.mock') {mockContains = data;} + }); + }); + + test('Targeted advertising get session storage returns null when canTarget is false', async () => { + const consentState: ConsentState = { + tcfv2: tcfv2ConsentState, + canTarget: false, + framework: 'tcfv2', + }; + mockOnConsentChange(consentState); + const sessionStorageValue = await cmpGetSessionStorageItem('Targeted advertising', 'gu.mock'); + expect(sessionStorageValue).toEqual(null); }); - test('Targeted advertising local storage when canTarget is false', async () => { + test('Targeted advertising can set and get session storage value when canTarget is true', async () => { const consentState: ConsentState = { tcfv2: tcfv2ConsentState, + canTarget: true, + framework: 'tcfv2', + }; + mockOnConsentChange(consentState); + const sessionStorageValueDefault = await cmpGetSessionStorageItem('Targeted advertising', 'gu.mock'); + expect(sessionStorageValueDefault).toEqual('someTestData'); + await cmpSetSessionStorageItem('Essential', 'gu.mock', 'testdataAd'); + const sessionStorageValue = await cmpGetSessionStorageItem('Targeted advertising', 'gu.mock'); + expect(sessionStorageValue).toEqual('testdataAd'); + }); + test('Essential can set and get session storage when no consents', async () => { + const consentState: ConsentState = { + tcfv2: tcfv2ConsentStateNoConsent, canTarget: false, framework: 'tcfv2', }; mockOnConsentChange(consentState); - const localStorageValue = await cmpGetLocalStorageItem('Targeted advertising', 'mockLocal'); - expect(localStorageValue).toEqual(null); + const sessionStorageValueDefault = await cmpGetSessionStorageItem('Essential', 'gu.mock'); + expect(sessionStorageValueDefault).toEqual('someTestData'); + await cmpSetSessionStorageItem('Essential', 'gu.mock', 'testdata'); + const sessionStorageValue = await cmpGetSessionStorageItem('Essential', 'gu.mock'); + expect(sessionStorageValue).toEqual('testdata'); }); - test('Essential local storage when no consents', async () => { + test('get null if session storage item does not exist', async () => { const consentState: ConsentState = { tcfv2: tcfv2ConsentStateNoConsent, canTarget: false, framework: 'tcfv2', }; mockOnConsentChange(consentState); - const localStorageValue = await cmpGetLocalStorageItem('Essential', 'mockLocal'); - expect(localStorageValue).toEqual(null); //TODO: need to mock the call to localStorage + const sessionStorageValue = await cmpGetSessionStorageItem('Essential', 'gu.does_not_exist'); + expect(sessionStorageValue).toEqual(null); }); }); + + +describe('cookies return the expected consent', () => { +}); diff --git a/src/cmpStorage.ts b/src/cmpStorage.ts index 3f7a8efb3..3ca17f990 100644 --- a/src/cmpStorage.ts +++ b/src/cmpStorage.ts @@ -60,7 +60,7 @@ export const cmpSetLocalStorageItem = async (useCase: UseCases, storageItem: str if(await hasConsentForUseCase(useCase)) { - return storage.local.set(storageItem, expires) + return storage.local.set(storageItem, value, expires) } else { @@ -89,7 +89,7 @@ export const cmpSetSessionStorageItem = async (useCase: UseCases, storageItem: s if(await hasConsentForUseCase(useCase)) { - return storage.session.set(storageItem, expires) + return storage.session.set(storageItem, value, expires) } else { From 81a9853c3d58c60c8437043903a4aa5ed1ad16f8 Mon Sep 17 00:00:00 2001 From: Anna Voelker Date: Tue, 19 Dec 2023 13:00:38 +0000 Subject: [PATCH 08/17] add cookie tests --- src/cmpStorage.test.ts | 71 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 68 insertions(+), 3 deletions(-) diff --git a/src/cmpStorage.test.ts b/src/cmpStorage.test.ts index 6e84caf17..9e93e4e75 100644 --- a/src/cmpStorage.test.ts +++ b/src/cmpStorage.test.ts @@ -1,9 +1,8 @@ import { onConsentChange } from './onConsentChange'; import type { Callback, ConsentState } from './types'; import type { TCFv2ConsentState } from './types/tcfv2'; -import {cmpGetLocalStorageItem, cmpSetLocalStorageItem, cmpGetSessionStorageItem, cmpSetSessionStorageItem, _private} from './cmpStorage'; -//import { getCookie as getCookie_ } from 'lib/cookies'; -import { storage as storageStub } from '@guardian/libs'; +import {cmpGetLocalStorageItem, cmpSetLocalStorageItem, cmpGetSessionStorageItem, cmpSetSessionStorageItem, cmpGetCookie, cmpSetCookie, _private } from './cmpStorage'; +import { getCookie as getCookie_, setCookie as setCookie_, storage as storageStub} from '@guardian/libs'; jest.mock('./onConsentChange'); jest.mock('@guardian/libs', () => ({ @@ -209,4 +208,70 @@ describe('session storage returns the expected consent', () => { describe('cookies return the expected consent', () => { + let mockContains:any; + + beforeEach(() => { + mockContains = 'someTestData'; + + (getCookie_ as jest.Mock).mockImplementation(({name }: { + name: string; + }) => { + if (name === 'gu.mock') {return mockContains} + else {return(null)} + }); + + (setCookie_ as jest.Mock).mockImplementation(({ name, value }: { + name: string; + value: string; + }) => { + if (name === 'gu.mock') {mockContains = value;} + }); + }); + + test('Targeted advertising get cookie returns null when canTarget is false', async () => { + const consentState: ConsentState = { + tcfv2: tcfv2ConsentState, + canTarget: false, + framework: 'tcfv2', + }; + mockOnConsentChange(consentState); + const cookieValue = await cmpGetCookie({useCase: 'Targeted advertising', name: 'gu.mock'}); + expect(cookieValue).toEqual(null); + }); + test('Targeted advertising can set and get cookies when canTarget is true', async () => { + const consentState: ConsentState = { + tcfv2: tcfv2ConsentState, + canTarget: true, + framework: 'tcfv2', + }; + mockOnConsentChange(consentState); + const cookieValueDefault = await cmpGetCookie({useCase: 'Targeted advertising', name: 'gu.mock'}); + expect(cookieValueDefault).toEqual('someTestData'); + await cmpSetCookie({useCase: 'Essential', name: 'gu.mock', value: 'testdataAd'}); + const cookieValue = await cmpGetCookie({useCase: 'Targeted advertising', name: 'gu.mock'}); + expect(cookieValue).toEqual('testdataAd'); + }); + test('Essential can set and get cookies when no consents', async () => { + const consentState: ConsentState = { + tcfv2: tcfv2ConsentStateNoConsent, + canTarget: false, + framework: 'tcfv2', + }; + mockOnConsentChange(consentState); + const cookieValueDefault = await cmpGetCookie({useCase: 'Essential', name:'gu.mock'}); + expect(cookieValueDefault).toEqual('someTestData'); + await cmpSetCookie({useCase: 'Essential', name: 'gu.mock', value: 'testdata'}); + const cookieValue = await cmpGetCookie({useCase: 'Essential', name: 'gu.mock'}); + expect(cookieValue).toEqual('testdata'); + }); + test('get null if cookie does not exist', async () => { + const consentState: ConsentState = { + tcfv2: tcfv2ConsentStateNoConsent, + canTarget: false, + framework: 'tcfv2', + }; + mockOnConsentChange(consentState); + const cookieValue = await cmpGetCookie({useCase: 'Essential', name: 'gu.does_not_exist'}); + expect(cookieValue).toEqual(null); + }); }); From 46f93d8bb7cbc4f73d29f473532fd922475a48f9 Mon Sep 17 00:00:00 2001 From: Anna Voelker Date: Tue, 19 Dec 2023 16:40:53 +0000 Subject: [PATCH 09/17] no private needed --- src/cmpStorage.test.ts | 8 ++++---- src/cmpStorage.ts | 13 ++++--------- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/src/cmpStorage.test.ts b/src/cmpStorage.test.ts index 9e93e4e75..41a622aea 100644 --- a/src/cmpStorage.test.ts +++ b/src/cmpStorage.test.ts @@ -1,7 +1,7 @@ import { onConsentChange } from './onConsentChange'; import type { Callback, ConsentState } from './types'; import type { TCFv2ConsentState } from './types/tcfv2'; -import {cmpGetLocalStorageItem, cmpSetLocalStorageItem, cmpGetSessionStorageItem, cmpSetSessionStorageItem, cmpGetCookie, cmpSetCookie, _private } from './cmpStorage'; +import {cmpGetLocalStorageItem, cmpSetLocalStorageItem, cmpGetSessionStorageItem, cmpSetSessionStorageItem, cmpGetCookie, cmpSetCookie, hasConsentForUseCase } from './cmpStorage'; import { getCookie as getCookie_, setCookie as setCookie_, storage as storageStub} from '@guardian/libs'; jest.mock('./onConsentChange'); @@ -51,7 +51,7 @@ describe('cmpStorage.hasConsentForUseCase returns the expected consent', () => { framework: 'tcfv2', }; mockOnConsentChange(consentState); - const hasConsent = await _private.hasConsentForUseCase('Targeted advertising'); + const hasConsent = await hasConsentForUseCase('Targeted advertising'); expect(hasConsent).toEqual(true); }); test('Targeted advertising has no consent when canTarget is false', async () => { @@ -61,7 +61,7 @@ describe('cmpStorage.hasConsentForUseCase returns the expected consent', () => { framework: 'tcfv2', }; mockOnConsentChange(consentState); - const hasConsent = await _private.hasConsentForUseCase('Targeted advertising'); + const hasConsent = await hasConsentForUseCase('Targeted advertising'); expect(hasConsent).toEqual(false); }); test('Essential has consent even when ConsentState has no consents', async () => { @@ -71,7 +71,7 @@ describe('cmpStorage.hasConsentForUseCase returns the expected consent', () => { framework: 'tcfv2', }; mockOnConsentChange(consentState); - const hasConsent = await _private.hasConsentForUseCase('Essential'); + const hasConsent = await hasConsentForUseCase('Essential'); expect(hasConsent).toEqual(true); }); }); diff --git a/src/cmpStorage.ts b/src/cmpStorage.ts index 3ca17f990..7ce427dcd 100644 --- a/src/cmpStorage.ts +++ b/src/cmpStorage.ts @@ -1,5 +1,5 @@ import { storage, getCookie, setCookie } from '@guardian/libs'; -import { onConsent } from '.'; +import { onConsent } from './onConsent'; export const UseCaseOptions = [ "Targeted advertising", @@ -7,7 +7,7 @@ export const UseCaseOptions = [ ] as const; export type UseCases = typeof UseCaseOptions[number]; -const hasConsentForUseCase = async (useCase: UseCases): Promise => +export const hasConsentForUseCase = async (useCase: UseCases): Promise => { const consentState = await onConsent(); @@ -33,8 +33,8 @@ const hasConsentForUseCase = async (useCase: UseCases): Promise => // && consentState.tcfv2.consents['3'])//Need the correct list of consents, this is just an example //|| (!consentState.ccpa?.doNotSell) //|| (consentState.aus?.personalisedAdvertising) - case "Essential": return(true) - default: return(false) + case "Essential": return(true) //could check for allow-list of essential cookies/storage here in the future + default: return(false) //do I need this line given that use-case is essentially an enum? } } @@ -136,10 +136,5 @@ export const cmpSetCookie = async ({ useCase, name, value, daysToLive, isCrossSu } }; -export const _private = { - hasConsentForUseCase -}; - - //await cmpGetLocalStorageItem("Targeted advertising", "dep") //await cmpGetLocalStorageItem("invalid", "dep") From 3eff973e8222bc48f00c37d758a5b516f94371fe Mon Sep 17 00:00:00 2001 From: Anna Voelker Date: Tue, 19 Dec 2023 18:29:14 +0000 Subject: [PATCH 10/17] add targeted marketing --- src/cmpStorage.ts | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/src/cmpStorage.ts b/src/cmpStorage.ts index 7ce427dcd..1a40f6ea8 100644 --- a/src/cmpStorage.ts +++ b/src/cmpStorage.ts @@ -1,8 +1,17 @@ import { storage, getCookie, setCookie } from '@guardian/libs'; import { onConsent } from './onConsent'; +/** + * List of permitted use-cases for cookies/storage: + * + * - `Targeted advertising`: if the user can be targeted for personalised advertising according to the active consent framework + * - 'Targeted marketing': if the user can be targeted for personalised marketing, e.g. article count + * - `Essential`: if essential cookies/storage can be used according to the active consent framework + * + */ export const UseCaseOptions = [ "Targeted advertising", + "Targeted marketing", "Essential" ] as const; export type UseCases = typeof UseCaseOptions[number]; @@ -27,14 +36,18 @@ export const hasConsentForUseCase = async (useCase: UseCases): Promise switch(useCase) { case "Targeted advertising": return(consentState.canTarget) - //could be more granular than this, for example by using explicit tcf purposes: - //(consentState.tcfv2?.consents['1'] - // && consentState.tcfv2.consents['2'] - // && consentState.tcfv2.consents['3'])//Need the correct list of consents, this is just an example - //|| (!consentState.ccpa?.doNotSell) - //|| (consentState.aus?.personalisedAdvertising) - case "Essential": return(true) //could check for allow-list of essential cookies/storage here in the future - default: return(false) //do I need this line given that use-case is essentially an enum? + case "Targeted marketing":{ + if(( + consentState.tcfv2?.consents['1'] + && consentState.tcfv2?.consents['3'] + && consentState.tcfv2?.consents['7']) + || !consentState.ccpa?.doNotSell + || consentState.aus?.personalisedAdvertising) + return(true) + else return(false) + } + case "Essential": return(true) //could check for allow-list of essential cookies/storage here in the future + default: return(false) } } From 3ba5a9ad4cf1fb50891225c50328601a72d47d67 Mon Sep 17 00:00:00 2001 From: Anna Voelker Date: Thu, 21 Dec 2023 13:01:48 +0000 Subject: [PATCH 11/17] refactor and wrap all exported storage from libs --- src/cmpCookies.tests.ts | 113 ++++++++++++ src/cmpCookies.ts | 62 +++++++ src/cmpStorage.test.ts | 142 +++------------ src/cmpStorage.ts | 298 +++++++++++++++++-------------- src/hasConsentForUseCase.test.ts | 78 ++++++++ src/hasConsentForUseCase.ts | 38 ++++ src/index.ts | 3 +- src/types/consentUseCases.ts | 14 ++ 8 files changed, 492 insertions(+), 256 deletions(-) create mode 100644 src/cmpCookies.tests.ts create mode 100644 src/cmpCookies.ts create mode 100644 src/hasConsentForUseCase.test.ts create mode 100644 src/hasConsentForUseCase.ts create mode 100644 src/types/consentUseCases.ts diff --git a/src/cmpCookies.tests.ts b/src/cmpCookies.tests.ts new file mode 100644 index 000000000..84178e507 --- /dev/null +++ b/src/cmpCookies.tests.ts @@ -0,0 +1,113 @@ +import { onConsentChange } from './onConsentChange'; +import type { Callback, ConsentState } from './types'; +import type { TCFv2ConsentState } from './types/tcfv2'; +import {cmpGetCookie, cmpSetCookie } from './cmpCookies'; +import { getCookie as getCookie_, setCookie as setCookie_} from '@guardian/libs'; + +jest.mock('./onConsentChange'); +jest.mock('@guardian/libs', () => ({ + getCookie: jest.fn(), + setCookie: jest.fn(), + storage: { + local: { + get: jest.fn(), + set: jest.fn(), + }, + session: { + get: jest.fn(), + set: jest.fn(), + }, + } +})); + +const tcfv2ConsentState: TCFv2ConsentState = { + consents: { 1: true }, + eventStatus: 'tcloaded', + vendorConsents: { + ['5efefe25b8e05c06542b2a77']: true, + }, + addtlConsent: 'xyz', + gdprApplies: true, + tcString: 'YAAA', +}; + +const tcfv2ConsentStateNoConsent: TCFv2ConsentState = { + consents: { 1: false }, + eventStatus: 'tcloaded', + vendorConsents: {}, + addtlConsent: 'xyz', + gdprApplies: true, + tcString: 'YAAA', +}; + +const mockOnConsentChange = (consentState: ConsentState) => + (onConsentChange as jest.Mock).mockImplementation((cb: Callback) => cb(consentState)); + +describe('cookies return the expected consent', () => { + let mockContains:any; + + beforeEach(() => { + mockContains = 'someTestData'; + + (getCookie_ as jest.Mock).mockImplementation(({name }: { + name: string; + }) => { + if (name === 'gu.mock') {return mockContains} + else {return(null)} + }); + + (setCookie_ as jest.Mock).mockImplementation(({ name, value }: { + name: string; + value: string; + }) => { + if (name === 'gu.mock') {mockContains = value;} + }); + }); + + test('Targeted advertising get cookie returns null when canTarget is false', async () => { + const consentState: ConsentState = { + tcfv2: tcfv2ConsentState, + canTarget: false, + framework: 'tcfv2', + }; + mockOnConsentChange(consentState); + const cookieValue = await cmpGetCookie({useCase: 'Targeted advertising', name: 'gu.mock'}); + expect(cookieValue).toEqual(null); + }); + test('Targeted advertising can set and get cookies when canTarget is true', async () => { + const consentState: ConsentState = { + tcfv2: tcfv2ConsentState, + canTarget: true, + framework: 'tcfv2', + }; + mockOnConsentChange(consentState); + const cookieValueDefault = await cmpGetCookie({useCase: 'Targeted advertising', name: 'gu.mock'}); + expect(cookieValueDefault).toEqual('someTestData'); + await cmpSetCookie({useCase: 'Essential', name: 'gu.mock', value: 'testdataAd'}); + const cookieValue = await cmpGetCookie({useCase: 'Targeted advertising', name: 'gu.mock'}); + expect(cookieValue).toEqual('testdataAd'); + }); + test('Essential can set and get cookies when no consents', async () => { + const consentState: ConsentState = { + tcfv2: tcfv2ConsentStateNoConsent, + canTarget: false, + framework: 'tcfv2', + }; + mockOnConsentChange(consentState); + const cookieValueDefault = await cmpGetCookie({useCase: 'Essential', name:'gu.mock'}); + expect(cookieValueDefault).toEqual('someTestData'); + await cmpSetCookie({useCase: 'Essential', name: 'gu.mock', value: 'testdata'}); + const cookieValue = await cmpGetCookie({useCase: 'Essential', name: 'gu.mock'}); + expect(cookieValue).toEqual('testdata'); + }); + test('get null if cookie does not exist', async () => { + const consentState: ConsentState = { + tcfv2: tcfv2ConsentStateNoConsent, + canTarget: false, + framework: 'tcfv2', + }; + mockOnConsentChange(consentState); + const cookieValue = await cmpGetCookie({useCase: 'Essential', name: 'gu.does_not_exist'}); + expect(cookieValue).toEqual(null); + }); +}); diff --git a/src/cmpCookies.ts b/src/cmpCookies.ts new file mode 100644 index 000000000..dfe691c65 --- /dev/null +++ b/src/cmpCookies.ts @@ -0,0 +1,62 @@ +import { getCookie, setCookie, setSessionCookie } from '@guardian/libs'; +import type { ConsentUseCases } from './types/consentUseCases'; +import { hasConsentForUseCase } from './hasConsentForUseCase'; + +//TODO: Write wrappers for the other cookie functions in @guardian/libs + +export const cmpGetCookie = async({ useCase, name, shouldMemoize, }: { + useCase: ConsentUseCases, + name: string; + shouldMemoize?: boolean | undefined; +}): Promise => +{ + console.log('in cmpGetCookie'); + + if(await hasConsentForUseCase(useCase)) + { + return getCookie({name: name, shouldMemoize: shouldMemoize}) + } + else + { + console.error('cmp', `Cannot get cookie ${name} due to missing consent for use-case ${useCase}`) + return(null) + } +}; + +export const cmpSetCookie = async ({ useCase, name, value, daysToLive, isCrossSubdomain, }: { + useCase: ConsentUseCases, + name: string; + value: string; + daysToLive?: number | undefined; + isCrossSubdomain?: boolean | undefined; +}): Promise => +{ + console.log('in cmpSetCookie'); + + if(await hasConsentForUseCase(useCase)) + { + setCookie({name:name, value:value, daysToLive:daysToLive, isCrossSubdomain:isCrossSubdomain}) + } + else + { + console.error('cmp', `Cannot set cookie ${name} due to missing consent for use-case ${useCase}`) + } +}; + +export const cmpSetSessionCookie = async ({ useCase, name, value }: { + useCase: ConsentUseCases, + name: string; + value: string; +}): Promise => +{ + console.log('in cmpSetSessionCookie'); + + if(await hasConsentForUseCase(useCase)) + { + setSessionCookie({name:name, value:value}) + } + else + { + console.error('cmp', `Cannot set cookie ${name} due to missing consent for use-case ${useCase}`) + } +}; diff --git a/src/cmpStorage.test.ts b/src/cmpStorage.test.ts index 41a622aea..533ffbd11 100644 --- a/src/cmpStorage.test.ts +++ b/src/cmpStorage.test.ts @@ -1,8 +1,10 @@ import { onConsentChange } from './onConsentChange'; import type { Callback, ConsentState } from './types'; import type { TCFv2ConsentState } from './types/tcfv2'; -import {cmpGetLocalStorageItem, cmpSetLocalStorageItem, cmpGetSessionStorageItem, cmpSetSessionStorageItem, cmpGetCookie, cmpSetCookie, hasConsentForUseCase } from './cmpStorage'; -import { getCookie as getCookie_, setCookie as setCookie_, storage as storageStub} from '@guardian/libs'; +import {storage} from './cmpStorage'; +import { storage as storageStub} from '@guardian/libs'; + +//TODO: add tests for all use-cases and all other storage functions jest.mock('./onConsentChange'); jest.mock('@guardian/libs', () => ({ @@ -43,40 +45,6 @@ const tcfv2ConsentStateNoConsent: TCFv2ConsentState = { const mockOnConsentChange = (consentState: ConsentState) => (onConsentChange as jest.Mock).mockImplementation((cb: Callback) => cb(consentState)); -describe('cmpStorage.hasConsentForUseCase returns the expected consent', () => { - test('Targeted advertising has consent when canTarget is true', async () => { - const consentState: ConsentState = { - tcfv2: tcfv2ConsentState, - canTarget: true, - framework: 'tcfv2', - }; - mockOnConsentChange(consentState); - const hasConsent = await hasConsentForUseCase('Targeted advertising'); - expect(hasConsent).toEqual(true); - }); - test('Targeted advertising has no consent when canTarget is false', async () => { - const consentState: ConsentState = { - tcfv2: tcfv2ConsentState, - canTarget: false, - framework: 'tcfv2', - }; - mockOnConsentChange(consentState); - const hasConsent = await hasConsentForUseCase('Targeted advertising'); - expect(hasConsent).toEqual(false); - }); - test('Essential has consent even when ConsentState has no consents', async () => { - const consentState: ConsentState = { - tcfv2: tcfv2ConsentStateNoConsent, - canTarget: false, - framework: 'tcfv2', - }; - mockOnConsentChange(consentState); - const hasConsent = await hasConsentForUseCase('Essential'); - expect(hasConsent).toEqual(true); - }); -}); - - describe('local storage returns the expected consent', () => { let mockContains:any; @@ -100,7 +68,7 @@ describe('local storage returns the expected consent', () => { framework: 'tcfv2', }; mockOnConsentChange(consentState); - const localStorageValue = await cmpGetLocalStorageItem('Targeted advertising', 'gu.mock'); + const localStorageValue = await storage.local.get('Targeted advertising', 'gu.mock'); expect(localStorageValue).toEqual(null); }); test('Targeted advertising can set and get local storage value when canTarget is true', async () => { @@ -110,10 +78,10 @@ describe('local storage returns the expected consent', () => { framework: 'tcfv2', }; mockOnConsentChange(consentState); - const localStorageValueDefault = await cmpGetLocalStorageItem('Targeted advertising', 'gu.mock'); + const localStorageValueDefault = await storage.local.get('Targeted advertising', 'gu.mock'); expect(localStorageValueDefault).toEqual('someTestData'); - await cmpSetLocalStorageItem('Essential', 'gu.mock', 'testdataAd'); - const localStorageValue = await cmpGetLocalStorageItem('Targeted advertising', 'gu.mock'); + await storage.local.set('Essential', 'gu.mock', 'testdataAd'); + const localStorageValue = await storage.local.get('Targeted advertising', 'gu.mock'); expect(localStorageValue).toEqual('testdataAd'); }); test('Essential can set and get local storage when no consents', async () => { @@ -123,10 +91,10 @@ describe('local storage returns the expected consent', () => { framework: 'tcfv2', }; mockOnConsentChange(consentState); - const localStorageValueDefault = await cmpGetLocalStorageItem('Essential', 'gu.mock'); + const localStorageValueDefault = await storage.local.get('Essential', 'gu.mock'); expect(localStorageValueDefault).toEqual('someTestData'); - await cmpSetLocalStorageItem('Essential', 'gu.mock', 'testdata'); - const localStorageValue = await cmpGetLocalStorageItem('Essential', 'gu.mock'); + await storage.local.set('Essential', 'gu.mock', 'testdata'); + const localStorageValue = await storage.local.get('Essential', 'gu.mock'); expect(localStorageValue).toEqual('testdata'); }); test('get null if local storage item does not exist', async () => { @@ -136,7 +104,7 @@ describe('local storage returns the expected consent', () => { framework: 'tcfv2', }; mockOnConsentChange(consentState); - const localStorageValue = await cmpGetLocalStorageItem('Essential', 'gu.does_not_exist'); + const localStorageValue = await storage.local.get('Essential', 'gu.does_not_exist'); expect(localStorageValue).toEqual(null); }); }); @@ -165,7 +133,7 @@ describe('session storage returns the expected consent', () => { framework: 'tcfv2', }; mockOnConsentChange(consentState); - const sessionStorageValue = await cmpGetSessionStorageItem('Targeted advertising', 'gu.mock'); + const sessionStorageValue = await storage.session.get('Targeted advertising', 'gu.mock'); expect(sessionStorageValue).toEqual(null); }); test('Targeted advertising can set and get session storage value when canTarget is true', async () => { @@ -175,10 +143,10 @@ describe('session storage returns the expected consent', () => { framework: 'tcfv2', }; mockOnConsentChange(consentState); - const sessionStorageValueDefault = await cmpGetSessionStorageItem('Targeted advertising', 'gu.mock'); + const sessionStorageValueDefault = await storage.session.get('Targeted advertising', 'gu.mock'); expect(sessionStorageValueDefault).toEqual('someTestData'); - await cmpSetSessionStorageItem('Essential', 'gu.mock', 'testdataAd'); - const sessionStorageValue = await cmpGetSessionStorageItem('Targeted advertising', 'gu.mock'); + await storage.session.set('Essential', 'gu.mock', 'testdataAd'); + const sessionStorageValue = await storage.session.get('Targeted advertising', 'gu.mock'); expect(sessionStorageValue).toEqual('testdataAd'); }); test('Essential can set and get session storage when no consents', async () => { @@ -188,10 +156,10 @@ describe('session storage returns the expected consent', () => { framework: 'tcfv2', }; mockOnConsentChange(consentState); - const sessionStorageValueDefault = await cmpGetSessionStorageItem('Essential', 'gu.mock'); + const sessionStorageValueDefault = await storage.session.get('Essential', 'gu.mock'); expect(sessionStorageValueDefault).toEqual('someTestData'); - await cmpSetSessionStorageItem('Essential', 'gu.mock', 'testdata'); - const sessionStorageValue = await cmpGetSessionStorageItem('Essential', 'gu.mock'); + await storage.session.set('Essential', 'gu.mock', 'testdata'); + const sessionStorageValue = await storage.session.get('Essential', 'gu.mock'); expect(sessionStorageValue).toEqual('testdata'); }); test('get null if session storage item does not exist', async () => { @@ -201,77 +169,7 @@ describe('session storage returns the expected consent', () => { framework: 'tcfv2', }; mockOnConsentChange(consentState); - const sessionStorageValue = await cmpGetSessionStorageItem('Essential', 'gu.does_not_exist'); + const sessionStorageValue = await storage.session.get('Essential', 'gu.does_not_exist'); expect(sessionStorageValue).toEqual(null); }); }); - - -describe('cookies return the expected consent', () => { - let mockContains:any; - - beforeEach(() => { - mockContains = 'someTestData'; - - (getCookie_ as jest.Mock).mockImplementation(({name }: { - name: string; - }) => { - if (name === 'gu.mock') {return mockContains} - else {return(null)} - }); - - (setCookie_ as jest.Mock).mockImplementation(({ name, value }: { - name: string; - value: string; - }) => { - if (name === 'gu.mock') {mockContains = value;} - }); - }); - - test('Targeted advertising get cookie returns null when canTarget is false', async () => { - const consentState: ConsentState = { - tcfv2: tcfv2ConsentState, - canTarget: false, - framework: 'tcfv2', - }; - mockOnConsentChange(consentState); - const cookieValue = await cmpGetCookie({useCase: 'Targeted advertising', name: 'gu.mock'}); - expect(cookieValue).toEqual(null); - }); - test('Targeted advertising can set and get cookies when canTarget is true', async () => { - const consentState: ConsentState = { - tcfv2: tcfv2ConsentState, - canTarget: true, - framework: 'tcfv2', - }; - mockOnConsentChange(consentState); - const cookieValueDefault = await cmpGetCookie({useCase: 'Targeted advertising', name: 'gu.mock'}); - expect(cookieValueDefault).toEqual('someTestData'); - await cmpSetCookie({useCase: 'Essential', name: 'gu.mock', value: 'testdataAd'}); - const cookieValue = await cmpGetCookie({useCase: 'Targeted advertising', name: 'gu.mock'}); - expect(cookieValue).toEqual('testdataAd'); - }); - test('Essential can set and get cookies when no consents', async () => { - const consentState: ConsentState = { - tcfv2: tcfv2ConsentStateNoConsent, - canTarget: false, - framework: 'tcfv2', - }; - mockOnConsentChange(consentState); - const cookieValueDefault = await cmpGetCookie({useCase: 'Essential', name:'gu.mock'}); - expect(cookieValueDefault).toEqual('someTestData'); - await cmpSetCookie({useCase: 'Essential', name: 'gu.mock', value: 'testdata'}); - const cookieValue = await cmpGetCookie({useCase: 'Essential', name: 'gu.mock'}); - expect(cookieValue).toEqual('testdata'); - }); - test('get null if cookie does not exist', async () => { - const consentState: ConsentState = { - tcfv2: tcfv2ConsentStateNoConsent, - canTarget: false, - framework: 'tcfv2', - }; - mockOnConsentChange(consentState); - const cookieValue = await cmpGetCookie({useCase: 'Essential', name: 'gu.does_not_exist'}); - expect(cookieValue).toEqual(null); - }); -}); diff --git a/src/cmpStorage.ts b/src/cmpStorage.ts index 1a40f6ea8..7a9563b9f 100644 --- a/src/cmpStorage.ts +++ b/src/cmpStorage.ts @@ -1,153 +1,185 @@ -import { storage, getCookie, setCookie } from '@guardian/libs'; -import { onConsent } from './onConsent'; +import { storage as libsStorage } from '@guardian/libs'; +import type { ConsentUseCases } from './types/consentUseCases'; +import { hasConsentForUseCase } from './hasConsentForUseCase'; -/** - * List of permitted use-cases for cookies/storage: - * - * - `Targeted advertising`: if the user can be targeted for personalised advertising according to the active consent framework - * - 'Targeted marketing': if the user can be targeted for personalised marketing, e.g. article count - * - `Essential`: if essential cookies/storage can be used according to the active consent framework - * - */ -export const UseCaseOptions = [ - "Targeted advertising", - "Targeted marketing", - "Essential" +export const storageOptions = [ + "localStorage", + "sessionStorage" ] as const; -export type UseCases = typeof UseCaseOptions[number]; - -export const hasConsentForUseCase = async (useCase: UseCases): Promise => -{ - const consentState = await onConsent(); - - /*console.log(`consentState.tcfv2?.consents['1']: ${consentState.tcfv2?.consents['1']}`); - console.log(`consentState.tcfv2?.consents['2']: ${consentState.tcfv2?.consents['2']}`); - console.log(`consentState.tcfv2?.consents['3']: ${consentState.tcfv2?.consents['3']}`); - console.log(`consentState.tcfv2?.consents['4']: ${consentState.tcfv2?.consents['4']}`); - console.log(`consentState.tcfv2?.consents['5']: ${consentState.tcfv2?.consents['5']}`); - console.log(`consentState.tcfv2?.consents['6']: ${consentState.tcfv2?.consents['6']}`); - console.log(`consentState.tcfv2?.consents['7']: ${consentState.tcfv2?.consents['7']}`); - console.log(`consentState.tcfv2?.consents['8']: ${consentState.tcfv2?.consents['8']}`); - console.log(`consentState.tcfv2?.consents['9']: ${consentState.tcfv2?.consents['9']}`); - console.log(`consentState.tcfv2?.consents['10']: ${consentState.tcfv2?.consents['10']}`); - console.log(`consentState.tcfv2?.consents['11']: ${consentState.tcfv2?.consents['11']}`); - console.log(`consentState.canTarget: ${consentState.canTarget}`); - */ - - switch(useCase) { - case "Targeted advertising": return(consentState.canTarget) - case "Targeted marketing":{ - if(( - consentState.tcfv2?.consents['1'] - && consentState.tcfv2?.consents['3'] - && consentState.tcfv2?.consents['7']) - || !consentState.ccpa?.doNotSell - || consentState.aus?.personalisedAdvertising) - return(true) - else return(false) - } - case "Essential": return(true) //could check for allow-list of essential cookies/storage here in the future - default: return(false) - } - -} +export type StorageOptions = typeof storageOptions[number]; +class StorageFactory { + #storageHandler: StorageOptions; -export const cmpGetLocalStorageItem = async (useCase: UseCases, storageItem: string): Promise => -{ - console.log('in cmpGetLocalStorageItem'); - - if(await hasConsentForUseCase(useCase)) - { - return storage.local.get(storageItem) + constructor(storageHandler: StorageOptions) { + this.#storageHandler = storageHandler; } - else - { - console.error('cmp', `Cannot get local storage item ${storageItem} due to missing consent for use-case ${useCase}`) - return(null) + + /** + * Check whether storage is available. + */ + isAvailable(): boolean { + switch(this.#storageHandler) { + case 'localStorage': { + return Boolean(libsStorage.local.isAvailable()) + } + case 'sessionStorage': { + return Boolean(libsStorage.session.isAvailable()) + } + } } -}; -export const cmpSetLocalStorageItem = async (useCase: UseCases, storageItem: string, value:unknown, expires?: string | number | Date): Promise => -{ - console.log('in cmpSetLocalStorageItem'); + /** + * Retrieve an item from storage. + * + * @param key - the name of the item + */ + async get(useCase: ConsentUseCases, key: string): Promise { + console.log('in cmp get storage'); + if(await hasConsentForUseCase(useCase)) + { + switch(this.#storageHandler) { + case 'localStorage': { + console.log('in cmp get local'); + return libsStorage.local.get(key) + } + case 'sessionStorage': { + console.log('in cmp get session'); + return libsStorage.session.get(key) + } + } + } + else + { + console.error('cmp', `Cannot get local storage item ${key} due to missing consent for use-case ${useCase}`) + return(null) + } + } - if(await hasConsentForUseCase(useCase)) - { - return storage.local.set(storageItem, value, expires) + /** + * Save a value to storage. + * + * @param key - the name of the item + * @param value - the data to save + * @param expires - optional date on which this data will expire + */ + async set(useCase:ConsentUseCases, key: string, value: unknown, expires?: string | number | Date): Promise { + console.log('in cmp set storage'); + if(await hasConsentForUseCase(useCase)) + { + switch(this.#storageHandler) { + case 'localStorage': return libsStorage.local.set(key, value, expires) + case 'sessionStorage': return libsStorage.session.set(key, value, expires) + } + } + else + { + console.error('cmp', `Cannot set local storage item ${key} due to missing consent for use-case ${useCase}`) + } } - else - { - console.error('cmp', `Cannot set local storage item ${storageItem} due to missing consent for use-case ${useCase}`) + + /** + * Remove an item from storage. + * + * @param key - the name of the item + */ + remove(key: string): void { + + switch(this.#storageHandler) { + case 'localStorage': { + return libsStorage.local.remove(key); + } + case 'sessionStorage': { + return libsStorage.session.remove(key); + } + } } -}; -export const cmpGetSessionStorageItem = async (useCase: UseCases, storageItem: string): Promise => -{ - console.log('in cmpGetSessionStorageItem'); + /** + * Removes all items from storage. + */ + clear(): void { + + switch(this.#storageHandler) { + case 'localStorage': { + return libsStorage.local.clear(); + } + case 'sessionStorage': { + return libsStorage.session.clear(); + } + } + } - if(await hasConsentForUseCase(useCase)) - { - return storage.session.get(storageItem) + /** + * Retrieve an item from storage in its raw state. + * + * @param key - the name of the item + */ + async getRaw(useCase: ConsentUseCases, key: string): Promise { + if(await hasConsentForUseCase(useCase)) + { + switch(this.#storageHandler) { + case 'localStorage': { + return libsStorage.local.getRaw(key) + } + case 'sessionStorage': { + return libsStorage.session.getRaw(key) + } + } + } + else + { + console.error('cmp', `Cannot get local storage item ${key} due to missing consent for use-case ${useCase}`) + return(null) + } } - else - { - console.error('cmp', `Cannot get session storage item ${storageItem} due to missing consent for use-case ${useCase}`) - return(null) + + /** + * Save a raw value to storage. + * + * @param key - the name of the item + * @param value - the data to save + */ + async setRaw(useCase: ConsentUseCases, key: string, value: string): Promise { + if(await hasConsentForUseCase(useCase)) + { + switch(this.#storageHandler) { + case 'localStorage': return libsStorage.local.setRaw(key, value) + case 'sessionStorage': return libsStorage.session.setRaw(key, value) + } + } + else + { + console.error('cmp', `Cannot set local storage item ${key} due to missing consent for use-case ${useCase}`) + } } -}; +} + +/** + * Manages using `localStorage` and `sessionStorage`. + * + * Has a few advantages over the native API, including + * - failing gracefully if storage is not available + * - you can save and retrieve any JSONable data + * + * All methods are available for both `localStorage` and `sessionStorage`. + */ +export const storage = new (class { + #local: StorageFactory | undefined; + #session: StorageFactory | undefined; -export const cmpSetSessionStorageItem = async (useCase: UseCases, storageItem: string, value:unknown, expires?: string | number | Date): Promise => -{ - console.log('in cmpSetSessionStorageItem'); + // creating the instance requires testing the native implementation + // which is blocking. therefore, only create new instances of the factory + // when it's accessed i.e. we know we're going to use it - if(await hasConsentForUseCase(useCase)) - { - return storage.session.set(storageItem, value, expires) - } - else - { - console.error('cmp', `Cannot set session storage item ${storageItem} due to missing consent for use-case ${useCase}`) - } -}; - -export const cmpGetCookie = async({ useCase, name, shouldMemoize, }: { - useCase: UseCases, - name: string; - shouldMemoize?: boolean | undefined; -}): Promise => -{ - console.log('in cmpGetCookie'); - - if(await hasConsentForUseCase(useCase)) - { - return getCookie({name: name, shouldMemoize: shouldMemoize}) + get local() { + return (this.#local ||= new StorageFactory('localStorage')); } - else - { - console.error('cmp', `Cannot get cookie ${name} due to missing consent for use-case ${useCase}`) - return(null) - } -}; - -export const cmpSetCookie = async ({ useCase, name, value, daysToLive, isCrossSubdomain, }: { - useCase: UseCases, - name: string; - value: string; - daysToLive?: number | undefined; - isCrossSubdomain?: boolean | undefined; -}): Promise => -{ - console.log('in cmpSetCookie'); - - if(await hasConsentForUseCase(useCase)) - { - setCookie({name:name, value:value, daysToLive:daysToLive, isCrossSubdomain:isCrossSubdomain}) - } - else - { - console.error('cmp', `Cannot set cookie ${name} due to missing consent for use-case ${useCase}`) + + get session() { + return (this.#session ||= new StorageFactory('sessionStorage')); } -}; +})(); + //await cmpGetLocalStorageItem("Targeted advertising", "dep") //await cmpGetLocalStorageItem("invalid", "dep") diff --git a/src/hasConsentForUseCase.test.ts b/src/hasConsentForUseCase.test.ts new file mode 100644 index 000000000..93f281a49 --- /dev/null +++ b/src/hasConsentForUseCase.test.ts @@ -0,0 +1,78 @@ +import { onConsentChange } from './onConsentChange'; +import type { Callback, ConsentState } from './types'; +import type { TCFv2ConsentState } from './types/tcfv2'; +import { hasConsentForUseCase } from './hasConsentForUseCase'; + +//TODO: add tests for all use-cases + +jest.mock('./onConsentChange'); +jest.mock('@guardian/libs', () => ({ + getCookie: jest.fn(), + setCookie: jest.fn(), + storage: { + local: { + get: jest.fn(), + set: jest.fn(), + }, + session: { + get: jest.fn(), + set: jest.fn(), + }, + } +})); + +const tcfv2ConsentState: TCFv2ConsentState = { + consents: { 1: true }, + eventStatus: 'tcloaded', + vendorConsents: { + ['5efefe25b8e05c06542b2a77']: true, + }, + addtlConsent: 'xyz', + gdprApplies: true, + tcString: 'YAAA', +}; + +const tcfv2ConsentStateNoConsent: TCFv2ConsentState = { + consents: { 1: false }, + eventStatus: 'tcloaded', + vendorConsents: {}, + addtlConsent: 'xyz', + gdprApplies: true, + tcString: 'YAAA', +}; + +const mockOnConsentChange = (consentState: ConsentState) => + (onConsentChange as jest.Mock).mockImplementation((cb: Callback) => cb(consentState)); + +describe('cmpStorage.hasConsentForUseCase returns the expected consent', () => { + test('Targeted advertising has consent when canTarget is true', async () => { + const consentState: ConsentState = { + tcfv2: tcfv2ConsentState, + canTarget: true, + framework: 'tcfv2', + }; + mockOnConsentChange(consentState); + const hasConsent = await hasConsentForUseCase('Targeted advertising'); + expect(hasConsent).toEqual(true); + }); + test('Targeted advertising has no consent when canTarget is false', async () => { + const consentState: ConsentState = { + tcfv2: tcfv2ConsentState, + canTarget: false, + framework: 'tcfv2', + }; + mockOnConsentChange(consentState); + const hasConsent = await hasConsentForUseCase('Targeted advertising'); + expect(hasConsent).toEqual(false); + }); + test('Essential has consent even when ConsentState has no consents', async () => { + const consentState: ConsentState = { + tcfv2: tcfv2ConsentStateNoConsent, + canTarget: false, + framework: 'tcfv2', + }; + mockOnConsentChange(consentState); + const hasConsent = await hasConsentForUseCase('Essential'); + expect(hasConsent).toEqual(true); + }); +}); diff --git a/src/hasConsentForUseCase.ts b/src/hasConsentForUseCase.ts new file mode 100644 index 000000000..30e1da655 --- /dev/null +++ b/src/hasConsentForUseCase.ts @@ -0,0 +1,38 @@ +import { onConsent } from './onConsent'; +import type { ConsentUseCases } from './types/consentUseCases'; + +export const hasConsentForUseCase = async (useCase: ConsentUseCases): Promise => +{ + const consentState = await onConsent(); + + /*console.log(`consentState.tcfv2?.consents['1']: ${consentState.tcfv2?.consents['1']}`); + console.log(`consentState.tcfv2?.consents['2']: ${consentState.tcfv2?.consents['2']}`); + console.log(`consentState.tcfv2?.consents['3']: ${consentState.tcfv2?.consents['3']}`); + console.log(`consentState.tcfv2?.consents['4']: ${consentState.tcfv2?.consents['4']}`); + console.log(`consentState.tcfv2?.consents['5']: ${consentState.tcfv2?.consents['5']}`); + console.log(`consentState.tcfv2?.consents['6']: ${consentState.tcfv2?.consents['6']}`); + console.log(`consentState.tcfv2?.consents['7']: ${consentState.tcfv2?.consents['7']}`); + console.log(`consentState.tcfv2?.consents['8']: ${consentState.tcfv2?.consents['8']}`); + console.log(`consentState.tcfv2?.consents['9']: ${consentState.tcfv2?.consents['9']}`); + console.log(`consentState.tcfv2?.consents['10']: ${consentState.tcfv2?.consents['10']}`); + console.log(`consentState.tcfv2?.consents['11']: ${consentState.tcfv2?.consents['11']}`); + console.log(`consentState.canTarget: ${consentState.canTarget}`); + */ + + switch(useCase) { + case "Targeted advertising": return(consentState.canTarget) + case "Targeted marketing":{ + if(( + consentState.tcfv2?.consents['1'] + && consentState.tcfv2?.consents['3'] + && consentState.tcfv2?.consents['7']) + || !consentState.ccpa?.doNotSell + || consentState.aus?.personalisedAdvertising) + return(true) + else return(false) + } + case "Essential": return(true) //could check for allow-list of essential cookies/storage here in the future + default: return(false) + } + +} diff --git a/src/index.ts b/src/index.ts index 4b2a3b8d4..86a8686de 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,7 +15,8 @@ import { import type { CMP, InitCMP, WillShowPrivacyMessage } from './types'; import { initVendorDataManager } from './vendorDataManager'; -export {cmpGetLocalStorageItem, cmpSetLocalStorageItem, cmpGetSessionStorageItem, cmpSetSessionStorageItem, cmpGetCookie, cmpSetCookie} from './cmpStorage'; +export { cmpGetCookie, cmpSetCookie, cmpSetSessionCookie} from './cmpCookies'; +export { storage } from './cmpStorage' // Store some bits in the global scope for reuse, in case there's more // than one instance of the CMP on the page in different scopes. diff --git a/src/types/consentUseCases.ts b/src/types/consentUseCases.ts new file mode 100644 index 000000000..cb000edc9 --- /dev/null +++ b/src/types/consentUseCases.ts @@ -0,0 +1,14 @@ +/** + * List of permitted use-cases for cookies/storage: + * + * - `Targeted advertising`: if the user can be targeted for personalised advertising according to the active consent framework + * - 'Targeted marketing': if the user can be targeted for personalised marketing, e.g. article count + * - `Essential`: if essential cookies/storage can be used according to the active consent framework + * + */ +export const ConsentUseCaseOptions = [ + "Targeted advertising", + "Targeted marketing", + "Essential" +] as const; +export type ConsentUseCases = typeof ConsentUseCaseOptions[number]; From 33e70a363fedfebab9f9b83162648978213bfecf Mon Sep 17 00:00:00 2001 From: Anna Voelker Date: Wed, 10 Jan 2024 14:53:09 +0000 Subject: [PATCH 12/17] option to pass ConsentState explicitly --- src/cmpCookies.ts | 45 ++++++++++++++++++-- src/cmpStorage.ts | 70 ++++++++++++++++++++++++++++---- src/hasConsentForUseCase.test.ts | 15 ++----- src/hasConsentForUseCase.ts | 6 +-- 4 files changed, 110 insertions(+), 26 deletions(-) diff --git a/src/cmpCookies.ts b/src/cmpCookies.ts index dfe691c65..a48c7c1fa 100644 --- a/src/cmpCookies.ts +++ b/src/cmpCookies.ts @@ -1,18 +1,31 @@ import { getCookie, setCookie, setSessionCookie } from '@guardian/libs'; import type { ConsentUseCases } from './types/consentUseCases'; import { hasConsentForUseCase } from './hasConsentForUseCase'; +import { ConsentState } from './types'; +import { onConsent } from './onConsent'; -//TODO: Write wrappers for the other cookie functions in @guardian/libs +//TODO?: Write wrappers for the other cookie functions in @guardian/libs export const cmpGetCookie = async({ useCase, name, shouldMemoize, }: { useCase: ConsentUseCases, name: string; shouldMemoize?: boolean | undefined; }): Promise => +{ + const consentState = await onConsent(); + return(cmpGetCookieWithConsentState({useCase, consentState, name, shouldMemoize})) +} + +export const cmpGetCookieWithConsentState = ({ useCase, consentState, name, shouldMemoize}: { + useCase: ConsentUseCases, + consentState: ConsentState, + name: string; + shouldMemoize?: boolean | undefined; +}): string | null => { console.log('in cmpGetCookie'); - if(await hasConsentForUseCase(useCase)) + if(hasConsentForUseCase(useCase, consentState)) { return getCookie({name: name, shouldMemoize: shouldMemoize}) } @@ -30,10 +43,23 @@ export const cmpSetCookie = async ({ useCase, name, value, daysToLive, isCrossSu daysToLive?: number | undefined; isCrossSubdomain?: boolean | undefined; }): Promise => +{ + const consentState = await onConsent(); + return(cmpSetCookieWithConsentState({useCase, consentState, name, value, daysToLive, isCrossSubdomain})) +} + +export const cmpSetCookieWithConsentState = ({ useCase, consentState, name, value, daysToLive, isCrossSubdomain, }: { + useCase: ConsentUseCases, + consentState: ConsentState, + name: string; + value: string; + daysToLive?: number | undefined; + isCrossSubdomain?: boolean | undefined; +}): void => { console.log('in cmpSetCookie'); - if(await hasConsentForUseCase(useCase)) + if(hasConsentForUseCase(useCase, consentState)) { setCookie({name:name, value:value, daysToLive:daysToLive, isCrossSubdomain:isCrossSubdomain}) } @@ -48,10 +74,21 @@ export const cmpSetSessionCookie = async ({ useCase, name, value }: { name: string; value: string; }): Promise => +{ + const consentState = await onConsent(); + return(cmpSetSessionCookieWithConsentState({useCase, consentState, name, value})) +}; + +export const cmpSetSessionCookieWithConsentState= async ({ useCase, consentState, name, value }: { + useCase: ConsentUseCases, + consentState: ConsentState, + name: string; + value: string; +}): Promise => { console.log('in cmpSetSessionCookie'); - if(await hasConsentForUseCase(useCase)) + if(hasConsentForUseCase(useCase, consentState)) { setSessionCookie({name:name, value:value}) } diff --git a/src/cmpStorage.ts b/src/cmpStorage.ts index 7a9563b9f..76c921a03 100644 --- a/src/cmpStorage.ts +++ b/src/cmpStorage.ts @@ -1,6 +1,8 @@ import { storage as libsStorage } from '@guardian/libs'; import type { ConsentUseCases } from './types/consentUseCases'; import { hasConsentForUseCase } from './hasConsentForUseCase'; +import { ConsentState } from './types'; +import { onConsent } from '.'; export const storageOptions = [ "localStorage", @@ -31,11 +33,24 @@ class StorageFactory { /** * Retrieve an item from storage. * + * @param useCase - the ConsentUseCase for which to get the data * @param key - the name of the item */ async get(useCase: ConsentUseCases, key: string): Promise { + const consentState = await onConsent(); + return(this.getWithConsentState(useCase, consentState, key)) + } + + /** + * Retrieve an item from storage. + * + * @param useCase - the ConsentUseCase for which to get the data + * @param consentState - the ConsentState to check if consent for the useCase has been given + * @param key - the name of the item + */ + getWithConsentState(useCase: ConsentUseCases, consentState: ConsentState, key: string): unknown { console.log('in cmp get storage'); - if(await hasConsentForUseCase(useCase)) + if(hasConsentForUseCase(useCase, consentState)) { switch(this.#storageHandler) { case 'localStorage': { @@ -55,16 +70,31 @@ class StorageFactory { } } - /** + /** * Save a value to storage. * + * @param useCase - the ConsentUseCase for which to get the data * @param key - the name of the item * @param value - the data to save * @param expires - optional date on which this data will expire */ async set(useCase:ConsentUseCases, key: string, value: unknown, expires?: string | number | Date): Promise { + const consentState = await onConsent(); + return(this.setWithConsentState(useCase, consentState, key, value, expires)) + } + + /** + * Save a value to storage. + * + * @param useCase - the ConsentUseCase for which to get the data + * @param consentState - the ConsentState to check if consent for the useCase has been given + * @param key - the name of the item + * @param value - the data to save + * @param expires - optional date on which this data will expire + */ + setWithConsentState(useCase:ConsentUseCases, consentState: ConsentState, key: string, value: unknown, expires?: string | number | Date): void { console.log('in cmp set storage'); - if(await hasConsentForUseCase(useCase)) + if(hasConsentForUseCase(useCase, consentState)) { switch(this.#storageHandler) { case 'localStorage': return libsStorage.local.set(key, value, expires) @@ -112,10 +142,23 @@ class StorageFactory { /** * Retrieve an item from storage in its raw state. * + * @param useCase - the ConsentUseCase for which to get the data * @param key - the name of the item */ async getRaw(useCase: ConsentUseCases, key: string): Promise { - if(await hasConsentForUseCase(useCase)) + const consentState = await onConsent(); + return(this.getRawWithConsentState(useCase,consentState,key)) + } + + /** + * Retrieve an item from storage in its raw state. + * + * @param useCase - the ConsentUseCase for which to get the data + * @param consentState - the ConsentState to check if consent for the useCase has been given + * @param key - the name of the item + */ + getRawWithConsentState(useCase: ConsentUseCases, consentState: ConsentState, key: string): string | null { + if(hasConsentForUseCase(useCase, consentState)) { switch(this.#storageHandler) { case 'localStorage': { @@ -136,11 +179,25 @@ class StorageFactory { /** * Save a raw value to storage. * + * @param useCase - the ConsentUseCase for which to get the data * @param key - the name of the item * @param value - the data to save */ async setRaw(useCase: ConsentUseCases, key: string, value: string): Promise { - if(await hasConsentForUseCase(useCase)) + const consentState = await onConsent(); + return(this.setRawWithConsentState(useCase, consentState, key, value)) + } + + /** + * Save a raw value to storage. + * + * @param useCase - the ConsentUseCase for which to get the data + * @param consentState - the ConsentState to check if consent for the useCase has been given + * @param key - the name of the item + * @param value - the data to save + */ + setRawWithConsentState(useCase: ConsentUseCases, consentState: ConsentState, key: string, value: string): void { + if(hasConsentForUseCase(useCase, consentState)) { switch(this.#storageHandler) { case 'localStorage': return libsStorage.local.setRaw(key, value) @@ -180,6 +237,3 @@ export const storage = new (class { } })(); - -//await cmpGetLocalStorageItem("Targeted advertising", "dep") -//await cmpGetLocalStorageItem("invalid", "dep") diff --git a/src/hasConsentForUseCase.test.ts b/src/hasConsentForUseCase.test.ts index 93f281a49..39fbcfdea 100644 --- a/src/hasConsentForUseCase.test.ts +++ b/src/hasConsentForUseCase.test.ts @@ -1,7 +1,6 @@ -import { onConsentChange } from './onConsentChange'; -import type { Callback, ConsentState } from './types'; import type { TCFv2ConsentState } from './types/tcfv2'; import { hasConsentForUseCase } from './hasConsentForUseCase'; +import { ConsentState } from './types'; //TODO: add tests for all use-cases @@ -41,9 +40,6 @@ const tcfv2ConsentStateNoConsent: TCFv2ConsentState = { tcString: 'YAAA', }; -const mockOnConsentChange = (consentState: ConsentState) => - (onConsentChange as jest.Mock).mockImplementation((cb: Callback) => cb(consentState)); - describe('cmpStorage.hasConsentForUseCase returns the expected consent', () => { test('Targeted advertising has consent when canTarget is true', async () => { const consentState: ConsentState = { @@ -51,8 +47,7 @@ describe('cmpStorage.hasConsentForUseCase returns the expected consent', () => { canTarget: true, framework: 'tcfv2', }; - mockOnConsentChange(consentState); - const hasConsent = await hasConsentForUseCase('Targeted advertising'); + const hasConsent = hasConsentForUseCase('Targeted advertising', consentState); expect(hasConsent).toEqual(true); }); test('Targeted advertising has no consent when canTarget is false', async () => { @@ -61,8 +56,7 @@ describe('cmpStorage.hasConsentForUseCase returns the expected consent', () => { canTarget: false, framework: 'tcfv2', }; - mockOnConsentChange(consentState); - const hasConsent = await hasConsentForUseCase('Targeted advertising'); + const hasConsent = hasConsentForUseCase('Targeted advertising', consentState); expect(hasConsent).toEqual(false); }); test('Essential has consent even when ConsentState has no consents', async () => { @@ -71,8 +65,7 @@ describe('cmpStorage.hasConsentForUseCase returns the expected consent', () => { canTarget: false, framework: 'tcfv2', }; - mockOnConsentChange(consentState); - const hasConsent = await hasConsentForUseCase('Essential'); + const hasConsent = hasConsentForUseCase('Essential', consentState); expect(hasConsent).toEqual(true); }); }); diff --git a/src/hasConsentForUseCase.ts b/src/hasConsentForUseCase.ts index 30e1da655..3f53ed80f 100644 --- a/src/hasConsentForUseCase.ts +++ b/src/hasConsentForUseCase.ts @@ -1,9 +1,9 @@ -import { onConsent } from './onConsent'; +//import { onConsent } from './onConsent'; +import { ConsentState } from './types'; import type { ConsentUseCases } from './types/consentUseCases'; -export const hasConsentForUseCase = async (useCase: ConsentUseCases): Promise => +export const hasConsentForUseCase = (useCase: ConsentUseCases, consentState: ConsentState): boolean => { - const consentState = await onConsent(); /*console.log(`consentState.tcfv2?.consents['1']: ${consentState.tcfv2?.consents['1']}`); console.log(`consentState.tcfv2?.consents['2']: ${consentState.tcfv2?.consents['2']}`); From c56a05ca78fd283cd766f9a75a37ae8b7bde13e3 Mon Sep 17 00:00:00 2001 From: Anna Voelker Date: Thu, 18 Jan 2024 12:54:06 +0000 Subject: [PATCH 13/17] testing cmp wrappers --- .changeset/dirty-radios-stare.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/dirty-radios-stare.md diff --git a/.changeset/dirty-radios-stare.md b/.changeset/dirty-radios-stare.md new file mode 100644 index 000000000..fe1ec08e3 --- /dev/null +++ b/.changeset/dirty-radios-stare.md @@ -0,0 +1,5 @@ +--- +'@guardian/consent-management-platform': patch +--- + +testing consent wrappers From 1fa1176607d7cd31737555d375fae81636c3904d Mon Sep 17 00:00:00 2001 From: Anna Voelker Date: Mon, 22 Jan 2024 14:30:33 +0000 Subject: [PATCH 14/17] add async hasConsentForUseCase --- src/hasConsentForUseCase.test.ts | 10 +++++----- src/hasConsentForUseCase.ts | 16 +++++++++++++++- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/hasConsentForUseCase.test.ts b/src/hasConsentForUseCase.test.ts index 39fbcfdea..b5327e283 100644 --- a/src/hasConsentForUseCase.test.ts +++ b/src/hasConsentForUseCase.test.ts @@ -1,6 +1,6 @@ -import type { TCFv2ConsentState } from './types/tcfv2'; -import { hasConsentForUseCase } from './hasConsentForUseCase'; +import { hasConsentForUseCaseWithConsentState } from './hasConsentForUseCase'; import { ConsentState } from './types'; +import type { TCFv2ConsentState } from './types/tcfv2'; //TODO: add tests for all use-cases @@ -47,7 +47,7 @@ describe('cmpStorage.hasConsentForUseCase returns the expected consent', () => { canTarget: true, framework: 'tcfv2', }; - const hasConsent = hasConsentForUseCase('Targeted advertising', consentState); + const hasConsent = hasConsentForUseCaseWithConsentState('Targeted advertising', consentState); expect(hasConsent).toEqual(true); }); test('Targeted advertising has no consent when canTarget is false', async () => { @@ -56,7 +56,7 @@ describe('cmpStorage.hasConsentForUseCase returns the expected consent', () => { canTarget: false, framework: 'tcfv2', }; - const hasConsent = hasConsentForUseCase('Targeted advertising', consentState); + const hasConsent = hasConsentForUseCaseWithConsentState('Targeted advertising', consentState); expect(hasConsent).toEqual(false); }); test('Essential has consent even when ConsentState has no consents', async () => { @@ -65,7 +65,7 @@ describe('cmpStorage.hasConsentForUseCase returns the expected consent', () => { canTarget: false, framework: 'tcfv2', }; - const hasConsent = hasConsentForUseCase('Essential', consentState); + const hasConsent = hasConsentForUseCaseWithConsentState('Essential', consentState); expect(hasConsent).toEqual(true); }); }); diff --git a/src/hasConsentForUseCase.ts b/src/hasConsentForUseCase.ts index 3f53ed80f..09e6a72f3 100644 --- a/src/hasConsentForUseCase.ts +++ b/src/hasConsentForUseCase.ts @@ -1,8 +1,21 @@ //import { onConsent } from './onConsent'; +import { cmp } from '.'; +import { onConsent } from './onConsent'; import { ConsentState } from './types'; import type { ConsentUseCases } from './types/consentUseCases'; -export const hasConsentForUseCase = (useCase: ConsentUseCases, consentState: ConsentState): boolean => +export const hasConsentForUseCase = async (useCase: ConsentUseCases): Promise => +{ + if(cmp.hasInitialised()) + { + const consentState = await onConsent(); + const hasconsent = hasConsentForUseCaseWithConsentState(useCase, consentState); + return(hasconsent); + } + else return(false); +} + +export const hasConsentForUseCaseWithConsentState = (useCase: ConsentUseCases, consentState: ConsentState): boolean => { /*console.log(`consentState.tcfv2?.consents['1']: ${consentState.tcfv2?.consents['1']}`); @@ -32,6 +45,7 @@ export const hasConsentForUseCase = (useCase: ConsentUseCases, consentState: Con else return(false) } case "Essential": return(true) //could check for allow-list of essential cookies/storage here in the future + case "No consent required": return(true) default: return(false) } From 35f3ce2585488eb54ed445643d1dc3963ea00750 Mon Sep 17 00:00:00 2001 From: Anna Voelker Date: Mon, 22 Jan 2024 14:36:45 +0000 Subject: [PATCH 15/17] hasConsentForUseCaseWithConsentState --- src/cmpCookies.ts | 12 ++++++------ src/cmpStorage.ts | 17 +++++++++-------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/cmpCookies.ts b/src/cmpCookies.ts index a48c7c1fa..31d558bc4 100644 --- a/src/cmpCookies.ts +++ b/src/cmpCookies.ts @@ -1,8 +1,8 @@ import { getCookie, setCookie, setSessionCookie } from '@guardian/libs'; -import type { ConsentUseCases } from './types/consentUseCases'; -import { hasConsentForUseCase } from './hasConsentForUseCase'; -import { ConsentState } from './types'; +import { hasConsentForUseCaseWithConsentState } from './hasConsentForUseCase'; import { onConsent } from './onConsent'; +import { ConsentState } from './types'; +import type { ConsentUseCases } from './types/consentUseCases'; //TODO?: Write wrappers for the other cookie functions in @guardian/libs @@ -25,7 +25,7 @@ export const cmpGetCookieWithConsentState = ({ useCase, consentState, name, shou { console.log('in cmpGetCookie'); - if(hasConsentForUseCase(useCase, consentState)) + if(hasConsentForUseCaseWithConsentState(useCase, consentState)) { return getCookie({name: name, shouldMemoize: shouldMemoize}) } @@ -59,7 +59,7 @@ export const cmpSetCookieWithConsentState = ({ useCase, consentState, name, valu { console.log('in cmpSetCookie'); - if(hasConsentForUseCase(useCase, consentState)) + if(hasConsentForUseCaseWithConsentState(useCase, consentState)) { setCookie({name:name, value:value, daysToLive:daysToLive, isCrossSubdomain:isCrossSubdomain}) } @@ -88,7 +88,7 @@ export const cmpSetSessionCookieWithConsentState= async ({ useCase, consentState { console.log('in cmpSetSessionCookie'); - if(hasConsentForUseCase(useCase, consentState)) + if(hasConsentForUseCaseWithConsentState(useCase, consentState)) { setSessionCookie({name:name, value:value}) } diff --git a/src/cmpStorage.ts b/src/cmpStorage.ts index 76c921a03..aede5f7f5 100644 --- a/src/cmpStorage.ts +++ b/src/cmpStorage.ts @@ -1,8 +1,9 @@ import { storage as libsStorage } from '@guardian/libs'; -import type { ConsentUseCases } from './types/consentUseCases'; -import { hasConsentForUseCase } from './hasConsentForUseCase'; +import { hasConsentForUseCaseWithConsentState } from './hasConsentForUseCase'; +import { onConsent } from './onConsent'; import { ConsentState } from './types'; -import { onConsent } from '.'; +import type { ConsentUseCases } from './types/consentUseCases'; + export const storageOptions = [ "localStorage", @@ -37,7 +38,7 @@ class StorageFactory { * @param key - the name of the item */ async get(useCase: ConsentUseCases, key: string): Promise { - const consentState = await onConsent(); + const consentState = await onConsent(); return(this.getWithConsentState(useCase, consentState, key)) } @@ -50,7 +51,7 @@ class StorageFactory { */ getWithConsentState(useCase: ConsentUseCases, consentState: ConsentState, key: string): unknown { console.log('in cmp get storage'); - if(hasConsentForUseCase(useCase, consentState)) + if(hasConsentForUseCaseWithConsentState(useCase, consentState)) { switch(this.#storageHandler) { case 'localStorage': { @@ -94,7 +95,7 @@ class StorageFactory { */ setWithConsentState(useCase:ConsentUseCases, consentState: ConsentState, key: string, value: unknown, expires?: string | number | Date): void { console.log('in cmp set storage'); - if(hasConsentForUseCase(useCase, consentState)) + if(hasConsentForUseCaseWithConsentState(useCase, consentState)) { switch(this.#storageHandler) { case 'localStorage': return libsStorage.local.set(key, value, expires) @@ -158,7 +159,7 @@ class StorageFactory { * @param key - the name of the item */ getRawWithConsentState(useCase: ConsentUseCases, consentState: ConsentState, key: string): string | null { - if(hasConsentForUseCase(useCase, consentState)) + if(hasConsentForUseCaseWithConsentState(useCase, consentState)) { switch(this.#storageHandler) { case 'localStorage': { @@ -197,7 +198,7 @@ class StorageFactory { * @param value - the data to save */ setRawWithConsentState(useCase: ConsentUseCases, consentState: ConsentState, key: string, value: string): void { - if(hasConsentForUseCase(useCase, consentState)) + if(hasConsentForUseCaseWithConsentState(useCase, consentState)) { switch(this.#storageHandler) { case 'localStorage': return libsStorage.local.setRaw(key, value) From a3f374a65e19f1d2f9c4a4a9bbd6dcc7fde92505 Mon Sep 17 00:00:00 2001 From: Anna Voelker Date: Mon, 22 Jan 2024 14:52:45 +0000 Subject: [PATCH 16/17] remove "no consent required for now" --- src/hasConsentForUseCase.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hasConsentForUseCase.ts b/src/hasConsentForUseCase.ts index 09e6a72f3..938b8d7b3 100644 --- a/src/hasConsentForUseCase.ts +++ b/src/hasConsentForUseCase.ts @@ -45,7 +45,7 @@ export const hasConsentForUseCaseWithConsentState = (useCase: ConsentUseCases, c else return(false) } case "Essential": return(true) //could check for allow-list of essential cookies/storage here in the future - case "No consent required": return(true) + //case "No consent required": return(true) //Would we want a use-case like this, perhaps for internal tools? default: return(false) } From 0f8b4bfb2886639924259ee3074711724ff88460 Mon Sep 17 00:00:00 2001 From: Anna Voelker Date: Mon, 22 Jan 2024 21:12:43 +0000 Subject: [PATCH 17/17] add "No consent required" --- src/hasConsentForUseCase.ts | 2 +- src/types/consentUseCases.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/hasConsentForUseCase.ts b/src/hasConsentForUseCase.ts index 938b8d7b3..91e383b10 100644 --- a/src/hasConsentForUseCase.ts +++ b/src/hasConsentForUseCase.ts @@ -45,7 +45,7 @@ export const hasConsentForUseCaseWithConsentState = (useCase: ConsentUseCases, c else return(false) } case "Essential": return(true) //could check for allow-list of essential cookies/storage here in the future - //case "No consent required": return(true) //Would we want a use-case like this, perhaps for internal tools? + case "No consent required": return(true) //Would we want a use-case like this, perhaps for internal tools? default: return(false) } diff --git a/src/types/consentUseCases.ts b/src/types/consentUseCases.ts index cb000edc9..4ee7b039f 100644 --- a/src/types/consentUseCases.ts +++ b/src/types/consentUseCases.ts @@ -4,11 +4,13 @@ * - `Targeted advertising`: if the user can be targeted for personalised advertising according to the active consent framework * - 'Targeted marketing': if the user can be targeted for personalised marketing, e.g. article count * - `Essential`: if essential cookies/storage can be used according to the active consent framework + * - `No Consent Required`: does not do any consent checks. This is almost certainly not the right choice. Please speak to T&C before using it. * */ export const ConsentUseCaseOptions = [ "Targeted advertising", "Targeted marketing", - "Essential" + "Essential", + "No consent required" ] as const; export type ConsentUseCases = typeof ConsentUseCaseOptions[number];