Skip to content

Commit 2cd74da

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 8d70cbb commit 2cd74da

File tree

7 files changed

+366
-123
lines changed

7 files changed

+366
-123
lines changed

packages/analytics/src/utils/getters.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,10 @@ export const getLocation = (
134134
* @returns The cookie value.
135135
*/
136136
export const getCookie = (name: string): string | void => {
137+
if (typeof document === 'undefined') {
138+
return;
139+
}
140+
137141
const value = `; ${document.cookie}`;
138142
const parts = value.split(`; ${name}=`);
139143

packages/react/src/analytics/analytics.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { get } from 'lodash-es';
2+
import { PACKAGE_NAME, PACKAGE_NAME_VERSION } from './constants.js';
23
import Analytics, {
34
type EventContextData,
45
type EventData,
@@ -11,12 +12,6 @@ import Analytics, {
1112
import webContext, { type WebContext } from './context.js';
1213
import WebContextStateManager from './WebContextStateManager.js';
1314

14-
const {
15-
name: PACKAGE_NAME,
16-
version: PACKAGE_VERSION,
17-
// eslint-disable-next-line @typescript-eslint/no-var-requires
18-
} = require('../../package.json');
19-
2015
/**
2116
* Analytics facade for web applications. Refer to \@farfetch/blackout-analytics
2217
* documentation to know the inherited methods from Analytics.
@@ -85,7 +80,7 @@ class AnalyticsWeb extends Analytics {
8580
if (context) {
8681
context.library = {
8782
name: PACKAGE_NAME,
88-
version: `${context.library.name}@${context.library.version};${PACKAGE_NAME}@${PACKAGE_VERSION};`,
83+
version: `${context.library.name}@${context.library.version};${PACKAGE_NAME_VERSION};`,
8984
};
9085

9186
const webContextStateSnapshot = this.webContextStateManager.getSnapshot();
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 as string;
8+
export const PACKAGE_NAME = name as string;
9+
export const PACKAGE_NAME_VERSION = `${PACKAGE_NAME}@${PACKAGE_VERSION}`;

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

Lines changed: 144 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,122 +1,189 @@
1-
import { type ConsentData } from '@farfetch/blackout-analytics';
1+
import { type ConsentData, utils } from '@farfetch/blackout-analytics';
2+
import { GCM_SHARED_COOKIE_NAME, setCookie } from './cookieUtils.js';
23
import {
34
type GoogleConsentCategoryConfig,
45
type GoogleConsentModeConfig,
56
GoogleConsentType,
67
} from './types.js';
7-
import { isEqual, omit } from 'lodash-es';
8+
import { omit } from 'lodash-es';
89

910
/**
1011
* GoogleConsentMode handles with Google Consent Mode v2.
1112
*/
1213
export class GoogleConsentMode {
1314
private dataLayer!: string; // Stores different data layer names
14-
private config?: GoogleConsentModeConfig; // Stores default or customized consent category mappings
15-
private configExcludingModeRegionsAndWaitForUpdate!: Record<
16-
string,
17-
GoogleConsentCategoryConfig
18-
>; // exclude not consent properties from config
19-
private lastConsent?: Record<
20-
string,
21-
Array<string> | string | number | undefined
22-
>;
15+
private configWithConsentOnly!: Record<string, GoogleConsentCategoryConfig>; // exclude not consent properties from config
16+
private consentDataLayerCommands: Array<
17+
[
18+
'consent',
19+
'default' | 'update',
20+
Record<string, Array<string> | string | number | undefined> | undefined,
21+
]
22+
> = [];
23+
private waitForUpdate?: number;
24+
private regions?: GoogleConsentModeConfig['regions'];
25+
private hasConfig: boolean;
2326

2427
constructor(
2528
dataLayer: string,
2629
initConsent: ConsentData | null,
2730
config?: GoogleConsentModeConfig,
2831
) {
2932
this.dataLayer = dataLayer;
30-
this.config = config;
33+
34+
this.waitForUpdate = config?.waitForUpdate;
35+
this.regions = config?.regions;
3136

3237
// select only the Google Consent Elements
33-
this.configExcludingModeRegionsAndWaitForUpdate = omit(this.config || {}, [
38+
this.configWithConsentOnly = omit(config || {}, [
3439
'waitForUpdate',
3540
'regions',
3641
'mode',
3742
]);
3843

39-
this.loadDefaults(initConsent);
44+
this.hasConfig = Object.keys(this.configWithConsentOnly).length > 0;
45+
46+
this.initialize(initConsent);
4047
}
48+
4149
/**
42-
* Write google consent default values to dataLayer.
43-
*
44-
* @param initConsent - The init consent data to be set.
50+
* Tries to load shared consent from cookies if available
51+
* and writes it to the dataLayer.
52+
* This method is only supposed to be called if no google
53+
* consent config was passed.
4554
*/
46-
private loadDefaults(initConsent: ConsentData | null) {
47-
if (this.config) {
48-
const initialValue: Record<string, string | number> = {};
55+
private loadSharedConsentFromCookies() {
56+
const consentModeCookieValue = utils.getCookie(GCM_SHARED_COOKIE_NAME);
4957

50-
if (this.config.waitForUpdate) {
51-
initialValue['wait_for_update'] = this.config.waitForUpdate;
58+
if (consentModeCookieValue) {
59+
try {
60+
const values = JSON.parse(consentModeCookieValue);
61+
62+
if (Array.isArray(values)) {
63+
values.forEach(value => {
64+
const [consentCommand, command, consent] = value;
65+
66+
this.write(consentCommand, command, consent);
67+
});
68+
}
69+
} catch {
70+
// Do nothing...
5271
}
72+
}
73+
}
5374

54-
// Obtain default google consent registry
55-
const consentRegistry = Object.keys(
56-
this.configExcludingModeRegionsAndWaitForUpdate,
57-
).reduce(
58-
(result, consentKey) => ({
59-
...result,
60-
[consentKey]:
61-
this.configExcludingModeRegionsAndWaitForUpdate[consentKey]
62-
?.default || GoogleConsentType.Denied,
63-
}),
64-
initialValue,
65-
);
75+
/**
76+
* Loads default values from the configuration and
77+
* writes them in a cookie for sharing.
78+
*
79+
* @param initConsent - The consent data available, which can be null if the user has not yet given consent.
80+
*/
81+
private loadDefaultsFromConfig(initConsent: ConsentData | null) {
82+
const initialValue: Record<string, string | number> = {};
6683

67-
// Write default consent to data layer
68-
this.write('consent', 'default', consentRegistry);
84+
if (this.waitForUpdate) {
85+
initialValue['wait_for_update'] = this.waitForUpdate;
86+
}
6987

70-
// write regions to data layer if they exists
71-
this.config.regions?.forEach(region => {
88+
// Obtain default google consent registry
89+
const consentRegistry = Object.keys(this.configWithConsentOnly).reduce(
90+
(result, consentKey) => ({
91+
...result,
92+
[consentKey]:
93+
this.configWithConsentOnly[consentKey]?.default ||
94+
GoogleConsentType.Denied,
95+
}),
96+
initialValue,
97+
);
98+
99+
// Write default consent to data layer
100+
this.write('consent', 'default', consentRegistry);
101+
102+
// write regions to data layer if they exist
103+
const regions = this.regions;
104+
105+
if (regions) {
106+
regions.forEach(region => {
72107
this.write('consent', 'default', region);
73108
});
109+
}
110+
111+
this.updateConsent(initConsent);
74112

75-
this.updateConsent(initConsent);
113+
this.saveConsent();
114+
}
115+
116+
/**
117+
* Try to set consent types with dataLayer. If a valid
118+
* config was passed, default values for the consent
119+
* types are used. Else, try to load the commands
120+
* set from the cookie if it is available.
121+
*
122+
* @param initConsent - The consent data available, which can be null if the user has not yet given consent.
123+
*/
124+
private initialize(initConsent: ConsentData | null) {
125+
if (this.hasConfig) {
126+
this.loadDefaultsFromConfig(initConsent);
127+
} else {
128+
this.loadSharedConsentFromCookies();
76129
}
77130
}
78131

79132
/**
80-
* Update consent.
133+
* Writes consent updates to the dataLayer
134+
* by applying the configuration (if any) to
135+
* the passed consent data.
81136
*
82-
* @param consentData - The consent data to be set.
137+
* @param consentData - Consent data obtained from the user or null if not available.
83138
*/
84139
updateConsent(consentData: ConsentData | null) {
85-
if (this.config) {
86-
// Dealing with null or undefined consent values
87-
const safeConsent = consentData || {};
88-
140+
if (this.hasConfig && consentData) {
89141
// Fill consent value into consent element, using analytics consent categories
90-
const consentRegistry = Object.keys(
91-
this.configExcludingModeRegionsAndWaitForUpdate,
92-
).reduce((result, consentKey) => {
93-
let consentValue = GoogleConsentType.Denied;
94-
const consent =
95-
this.configExcludingModeRegionsAndWaitForUpdate[consentKey];
96-
97-
if (consent) {
98-
// has consent config key
99-
100-
if (consent.getConsentValue) {
101-
// give priority to custom function
102-
consentValue = consent.getConsentValue(safeConsent);
103-
} else if (
104-
consent?.categories !== undefined &&
105-
consent.categories.every(consent => safeConsent[consent])
106-
) {
107-
// The second option to assign value is by categories list
108-
consentValue = GoogleConsentType.Granted;
142+
const consentRegistry = Object.keys(this.configWithConsentOnly).reduce(
143+
(result, consentKey) => {
144+
let consentValue = GoogleConsentType.Denied;
145+
const consent = this.configWithConsentOnly[consentKey];
146+
147+
if (consent) {
148+
// has consent config key
149+
if (consent.getConsentValue) {
150+
// give priority to custom function
151+
consentValue = consent.getConsentValue(consentData);
152+
} else if (
153+
consent?.categories !== undefined &&
154+
consent.categories.every(consent => consentData[consent])
155+
) {
156+
// The second option to assign value is by categories list
157+
consentValue = GoogleConsentType.Granted;
158+
}
109159
}
110-
}
111160

112-
return {
113-
...result,
114-
[consentKey]: consentValue,
115-
};
116-
}, {});
161+
return {
162+
...result,
163+
[consentKey]: consentValue,
164+
};
165+
},
166+
{},
167+
);
117168

118169
// Write consent to data layer
119170
this.write('consent', 'update', consentRegistry);
171+
172+
this.saveConsent();
173+
}
174+
}
175+
176+
/**
177+
* Saves calculated google consent mode to a cookie
178+
* for sharing consent between apps in same
179+
* domain.
180+
*/
181+
saveConsent() {
182+
if (this.consentDataLayerCommands.length > 0) {
183+
setCookie(
184+
GCM_SHARED_COOKIE_NAME,
185+
JSON.stringify(this.consentDataLayerCommands),
186+
);
120187
}
121188
}
122189

@@ -128,11 +195,8 @@ export class GoogleConsentMode {
128195
* @param consentParams - The consent arguments.
129196
*/
130197
private write(
131-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
132198
consentCommand: 'consent',
133-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
134199
command: 'default' | 'update',
135-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
136200
consentParams:
137201
| Record<string, Array<string> | string | number | undefined>
138202
| undefined,
@@ -141,19 +205,19 @@ export class GoogleConsentMode {
141205
// that was written to the datalayer, so the parameters added to the function signature are only to
142206
// avoid mistakes when calling the function.
143207

144-
if (
145-
this.config &&
146-
typeof window !== 'undefined' &&
147-
consentParams &&
148-
!isEqual(this.lastConsent, consentParams)
149-
) {
208+
if (typeof window !== 'undefined' && consentParams) {
150209
// @ts-ignore
151210
window[this.dataLayer] = window[this.dataLayer] || [];
152211

153212
// @ts-ignore
154213
// eslint-disable-next-line prefer-rest-params
155214
window[this.dataLayer].push(arguments);
156-
this.lastConsent = consentParams;
215+
216+
this.consentDataLayerCommands.push([
217+
consentCommand,
218+
command,
219+
consentParams,
220+
]);
157221
}
158222
}
159223
}

0 commit comments

Comments
 (0)