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' ;
23import {
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 */
1213export 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