Skip to content

Commit c72ed2b

Browse files
Bruno Oliveiraboliveira
Bruno Oliveira
authored andcommitted
feat: add support for sharing gcm
This adds support for sharing google consent mode with other apps such as Luxury Checkout through a cookie set for the same domain.
1 parent e95d4d4 commit c72ed2b

File tree

7 files changed

+393
-120
lines changed

7 files changed

+393
-120
lines changed

packages/core/src/analytics/utils/getters.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ export const getLocation = data => {
6666
};
6767

6868
export const getCookie = name => {
69+
if (typeof document === 'undefined') {
70+
return;
71+
}
72+
6973
const value = `; ${document.cookie}`;
7074
const parts = value.split(`; ${name}=`);
7175

packages/react/src/analytics/analytics.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import { name as PCKG_NAME, version as PCKG_VERSION } from '../../package.json';
1+
import {
2+
PACKAGE_NAME as PCKG_NAME,
3+
PACKAGE_NAME_VERSION as PCKG_VERSION,
4+
} from './constants';
25
import Analytics, {
36
trackTypes as analyticsTrackTypes,
47
platformTypes,
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// We use a require here to avoid typescript complaining of `package.json` is not
2+
// under rootDir that we would get if we used an import. Typescript apparently ignores
3+
// requires.
4+
// eslint-disable-next-line @typescript-eslint/no-var-requires
5+
const { name, version } = require('../../package.json');
6+
7+
export const PACKAGE_VERSION = version;
8+
export const PACKAGE_NAME = name;
9+
export const PACKAGE_NAME_VERSION = `${PACKAGE_NAME}@${PACKAGE_VERSION}`;

packages/react/src/analytics/integrations/shared/GoogleConsentMode/GoogleConsentMode.js

Lines changed: 151 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,144 +1,213 @@
1-
/**
2-
* GoogleConsentMode handles with Google Consent Mode v2.
3-
*/
4-
import isEqual from 'lodash/isEqual';
1+
import { GCM_SHARED_COOKIE_NAME, setCookie } from './cookieUtils';
2+
import { utils } from '@farfetch/blackout-core/analytics';
53
import omit from 'lodash/omit';
64

75
export const googleConsentTypes = {
86
GRANTED: 'granted',
97
DENIED: 'denied',
108
};
9+
10+
/**
11+
* GoogleConsentMode handles with Google Consent Mode v2.
12+
*/
1113
export class GoogleConsentMode {
14+
dataLayer; // Stores different data layer names
15+
configWithConsentOnly; // exclude not consent properties from config
16+
consentDataLayerCommands = [];
17+
waitForUpdate;
18+
regions;
19+
hasConfig;
20+
1221
/**
1322
* Creates a new GoogleConsentMode instance.
1423
*
1524
* @param {string} dataLayer - DataLayer name.
16-
* @param {string} initConsent - The init consent data.
25+
* @param {object} initConsent - The init consent data.
1726
* @param {object} config - The configuration properties of Google Consent Mode.
1827
*/
1928
constructor(dataLayer, initConsent, config) {
2029
this.dataLayer = dataLayer;
21-
this.config = config;
30+
31+
this.waitForUpdate = config?.waitForUpdate;
32+
this.regions = config?.regions;
2233

2334
// select only the Google Consent Elements
24-
this.configExcludingModeRegionsAndWaitForUpdate = omit(this.config || {}, [
35+
this.configWithConsentOnly = omit(config || {}, [
2536
'waitForUpdate',
2637
'regions',
2738
'mode',
2839
]);
2940

30-
this.loadDefaults(initConsent);
41+
this.hasConfig = Object.keys(this.configWithConsentOnly).length > 0;
42+
43+
this.initialize(initConsent);
3144
}
3245

3346
/**
34-
* Initialize Google Consent Mode instance.
35-
*
36-
* @param {string} initConsent - The init consent data.
47+
* Tries to load shared consent from cookies if available
48+
* and writes it to the dataLayer.
49+
* This method is only supposed to be called if no google
50+
* consent config was passed.
3751
*/
38-
loadDefaults(initConsent) {
39-
if (this.config) {
40-
const initialValue = {};
52+
loadSharedConsentFromCookies() {
53+
const consentModeCookieValue = utils.getCookie(GCM_SHARED_COOKIE_NAME);
4154

42-
if (this.config.waitForUpdate) {
43-
initialValue['wait_for_update'] = this.config.waitForUpdate;
44-
}
55+
if (consentModeCookieValue) {
56+
try {
57+
const values = JSON.parse(consentModeCookieValue);
58+
59+
if (Array.isArray(values)) {
60+
values.forEach(value => {
61+
const [consentCommand, command, consent] = value;
4562

46-
// Obtain default google consent registry
47-
const consentRegistry = Object.keys(
48-
this.configExcludingModeRegionsAndWaitForUpdate,
49-
).reduce((result, consentKey) => {
50-
return {
51-
...result,
52-
[consentKey]:
53-
this.configExcludingModeRegionsAndWaitForUpdate[consentKey]
54-
?.default || googleConsentTypes.DENIED,
55-
};
56-
}, initialValue);
57-
58-
// Write default consent to data layer
59-
this.write('consent', 'default', consentRegistry);
60-
61-
// write regions to data layer if they exists
62-
const regions = this.config.regions;
63-
if (regions) {
64-
regions.forEach(region => {
65-
this.write('consent', 'default', region);
66-
});
63+
this.write(consentCommand, command, consent);
64+
});
65+
}
66+
} catch {
67+
// Do nothing...
6768
}
69+
}
70+
}
71+
72+
/**
73+
* Loads default values from the configuration and
74+
* writes them in a cookie for sharing.
75+
*
76+
* @param {object} initConsent - The consent data available, which can be null if the user has not yet given consent.
77+
*/
78+
loadDefaultsFromConfig(initConsent) {
79+
const initialValue = {};
6880

69-
// after write default consents, then write first update with initial consent data
70-
this.updateConsent(initConsent);
81+
if (this.waitForUpdate) {
82+
initialValue['wait_for_update'] = this.waitForUpdate;
7183
}
84+
85+
// Obtain default google consent registry
86+
const consentRegistry = Object.keys(this.configWithConsentOnly).reduce(
87+
(result, consentKey) => ({
88+
...result,
89+
[consentKey]:
90+
this.configWithConsentOnly[consentKey]?.default ||
91+
googleConsentTypes.DENIED,
92+
}),
93+
initialValue,
94+
);
95+
96+
// Write default consent to data layer
97+
this.write('consent', 'default', consentRegistry);
98+
99+
// write regions to data layer if they exist
100+
const regions = this.regions;
101+
102+
if (regions) {
103+
regions.forEach(region => {
104+
this.write('consent', 'default', region);
105+
});
106+
}
107+
108+
this.updateConsent(initConsent);
109+
110+
this.saveConsent();
72111
}
73112

74113
/**
75-
* Update consent.
114+
* Try to set consent types with dataLayer. If a valid
115+
* config was passed, default values for the consent
116+
* types are used. Else, try to load the commands
117+
* set from the cookie if it is available.
76118
*
77-
* @param {object} consentData - The consent data to be set.
119+
* @param initConsent - The consent data available, which can be null if the user has not yet given consent.
78120
*/
79-
updateConsent(consentData) {
80-
if (this.config) {
81-
// Dealing with null or undefined consent values
82-
const safeConsent = consentData || {};
121+
initialize(initConsent) {
122+
if (this.hasConfig) {
123+
this.loadDefaultsFromConfig(initConsent);
124+
} else {
125+
this.loadSharedConsentFromCookies();
126+
}
127+
}
83128

129+
/**
130+
* Writes consent updates to the dataLayer
131+
* by applying the configuration (if any) to
132+
* the passed consent data.
133+
*
134+
* @param {object} consentData - Consent data obtained from the user or null if not available.
135+
*/
136+
updateConsent(consentData) {
137+
if (this.hasConfig && consentData) {
84138
// Fill consent value into consent element, using analytics consent categories
85-
const consentRegistry = Object.keys(
86-
this.configExcludingModeRegionsAndWaitForUpdate,
87-
).reduce((result, consentKey) => {
88-
let consentValue = googleConsentTypes.DENIED;
89-
const consent =
90-
this.configExcludingModeRegionsAndWaitForUpdate[consentKey];
91-
92-
if (consent) {
93-
// has consent config key
94-
95-
if (consent.getConsentValue) {
96-
// give priority to custom function
97-
consentValue = consent.getConsentValue(safeConsent);
98-
} else if (
99-
consent?.categories !== undefined &&
100-
consent.categories.every(consent => safeConsent[consent])
101-
) {
102-
// The second option to assign value is by categories list
103-
consentValue = googleConsentTypes.GRANTED;
139+
const consentRegistry = Object.keys(this.configWithConsentOnly).reduce(
140+
(result, consentKey) => {
141+
let consentValue = googleConsentTypes.DENIED;
142+
const consent = this.configWithConsentOnly[consentKey];
143+
144+
if (consent) {
145+
// has consent config key
146+
if (consent.getConsentValue) {
147+
// give priority to custom function
148+
consentValue = consent.getConsentValue(consentData);
149+
} else if (
150+
consent?.categories !== undefined &&
151+
consent.categories.every(consent => consentData[consent])
152+
) {
153+
// The second option to assign value is by categories list
154+
consentValue = googleConsentTypes.GRANTED;
155+
}
104156
}
105-
}
106157

107-
return {
108-
...result,
109-
[consentKey]: consentValue,
110-
};
111-
}, {});
158+
return {
159+
...result,
160+
[consentKey]: consentValue,
161+
};
162+
},
163+
{},
164+
);
112165

113166
// Write consent to data layer
114167
this.write('consent', 'update', consentRegistry);
168+
169+
this.saveConsent();
115170
}
116171
}
117172

118173
/**
119-
* Write consent on data layer.
174+
* Saves calculated google consent mode to a cookie
175+
* for sharing consent between apps in same
176+
* domain.
177+
*/
178+
saveConsent() {
179+
if (this.consentDataLayerCommands.length > 0) {
180+
setCookie(
181+
GCM_SHARED_COOKIE_NAME,
182+
JSON.stringify(this.consentDataLayerCommands),
183+
);
184+
}
185+
}
186+
187+
/**
188+
* Handles consent by updating the data layer with consent information.
120189
*
121-
* @param {string} consentCommand - The consent command "consent".
190+
* @param {string} consent - The consent command "consent".
191+
* @param consentCommand
122192
* @param {string} command - The command "default" or "update".
123193
* @param {object} consentParams - The consent arguments.
124194
*/
125-
// eslint-disable-next-line no-unused-vars
126195
write(consentCommand, command, consentParams) {
127196
// Without using the arguments reference, google debug mode would not seem to register the consent
128197
// that was written to the datalayer, so the parameters added to the function signature are only to
129198
// avoid mistakes when calling the function.
130199

131-
if (
132-
this.config &&
133-
typeof window !== 'undefined' &&
134-
consentParams &&
135-
!isEqual(this.lastConsent, consentParams)
136-
) {
137-
// @ts-ignore
200+
if (typeof window !== 'undefined' && consentParams) {
138201
window[this.dataLayer] = window[this.dataLayer] || [];
139202

203+
// eslint-disable-next-line prefer-rest-params
140204
window[this.dataLayer].push(arguments);
141-
this.lastConsent = consentParams;
205+
206+
this.consentDataLayerCommands.push([
207+
consentCommand,
208+
command,
209+
consentParams,
210+
]);
142211
}
143212
}
144213
}

0 commit comments

Comments
 (0)