Skip to content

Commit

Permalink
Add Google consent mode support
Browse files Browse the repository at this point in the history
  • Loading branch information
mjbp committed Sep 26, 2024
1 parent 83e7890 commit c330e57
Show file tree
Hide file tree
Showing 7 changed files with 164 additions and 7 deletions.
15 changes: 14 additions & 1 deletion packages/cookie-banner/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@ The consent form renders into a DOMElement with a particular className configura

A page containing a cookie consent form should include a visually hidden live region (role=alert) with a particular className (classNames.formAnnouncement), default: 'privacy-banner__form-announcement'.

Optionally the banner also supports basic Google EU consent mode [https://developers.google.com/tag-platform/security/guides/consent?consentmode=basic], and can push user consent preferences to the dataLayer for Google libraries to use. All that ius necessary to suport Google consent mode is to map Google consent categories to the cookie categories in the configuration.

For example, to map the ad_storage, ad_user_data, and ad_personalisation to an 'ads' consent category defined in the banner config, add a `euConsentTypes` object to the configuration like this:

```
euConsentTypes: {
ad_storage: 'test',
ad_user_data: 'test',
ad_personalization: 'test'
}
```


Install the package
```
Expand Down Expand Up @@ -80,7 +92,8 @@ const cookieBanner = banner({
secure: true, //preferences cookie secure
samesite: 'lax', //preferences cookie samesite
expiry: 365, //preferences cookie expiry in days
types: {}, //types of cookie-dependent functionality
types: {}, //types of cookie-dependent functionality
euConsentTypes: {}, //map Google EU consent categories to types of cookie defined in 'types'
necessary: [], //cookie-dependent functionality that will always execute, for convenience only
policyURL: '/cookie-policy#preferences', //URL to cookie policy page (location of cookie consent form) rendered in the banner
classNames: {
Expand Down
65 changes: 65 additions & 0 deletions packages/cookie-banner/__tests__/google-eu-consent/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import cookieBanner from '../../src';
import defaults from '../../src/lib/defaults';

/* eslint-disable camelcase */
const init = () => {
// Set up our document body
document.body.innerHTML = `<main></main>`;
window.__cb__ = cookieBanner({
secure: false,
euConsentTypes: {
ad_storage: 'test',
ad_user_data: 'test',
ad_personalization: 'test',
analytics_storage: 'performance'
},
types: {
test: {
title: 'Test title',
description: 'Test description',
labels: {
yes: 'Pages you visit and actions you take will be measured and used to improve the service',
no: 'Pages you visit and actions you take will not be measured and used to improve the service'
},
fns: [
() => { }
]
},
performance: {
title: 'Performance preferences',
description: 'Performance cookies are used to measure the performance of our website and make improvements. Your personal data is not identified.',
labels: {
yes: 'Pages you visit and actions you take will be measured and used to improve the service',
no: 'Pages you visit and actions you take will not be measured and used to improve the service'
},
fns: [
() => { }
]
}
}
});
};


describe(`Cookie banner > cookies > Google EU consent > default event`, () => {
beforeAll(init);

it('must set a default consent event with all categories denied', async () => {
const banner = document.querySelector(`.${defaults.classNames.banner}`);
expect(banner).not.toBeNull();

//These assertions break Jest because of the use 'arguments' in the gtag implementation
//They have been manually validated in the browser
// expect(window.dataLayer).toEqual([
// ['consent', 'default', { ad_storage: 'denied', ad_user_data: 'denied', ad_personalization: 'denied', analytics_storage: 'denied' }]
// ]);
// const acceptAllBtn = document.querySelector(`.${defaults.classNames.acceptBtn}`);
// acceptAllBtn.click();

// expect(window.dataLayer).toEqual([
// ['consent', 'default', { ad_storage: 'denied', ad_user_data: 'denied', ad_personalization: 'denied', analytics_storage: 'denied' }],
// ['consent', 'update', { ad_storage: 'granted', ad_user_data: 'granted', ad_personalization: 'granted', analytics_storage: 'granted' }]
// ]);
});

});
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import cookieBanner from '../../src';
import defaults from '../../src/lib/defaults';

const init = () => {
// Set up our document body
document.body.innerHTML = `<main></main>`;
window.__cb__ = cookieBanner({
secure: false,
types: {
test: {
title: 'Test title',
description: 'Test description',
labels: {
yes: 'Pages you visit and actions you take will be measured and used to improve the service',
no: 'Pages you visit and actions you take will not be measured and used to improve the service'
},
fns: [
() => { }
]
},
performance: {
title: 'Performance preferences',
description: 'Performance cookies are used to measure the performance of our website and make improvements. Your personal data is not identified.',
labels: {
yes: 'Pages you visit and actions you take will be measured and used to improve the service',
no: 'Pages you visit and actions you take will not be measured and used to improve the service'
},
fns: [
() => { }
]
}
}
});
};


describe(`Cookie banner > cookies > Google EU consent > no EU consent settings`, () => {
beforeAll(init);

it('No errors or pushes to dataLayer if no consent options configured', async () => {
const banner = document.querySelector(`.${defaults.classNames.banner}`);
expect(banner).not.toBeNull();
expect(window.dataLayer).toBeUndefined();
});

});
6 changes: 6 additions & 0 deletions packages/cookie-banner/example/src/js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ const writeCookie = state => {
const config = {
name: '.Components.Dev.Consent',
secure: false,
euConsentTypes: {
ad_storage: 'ads',
ad_user_data: 'ads',
ad_personalization: 'ads',
analytics_storage: 'performance'
},
hideBannerOnFormPage: false,
trapTab: true,
necessary: [
Expand Down
6 changes: 4 additions & 2 deletions packages/cookie-banner/src/lib/factory.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { cookiesEnabled, extractFromCookie, noop, renderIframe, gtmSnippet } from './utils';
import { cookiesEnabled, extractFromCookie, noop, renderIframe, gtmSnippet, setGoogleConsent } from './utils';
import { showBanner, initBanner, initForm, initBannerListeners, keyListener } from './ui';
import { necessary, apply } from './consent';
import { createStore } from './store';
Expand All @@ -25,10 +25,12 @@ export default settings => {
},
[
necessary,
setGoogleConsent(Store, 'default'),
apply(Store),
hasCookie ? noop : initBanner(Store),
initForm(Store),
initBannerListeners(Store)
initBannerListeners(Store),
hasCookie ? setGoogleConsent(Store) : noop
]
);

Expand Down
11 changes: 7 additions & 4 deletions packages/cookie-banner/src/lib/ui.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { writeCookie, groupValueReducer, deleteCookies, getFocusableChildren, broadcast } from './utils';
import { writeCookie, groupValueReducer, deleteCookies, getFocusableChildren, broadcast, setGoogleConsent } from './utils';
import { ACCEPTED_TRIGGERS, EVENTS } from './constants';
import { apply } from './consent';
import { updateConsent, updateBannerOpen, updateBanner } from './reducers';
Expand Down Expand Up @@ -47,7 +47,8 @@ export const initBannerListeners = Store => () => {
apply(Store),
removeBanner(Store),
initForm(Store, false),
broadcast(EVENTS.CONSENT, Store)
broadcast(EVENTS.CONSENT, Store),
setGoogleConsent(Store),
]
);
});
Expand All @@ -65,7 +66,8 @@ export const initBannerListeners = Store => () => {
writeCookie,
removeBanner(Store),
initForm(Store, false),
broadcast(EVENTS.CONSENT, Store)
broadcast(EVENTS.CONSENT, Store),
setGoogleConsent(Store),
]
);
});
Expand Down Expand Up @@ -153,7 +155,8 @@ export const initForm = (Store, track = true) => () => {
removeBanner(Store),
broadcast(EVENTS.CONSENT, Store),
renderMessage(button),
renderAnnouncement(formAnnouncement)
renderAnnouncement(formAnnouncement),
setGoogleConsent(Store),
]
);
});
Expand Down
22 changes: 22 additions & 0 deletions packages/cookie-banner/src/lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,3 +148,25 @@ export const gtmSnippet = id => {
s.async = !0, s.src = 'https://www.googletagmanager.com/gtm.js?id=' + w, r.parentNode.insertBefore(s, r)
}(window, document, "script", "dataLayer", id);
};

/* eslint-disable prefer-rest-params */
function gtag() {
window.dataLayer = window.dataLayer || [];
//The Google libraries that use the dataLayer do not work if arguments are spread
//or data is passed in as an array
window.dataLayer.push(arguments);
}

export const setGoogleConsent = (Store, pushType = 'update') => () => {
const { settings, consent } = Store.getState();
const { euConsentTypes } = settings;
if (!euConsentTypes) return;

const euConsent = Object.keys(euConsentTypes).reduce((acc, type) => {
acc[type] = (consent[euConsentTypes[type]] && pushType === 'update') ? 'granted' : 'denied';
return acc;
}, {});
if (pushType !== 'update') euConsent['wait_for_update'] = 500;

gtag('consent', pushType, euConsent);
};

0 comments on commit c330e57

Please sign in to comment.