@owlmeans/consent 0.1.18-rc.5 → 0.1.18-rc.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/consts.d.ts CHANGED
@@ -17,6 +17,16 @@ export declare const CONSENT_COOKIE_DAYS = 365;
17
17
  export declare const CONSENT_SCHEMA_VERSION = 2;
18
18
  /** Idempotence flag for the Consent Mode defaults. Fixed: a page may carry two bundles. */
19
19
  export declare const CONSENT_SETUP_FLAG = "cookieConsentSetup";
20
+ /**
21
+ * The DOM event `applyConsent` dispatches on `window` after it finishes writing globals and
22
+ * pushing the Consent Mode update.
23
+ *
24
+ * A loader that already ran (a gate script that decided, on first paint, not to load yet) has no
25
+ * other way to hear a LATER grant — Consent Mode itself speaks only on `window.dataLayer`, which a
26
+ * tag that has not loaded is not listening to. `event.detail.record` carries the record that was
27
+ * just applied.
28
+ */
29
+ export declare const CONSENT_EVENT = "owlmeans:consent";
20
30
  export declare const CONSENT_ESSENTIAL = "essential";
21
31
  export declare const CONSENT_ANALYTICS = "analytics";
22
32
  export declare const CONSENT_MARKETING = "marketing";
@@ -1 +1 @@
1
- {"version":3,"file":"consts.d.ts","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAEjD;;;;;GAKG;AACH,eAAO,MAAM,WAAW,wBAAwB,CAAA;AAEhD,eAAO,MAAM,mBAAmB,MAAM,CAAA;AAEtC;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,IAAI,CAAA;AAEvC,2FAA2F;AAC3F,eAAO,MAAM,kBAAkB,uBAAuB,CAAA;AAEtD,eAAO,MAAM,iBAAiB,cAAc,CAAA;AAC5C,eAAO,MAAM,iBAAiB,cAAc,CAAA;AAC5C,eAAO,MAAM,iBAAiB,cAAc,CAAA;AAE5C,eAAO,MAAM,eAAe,YAAI,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAU,CAAA;AAExF,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAA;AAE5D;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B,EAAE,eAAe,EAmBvD,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,GAAG,QAAQ,CAQxE,CAAA"}
1
+ {"version":3,"file":"consts.d.ts","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAEjD;;;;;GAKG;AACH,eAAO,MAAM,WAAW,wBAAwB,CAAA;AAEhD,eAAO,MAAM,mBAAmB,MAAM,CAAA;AAEtC;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,IAAI,CAAA;AAEvC,2FAA2F;AAC3F,eAAO,MAAM,kBAAkB,uBAAuB,CAAA;AAEtD;;;;;;;;GAQG;AACH,eAAO,MAAM,aAAa,qBAAqB,CAAA;AAE/C,eAAO,MAAM,iBAAiB,cAAc,CAAA;AAC5C,eAAO,MAAM,iBAAiB,cAAc,CAAA;AAC5C,eAAO,MAAM,iBAAiB,cAAc,CAAA;AAE5C,eAAO,MAAM,eAAe,YAAI,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAU,CAAA;AAExF,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAA;AAE5D;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B,EAAE,eAAe,EAmBvD,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,GAAG,QAAQ,CAQxE,CAAA"}
package/build/consts.js CHANGED
@@ -16,6 +16,16 @@ export const CONSENT_COOKIE_DAYS = 365;
16
16
  export const CONSENT_SCHEMA_VERSION = 2;
17
17
  /** Idempotence flag for the Consent Mode defaults. Fixed: a page may carry two bundles. */
18
18
  export const CONSENT_SETUP_FLAG = 'cookieConsentSetup';
19
+ /**
20
+ * The DOM event `applyConsent` dispatches on `window` after it finishes writing globals and
21
+ * pushing the Consent Mode update.
22
+ *
23
+ * A loader that already ran (a gate script that decided, on first paint, not to load yet) has no
24
+ * other way to hear a LATER grant — Consent Mode itself speaks only on `window.dataLayer`, which a
25
+ * tag that has not loaded is not listening to. `event.detail.record` carries the record that was
26
+ * just applied.
27
+ */
28
+ export const CONSENT_EVENT = 'owlmeans:consent';
19
29
  export const CONSENT_ESSENTIAL = 'essential';
20
30
  export const CONSENT_ANALYTICS = 'analytics';
21
31
  export const CONSENT_MARKETING = 'marketing';
@@ -1 +1 @@
1
- {"version":3,"file":"consts.js","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,qBAAqB,CAAA;AAEhD,MAAM,CAAC,MAAM,mBAAmB,GAAG,GAAG,CAAA;AAEtC;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAA;AAEvC,2FAA2F;AAC3F,MAAM,CAAC,MAAM,kBAAkB,GAAG,oBAAoB,CAAA;AAEtD,MAAM,CAAC,MAAM,iBAAiB,GAAG,WAAW,CAAA;AAC5C,MAAM,CAAC,MAAM,iBAAiB,GAAG,WAAW,CAAA;AAC5C,MAAM,CAAC,MAAM,iBAAiB,GAAG,WAAW,CAAA;AAE5C,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAU,CAAA;AAIxF;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAsB;IAC3D;QACE,GAAG,EAAE,iBAAiB,EAAE,QAAQ,EAAE,IAAI;QACtC,QAAQ,EAAE,WAAW,EAAE,cAAc,EAAE,eAAe;QACtD,SAAS,EAAE,qBAAqB;QAChC,OAAO,EAAE,CAAC,kBAAkB,EAAE,uBAAuB,CAAC;KACvD;IACD;QACE,GAAG,EAAE,iBAAiB;QACtB,QAAQ,EAAE,WAAW,EAAE,cAAc,EAAE,eAAe;QACtD,SAAS,EAAE,qBAAqB;QAChC,OAAO,EAAE,CAAC,mBAAmB,CAAC;KAC/B;IACD;QACE,GAAG,EAAE,iBAAiB;QACtB,QAAQ,EAAE,WAAW,EAAE,cAAc,EAAE,eAAe;QACtD,SAAS,EAAE,qBAAqB;QAChC,OAAO,EAAE,CAAC,YAAY,EAAE,cAAc,EAAE,oBAAoB,CAAC;KAC9D;CACF,CAAA;AAED;;;;;GAKG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAyC;IAC3E,UAAU,EAAE,QAAQ;IACpB,YAAY,EAAE,QAAQ;IACtB,kBAAkB,EAAE,QAAQ;IAC5B,iBAAiB,EAAE,QAAQ;IAC3B,qBAAqB,EAAE,QAAQ;IAC/B,uBAAuB,EAAE,QAAQ;IACjC,gBAAgB,EAAE,SAAS;CAC5B,CAAA"}
1
+ {"version":3,"file":"consts.js","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,qBAAqB,CAAA;AAEhD,MAAM,CAAC,MAAM,mBAAmB,GAAG,GAAG,CAAA;AAEtC;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAA;AAEvC,2FAA2F;AAC3F,MAAM,CAAC,MAAM,kBAAkB,GAAG,oBAAoB,CAAA;AAEtD;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,kBAAkB,CAAA;AAE/C,MAAM,CAAC,MAAM,iBAAiB,GAAG,WAAW,CAAA;AAC5C,MAAM,CAAC,MAAM,iBAAiB,GAAG,WAAW,CAAA;AAC5C,MAAM,CAAC,MAAM,iBAAiB,GAAG,WAAW,CAAA;AAE5C,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAU,CAAA;AAIxF;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAsB;IAC3D;QACE,GAAG,EAAE,iBAAiB,EAAE,QAAQ,EAAE,IAAI;QACtC,QAAQ,EAAE,WAAW,EAAE,cAAc,EAAE,eAAe;QACtD,SAAS,EAAE,qBAAqB;QAChC,OAAO,EAAE,CAAC,kBAAkB,EAAE,uBAAuB,CAAC;KACvD;IACD;QACE,GAAG,EAAE,iBAAiB;QACtB,QAAQ,EAAE,WAAW,EAAE,cAAc,EAAE,eAAe;QACtD,SAAS,EAAE,qBAAqB;QAChC,OAAO,EAAE,CAAC,mBAAmB,CAAC;KAC/B;IACD;QACE,GAAG,EAAE,iBAAiB;QACtB,QAAQ,EAAE,WAAW,EAAE,cAAc,EAAE,eAAe;QACtD,SAAS,EAAE,qBAAqB;QAChC,OAAO,EAAE,CAAC,YAAY,EAAE,cAAc,EAAE,oBAAoB,CAAC;KAC9D;CACF,CAAA;AAED;;;;;GAKG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAyC;IAC3E,UAAU,EAAE,QAAQ;IACpB,YAAY,EAAE,QAAQ;IACtB,kBAAkB,EAAE,QAAQ;IAC5B,iBAAiB,EAAE,QAAQ;IAC3B,qBAAqB,EAAE,QAAQ;IAC/B,uBAAuB,EAAE,QAAQ;IACjC,gBAAgB,EAAE,SAAS;CAC5B,CAAA"}
package/build/gtm.d.ts CHANGED
@@ -26,6 +26,16 @@ export declare const pushConsentDefaults: (opts?: ConsentOptions) => void;
26
26
  * turn, so writing them afterwards would let the first firing see the previous answer.
27
27
  */
28
28
  export declare const applyConsent: (record: ConsentRecord, opts?: ConsentOptions) => void;
29
+ /**
30
+ * Whether a stored decision grants tracking at all — a signal-bearing category, not merely the
31
+ * required essential one.
32
+ *
33
+ * "Signal-bearing" is what a required category can never be: it exists to gate whether a LOADER
34
+ * runs, and a required category is disclosure, not a question — everyone gets it. Only a category
35
+ * that both is optional and drives at least one Consent Mode signal counts, which is exactly what
36
+ * `googleTagHeadScript`'s `'basic'` mode gates a Google tag behind.
37
+ */
38
+ export declare const trackingGranted: (record: ConsentRecord | null, categories?: ConsentCategory[]) => boolean;
29
39
  /**
30
40
  * The inline script a document stamps ABOVE its tag-manager snippet.
31
41
  *
@@ -36,4 +46,27 @@ export declare const applyConsent: (record: ConsentRecord, opts?: ConsentOptions
36
46
  * denied for the first paint of every page.
37
47
  */
38
48
  export declare const consentBootstrapScript: (opts?: ConsentOptions) => string;
49
+ /**
50
+ * Withhold a loader until a stored or later decision grants tracking.
51
+ *
52
+ * The counterpart to `consentBootstrapScript` for the `'basic'` gated loading mode
53
+ * (`@owlmeans/web-gtm`'s `GOOGLE_TAG_DEFAULT_MODE`): where the bootstrap always declares Consent
54
+ * Mode's defaults so a tag may load cookieless, this withholds the tag itself — the loader string
55
+ * is only ever reached once `trackingGranted` is true, either because a returning visitor's stored
56
+ * record already says so, or because `applyConsent` later dispatches {@link CONSENT_EVENT} with
57
+ * one that does. The listener removes itself on the first passing event, so the loader — which
58
+ * marks its own element and refuses to run twice — is asked to run at most once from here.
59
+ *
60
+ * `loaderExpr` is a complete statement or expression that runs the loader when reached — the
61
+ * self-invoking IIFE string `googleTagHeadScript`'s `gtagScript`/`gtmContainerScript` already
62
+ * produce, embedded here verbatim rather than called. It is placed inline exactly as
63
+ * `consentBootstrapScript`'s own output is: dynamic values (the storage key, the category shape,
64
+ * the event name) are JSON-encoded so nothing configured can break out of a string literal, and
65
+ * escaping the RESULT for HTML (`</script`, `<!--`) is left to whoever composes the final inline
66
+ * script — the same discipline `consentBootstrapScript` follows, and `googleTagHeadScript` already
67
+ * applies to the whole script it stamps.
68
+ */
69
+ export declare const consentGateScript: (loaderExpr: string, opts?: ConsentOptions & {
70
+ categories?: ConsentCategory[];
71
+ }) => string;
39
72
  //# sourceMappingURL=gtm.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"gtm.d.ts","sourceRoot":"","sources":["../src/gtm.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAUhF;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,GAAG,KAAK,EAAE,OAAO,EAAE,GAAG,IAAI,CAUrD;AAKD,8EAA8E;AAC9E,eAAO,MAAM,eAAe,gBACb,eAAe,EAAE,KAC7B,MAAM,CAAC,MAAM,EAAE,SAAS,GAAG,QAAQ,CAMrC,CAAA;AAED,sDAAsD;AACtD,eAAO,MAAM,aAAa,WAChB,aAAa,eAAe,eAAe,EAAE,KACpD,MAAM,CAAC,MAAM,EAAE,SAAS,GAAG,QAAQ,CAWrC,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,UAAW,cAAc,KAAG,IAO3D,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,YAAY,WAAY,aAAa,SAAS,cAAc,KAAG,IAqB3E,CAAA;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,sBAAsB,UAAW,cAAc,KAAG,MAwB9D,CAAA"}
1
+ {"version":3,"file":"gtm.d.ts","sourceRoot":"","sources":["../src/gtm.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAUhF;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,GAAG,KAAK,EAAE,OAAO,EAAE,GAAG,IAAI,CAUrD;AAKD,8EAA8E;AAC9E,eAAO,MAAM,eAAe,gBACb,eAAe,EAAE,KAC7B,MAAM,CAAC,MAAM,EAAE,SAAS,GAAG,QAAQ,CAMrC,CAAA;AAED,sDAAsD;AACtD,eAAO,MAAM,aAAa,WAChB,aAAa,eAAe,eAAe,EAAE,KACpD,MAAM,CAAC,MAAM,EAAE,SAAS,GAAG,QAAQ,CAWrC,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,UAAW,cAAc,KAAG,IAO3D,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,YAAY,WAAY,aAAa,SAAS,cAAc,KAAG,IA6B3E,CAAA;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe,WAClB,aAAa,GAAG,IAAI,eAAc,eAAe,EAAE,KAC1D,OAEF,CAAA;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,sBAAsB,UAAW,cAAc,KAAG,MAwB9D,CAAA;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,iBAAiB,eAChB,MAAM,SAAS,cAAc,GAAG;IAAE,UAAU,CAAC,EAAE,eAAe,EAAE,CAAA;CAAE,KAC7E,MAuBF,CAAA"}
package/build/gtm.js CHANGED
@@ -1,4 +1,4 @@
1
- import { CONSENT_KEY, CONSENT_SETUP_FLAG, CONSENT_SIGNAL_DEFAULTS, DEFAULT_CONSENT_CATEGORIES, } from './consts.js';
1
+ import { CONSENT_EVENT, CONSENT_KEY, CONSENT_SETUP_FLAG, CONSENT_SIGNAL_DEFAULTS, DEFAULT_CONSENT_CATEGORIES, } from './consts.js';
2
2
  const global = () => typeof window !== 'undefined' ? window : null;
3
3
  /**
4
4
  * Push onto the tag-manager queue in the shape Google's own snippet uses.
@@ -77,7 +77,24 @@ export const applyConsent = (record, opts) => {
77
77
  win.dataLayer.push({ event: category.event });
78
78
  }
79
79
  }
80
+ // A gate script (`consentGateScript`) that already decided NOT to load, because nothing was
81
+ // granted at the time it ran, has no other way to hear this. Consent Mode itself speaks only on
82
+ // `dataLayer`, which a tag that never loaded is not listening to.
83
+ if (typeof window !== 'undefined' && typeof window.dispatchEvent === 'function'
84
+ && typeof CustomEvent === 'function') {
85
+ window.dispatchEvent(new CustomEvent(CONSENT_EVENT, { detail: { record } }));
86
+ }
80
87
  };
88
+ /**
89
+ * Whether a stored decision grants tracking at all — a signal-bearing category, not merely the
90
+ * required essential one.
91
+ *
92
+ * "Signal-bearing" is what a required category can never be: it exists to gate whether a LOADER
93
+ * runs, and a required category is disclosure, not a question — everyone gets it. Only a category
94
+ * that both is optional and drives at least one Consent Mode signal counts, which is exactly what
95
+ * `googleTagHeadScript`'s `'basic'` mode gates a Google tag behind.
96
+ */
97
+ export const trackingGranted = (record, categories = DEFAULT_CONSENT_CATEGORIES) => record != null && categories.some(category => !category.required && (category.signals?.length ?? 0) > 0 && record[category.key] === true);
81
98
  /**
82
99
  * The inline script a document stamps ABOVE its tag-manager snippet.
83
100
  *
@@ -111,4 +128,47 @@ export const consentBootstrapScript = (opts) => {
111
128
  `u[s]=ok&&u[s]!=='denied'?'granted':'denied'}}` +
112
129
  `g('consent','update',u)})(window,document)`;
113
130
  };
131
+ /**
132
+ * Withhold a loader until a stored or later decision grants tracking.
133
+ *
134
+ * The counterpart to `consentBootstrapScript` for the `'basic'` gated loading mode
135
+ * (`@owlmeans/web-gtm`'s `GOOGLE_TAG_DEFAULT_MODE`): where the bootstrap always declares Consent
136
+ * Mode's defaults so a tag may load cookieless, this withholds the tag itself — the loader string
137
+ * is only ever reached once `trackingGranted` is true, either because a returning visitor's stored
138
+ * record already says so, or because `applyConsent` later dispatches {@link CONSENT_EVENT} with
139
+ * one that does. The listener removes itself on the first passing event, so the loader — which
140
+ * marks its own element and refuses to run twice — is asked to run at most once from here.
141
+ *
142
+ * `loaderExpr` is a complete statement or expression that runs the loader when reached — the
143
+ * self-invoking IIFE string `googleTagHeadScript`'s `gtagScript`/`gtmContainerScript` already
144
+ * produce, embedded here verbatim rather than called. It is placed inline exactly as
145
+ * `consentBootstrapScript`'s own output is: dynamic values (the storage key, the category shape,
146
+ * the event name) are JSON-encoded so nothing configured can break out of a string literal, and
147
+ * escaping the RESULT for HTML (`</script`, `<!--`) is left to whoever composes the final inline
148
+ * script — the same discipline `consentBootstrapScript` follows, and `googleTagHeadScript` already
149
+ * applies to the whole script it stamps.
150
+ */
151
+ export const consentGateScript = (loaderExpr, opts) => {
152
+ const gate = JSON.stringify(categoriesOf(opts).map(category => ({
153
+ key: category.key,
154
+ required: category.required === true,
155
+ hasSignal: (category.signals ?? []).length > 0,
156
+ })));
157
+ const storageKey = JSON.stringify(opts?.storageKey ?? CONSENT_KEY);
158
+ const event = JSON.stringify(CONSENT_EVENT);
159
+ return `(function(w,d){` +
160
+ `function ok(r){if(!r)return false;var cs=${gate};` +
161
+ `for(var i=0;i<cs.length;i++){var c=cs[i];` +
162
+ `if(!c.required&&c.hasSignal&&r[c.key]===true)return true}return false}` +
163
+ `function load(){${loaderExpr}}` +
164
+ `var raw=null;try{raw=w.localStorage.getItem(${storageKey})}catch(e){}` +
165
+ `if(!raw){var p=('; '+d.cookie).split('; '+${storageKey}+'=');` +
166
+ `if(p.length===2){raw=p.pop().split(';').shift()}}` +
167
+ `var r=null;if(raw){try{r=JSON.parse(raw)}catch(e){r=null}}` +
168
+ `if(ok(r)){load();return}` +
169
+ `function h(ev){if(ok(ev&&ev.detail&&ev.detail.record)){` +
170
+ `w.removeEventListener(${event},h);load()}}` +
171
+ `w.addEventListener(${event},h)` +
172
+ `})(window,document)`;
173
+ };
114
174
  //# sourceMappingURL=gtm.js.map
package/build/gtm.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"gtm.js","sourceRoot":"","sources":["../src/gtm.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"gtm.js","sourceRoot":"","sources":["../src/gtm.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owlmeans/consent",
3
- "version": "0.1.18-rc.5",
3
+ "version": "0.1.18-rc.6",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {
package/src/consts.ts CHANGED
@@ -22,6 +22,17 @@ export const CONSENT_SCHEMA_VERSION = 2
22
22
  /** Idempotence flag for the Consent Mode defaults. Fixed: a page may carry two bundles. */
23
23
  export const CONSENT_SETUP_FLAG = 'cookieConsentSetup'
24
24
 
25
+ /**
26
+ * The DOM event `applyConsent` dispatches on `window` after it finishes writing globals and
27
+ * pushing the Consent Mode update.
28
+ *
29
+ * A loader that already ran (a gate script that decided, on first paint, not to load yet) has no
30
+ * other way to hear a LATER grant — Consent Mode itself speaks only on `window.dataLayer`, which a
31
+ * tag that has not loaded is not listening to. `event.detail.record` carries the record that was
32
+ * just applied.
33
+ */
34
+ export const CONSENT_EVENT = 'owlmeans:consent'
35
+
25
36
  export const CONSENT_ESSENTIAL = 'essential'
26
37
  export const CONSENT_ANALYTICS = 'analytics'
27
38
  export const CONSENT_MARKETING = 'marketing'
package/src/gtm.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import {
2
- CONSENT_KEY, CONSENT_SETUP_FLAG, CONSENT_SIGNAL_DEFAULTS, DEFAULT_CONSENT_CATEGORIES,
2
+ CONSENT_EVENT, CONSENT_KEY, CONSENT_SETUP_FLAG, CONSENT_SIGNAL_DEFAULTS,
3
+ DEFAULT_CONSENT_CATEGORIES,
3
4
  } from './consts.js'
4
5
  import type { ConsentCategory, ConsentOptions, ConsentRecord } from './types.js'
5
6
 
@@ -103,8 +104,31 @@ export const applyConsent = (record: ConsentRecord, opts?: ConsentOptions): void
103
104
  win.dataLayer.push({ event: category.event })
104
105
  }
105
106
  }
107
+
108
+ // A gate script (`consentGateScript`) that already decided NOT to load, because nothing was
109
+ // granted at the time it ran, has no other way to hear this. Consent Mode itself speaks only on
110
+ // `dataLayer`, which a tag that never loaded is not listening to.
111
+ if (typeof window !== 'undefined' && typeof window.dispatchEvent === 'function'
112
+ && typeof CustomEvent === 'function') {
113
+ window.dispatchEvent(new CustomEvent(CONSENT_EVENT, { detail: { record } }))
114
+ }
106
115
  }
107
116
 
117
+ /**
118
+ * Whether a stored decision grants tracking at all — a signal-bearing category, not merely the
119
+ * required essential one.
120
+ *
121
+ * "Signal-bearing" is what a required category can never be: it exists to gate whether a LOADER
122
+ * runs, and a required category is disclosure, not a question — everyone gets it. Only a category
123
+ * that both is optional and drives at least one Consent Mode signal counts, which is exactly what
124
+ * `googleTagHeadScript`'s `'basic'` mode gates a Google tag behind.
125
+ */
126
+ export const trackingGranted = (
127
+ record: ConsentRecord | null, categories: ConsentCategory[] = DEFAULT_CONSENT_CATEGORIES
128
+ ): boolean => record != null && categories.some(
129
+ category => !category.required && (category.signals?.length ?? 0) > 0 && record[category.key] === true
130
+ )
131
+
108
132
  /**
109
133
  * The inline script a document stamps ABOVE its tag-manager snippet.
110
134
  *
@@ -139,3 +163,50 @@ export const consentBootstrapScript = (opts?: ConsentOptions): string => {
139
163
  `u[s]=ok&&u[s]!=='denied'?'granted':'denied'}}` +
140
164
  `g('consent','update',u)})(window,document)`
141
165
  }
166
+
167
+ /**
168
+ * Withhold a loader until a stored or later decision grants tracking.
169
+ *
170
+ * The counterpart to `consentBootstrapScript` for the `'basic'` gated loading mode
171
+ * (`@owlmeans/web-gtm`'s `GOOGLE_TAG_DEFAULT_MODE`): where the bootstrap always declares Consent
172
+ * Mode's defaults so a tag may load cookieless, this withholds the tag itself — the loader string
173
+ * is only ever reached once `trackingGranted` is true, either because a returning visitor's stored
174
+ * record already says so, or because `applyConsent` later dispatches {@link CONSENT_EVENT} with
175
+ * one that does. The listener removes itself on the first passing event, so the loader — which
176
+ * marks its own element and refuses to run twice — is asked to run at most once from here.
177
+ *
178
+ * `loaderExpr` is a complete statement or expression that runs the loader when reached — the
179
+ * self-invoking IIFE string `googleTagHeadScript`'s `gtagScript`/`gtmContainerScript` already
180
+ * produce, embedded here verbatim rather than called. It is placed inline exactly as
181
+ * `consentBootstrapScript`'s own output is: dynamic values (the storage key, the category shape,
182
+ * the event name) are JSON-encoded so nothing configured can break out of a string literal, and
183
+ * escaping the RESULT for HTML (`</script`, `<!--`) is left to whoever composes the final inline
184
+ * script — the same discipline `consentBootstrapScript` follows, and `googleTagHeadScript` already
185
+ * applies to the whole script it stamps.
186
+ */
187
+ export const consentGateScript = (
188
+ loaderExpr: string, opts?: ConsentOptions & { categories?: ConsentCategory[] }
189
+ ): string => {
190
+ const gate = JSON.stringify(categoriesOf(opts).map(category => ({
191
+ key: category.key,
192
+ required: category.required === true,
193
+ hasSignal: (category.signals ?? []).length > 0,
194
+ })))
195
+ const storageKey = JSON.stringify(opts?.storageKey ?? CONSENT_KEY)
196
+ const event = JSON.stringify(CONSENT_EVENT)
197
+
198
+ return `(function(w,d){` +
199
+ `function ok(r){if(!r)return false;var cs=${gate};` +
200
+ `for(var i=0;i<cs.length;i++){var c=cs[i];` +
201
+ `if(!c.required&&c.hasSignal&&r[c.key]===true)return true}return false}` +
202
+ `function load(){${loaderExpr}}` +
203
+ `var raw=null;try{raw=w.localStorage.getItem(${storageKey})}catch(e){}` +
204
+ `if(!raw){var p=('; '+d.cookie).split('; '+${storageKey}+'=');` +
205
+ `if(p.length===2){raw=p.pop().split(';').shift()}}` +
206
+ `var r=null;if(raw){try{r=JSON.parse(raw)}catch(e){r=null}}` +
207
+ `if(ok(r)){load();return}` +
208
+ `function h(ev){if(ok(ev&&ev.detail&&ev.detail.record)){` +
209
+ `w.removeEventListener(${event},h);load()}}` +
210
+ `w.addEventListener(${event},h)` +
211
+ `})(window,document)`
212
+ }
@@ -1,10 +1,10 @@
1
1
  import { describe, test, expect, beforeEach } from 'bun:test'
2
2
  import {
3
- CONSENT_ANALYTICS, CONSENT_ESSENTIAL, CONSENT_KEY, CONSENT_LOCALES, CONSENT_MARKETING,
4
- CONSENT_SCHEMA_VERSION, DEFAULT_CONSENT_CATEGORIES,
3
+ CONSENT_ANALYTICS, CONSENT_ESSENTIAL, CONSENT_EVENT, CONSENT_KEY, CONSENT_LOCALES,
4
+ CONSENT_MARKETING, CONSENT_SCHEMA_VERSION, DEFAULT_CONSENT_CATEGORIES,
5
5
  } from '../src/consts.js'
6
6
  import { migrateConsent, readConsent, writeConsent, clearConsent } from '../src/storage.js'
7
- import { consentDefaults, consentUpdate, consentBootstrapScript } from '../src/gtm.js'
7
+ import { applyConsent, consentDefaults, consentUpdate, consentBootstrapScript } from '../src/gtm.js'
8
8
  import { makeConsentStore } from '../src/store.js'
9
9
  import {
10
10
  DEFAULT_CONSENT_MESSAGES, defaultConsentTranslate, interpolate, normalizeLocale,
@@ -117,6 +117,36 @@ describe('consent mode signals', () => {
117
117
  })
118
118
  })
119
119
 
120
+ describe('applyConsent dispatches a DOM event', () => {
121
+ test('fires CONSENT_EVENT with the record after writing globals and pushing the update', () => {
122
+ const seen: unknown[] = []
123
+ const listener = (event: Event) => { seen.push((event as CustomEvent).detail) }
124
+ ;(globalThis as any).addEventListener(CONSENT_EVENT, listener)
125
+
126
+ try {
127
+ applyConsent({ [CONSENT_ANALYTICS]: true })
128
+
129
+ expect(seen).toEqual([{ record: { [CONSENT_ANALYTICS]: true } }])
130
+ } finally {
131
+ ;(globalThis as any).removeEventListener(CONSENT_EVENT, listener)
132
+ }
133
+ })
134
+
135
+ test('silent mode dispatches nothing', () => {
136
+ const seen: unknown[] = []
137
+ const listener = (event: Event) => { seen.push((event as CustomEvent).detail) }
138
+ ;(globalThis as any).addEventListener(CONSENT_EVENT, listener)
139
+
140
+ try {
141
+ applyConsent({ [CONSENT_ANALYTICS]: true }, { silent: true })
142
+
143
+ expect(seen).toEqual([])
144
+ } finally {
145
+ ;(globalThis as any).removeEventListener(CONSENT_EVENT, listener)
146
+ }
147
+ })
148
+ })
149
+
120
150
  describe('the bootstrap script', () => {
121
151
  const script = consentBootstrapScript()
122
152
 
@@ -0,0 +1,191 @@
1
+ import { describe, test, expect } from 'bun:test'
2
+ import { CONSENT_ANALYTICS, CONSENT_ESSENTIAL, CONSENT_EVENT, CONSENT_KEY, CONSENT_MARKETING } from '../src/consts.js'
3
+ import { consentGateScript, trackingGranted } from '../src/gtm.js'
4
+ import type { ConsentCategory, ConsentRecord } from '../src/types.js'
5
+
6
+ describe('trackingGranted', () => {
7
+ test('no record at all is not granted', () => {
8
+ expect(trackingGranted(null)).toBe(false)
9
+ })
10
+
11
+ test('an essential-only record is not granted — required categories never count', () => {
12
+ const record: ConsentRecord = {
13
+ [CONSENT_ESSENTIAL]: true, [CONSENT_ANALYTICS]: false, [CONSENT_MARKETING]: false,
14
+ }
15
+
16
+ expect(trackingGranted(record)).toBe(false)
17
+ })
18
+
19
+ test('analytics granted is tracking-granted', () => {
20
+ const record: ConsentRecord = { [CONSENT_ESSENTIAL]: true, [CONSENT_ANALYTICS]: true }
21
+
22
+ expect(trackingGranted(record)).toBe(true)
23
+ })
24
+
25
+ test('marketing granted is tracking-granted too', () => {
26
+ const record: ConsentRecord = { [CONSENT_ESSENTIAL]: true, [CONSENT_MARKETING]: true }
27
+
28
+ expect(trackingGranted(record)).toBe(true)
29
+ })
30
+
31
+ test('a required category is ignored even when the record marks it granted', () => {
32
+ // A category can be BOTH required and signal-bearing (`essential` drives security_storage /
33
+ // functionality_storage by default) — required must still veto it, because everyone gets it
34
+ // regardless of what the record says.
35
+ const categories: ConsentCategory[] = [
36
+ {
37
+ key: 'essential', required: true, labelKey: 'a', descriptionKey: 'b',
38
+ signals: ['security_storage'],
39
+ },
40
+ ]
41
+
42
+ expect(trackingGranted({ essential: true }, categories)).toBe(false)
43
+ })
44
+
45
+ test('a category with no signals is ignored even when granted', () => {
46
+ const categories: ConsentCategory[] = [
47
+ { key: 'preferences', labelKey: 'a', descriptionKey: 'b' },
48
+ ]
49
+
50
+ expect(trackingGranted({ preferences: true }, categories)).toBe(false)
51
+ })
52
+ })
53
+
54
+ describe('consentGateScript — the two reachable paths', () => {
55
+ /** A minimal window/document the generated script can run against, with an event bus. */
56
+ const makeBrowser = (stored?: { key?: string, record: Record<string, unknown> }) => {
57
+ const storage = new Map<string, string>()
58
+ if (stored != null) {
59
+ storage.set(stored.key ?? CONSENT_KEY, JSON.stringify(stored.record))
60
+ }
61
+ const listeners = new Map<string, ((event: unknown) => void)[]>()
62
+ const win: Record<string, unknown> & {
63
+ addEventListener: (type: string, cb: (event: unknown) => void) => void
64
+ removeEventListener: (type: string, cb: (event: unknown) => void) => void
65
+ } = {
66
+ localStorage: { getItem: (k: string) => storage.get(k) ?? null },
67
+ addEventListener: (type, cb) => {
68
+ listeners.set(type, [...(listeners.get(type) ?? []), cb])
69
+ },
70
+ removeEventListener: (type, cb) => {
71
+ listeners.set(type, (listeners.get(type) ?? []).filter(listener => listener !== cb))
72
+ },
73
+ }
74
+ const doc = { cookie: '' }
75
+
76
+ return {
77
+ win,
78
+ run: (script: string) => {
79
+ // eslint-disable-next-line no-new-func
80
+ new Function('window', 'document', script)(win, doc)
81
+ },
82
+ dispatch: (detail: unknown) => {
83
+ for (const cb of listeners.get(CONSENT_EVENT) ?? []) {
84
+ cb({ detail })
85
+ }
86
+ },
87
+ listenerCount: () => listeners.get(CONSENT_EVENT)?.length ?? 0,
88
+ }
89
+ }
90
+
91
+ test('a returning visitor with a stored grant runs the loader immediately, synchronously', () => {
92
+ const browser = makeBrowser({ record: { essential: true, analytics: true, v: 2 } })
93
+ browser.win.__loaded = 0
94
+ browser.run(consentGateScript('window.__loaded = (window.__loaded || 0) + 1'))
95
+
96
+ expect(browser.win.__loaded).toBe(1)
97
+ // Nothing left listening — the immediate branch never subscribes.
98
+ expect(browser.listenerCount()).toBe(0)
99
+ })
100
+
101
+ test('a stored record that grants nothing does not run the loader immediately', () => {
102
+ const browser = makeBrowser({ record: { essential: true, analytics: false, v: 2 } })
103
+ browser.win.__loaded = 0
104
+ browser.run(consentGateScript('window.__loaded = (window.__loaded || 0) + 1'))
105
+
106
+ expect(browser.win.__loaded).toBe(0)
107
+ expect(browser.listenerCount()).toBe(1)
108
+ })
109
+
110
+ test('no stored record at all falls to the event-listener branch and waits', () => {
111
+ const browser = makeBrowser()
112
+ browser.win.__loaded = 0
113
+ browser.run(consentGateScript('window.__loaded = (window.__loaded || 0) + 1'))
114
+
115
+ expect(browser.win.__loaded).toBe(0)
116
+
117
+ // A denying event changes nothing.
118
+ browser.dispatch({ record: { essential: true, analytics: false } })
119
+ expect(browser.win.__loaded).toBe(0)
120
+
121
+ // The first granting event runs the loader and removes the listener.
122
+ browser.dispatch({ record: { essential: true, analytics: true } })
123
+ expect(browser.win.__loaded).toBe(1)
124
+ expect(browser.listenerCount()).toBe(0)
125
+
126
+ // A later event does nothing further — the listener already removed itself.
127
+ browser.dispatch({ record: { essential: true, analytics: true } })
128
+ expect(browser.win.__loaded).toBe(1)
129
+ })
130
+
131
+ test('the cookie answers when localStorage has nothing stored', () => {
132
+ // Same fallback order `consentBootstrapScript`/`readConsent` use: localStorage first, the
133
+ // cookie second. Simulated here by handing the script a `document` whose cookie carries the
134
+ // record and a `window` whose localStorage is empty.
135
+ const record = JSON.stringify({ essential: true, analytics: true, v: 2 })
136
+ const win: Record<string, unknown> & {
137
+ addEventListener: (type: string, cb: (event: unknown) => void) => void
138
+ removeEventListener: (type: string, cb: (event: unknown) => void) => void
139
+ } = {
140
+ localStorage: { getItem: () => null },
141
+ addEventListener: () => {},
142
+ removeEventListener: () => {},
143
+ __loaded: 0,
144
+ }
145
+ const doc = { cookie: `${CONSENT_KEY}=${record}` }
146
+ // eslint-disable-next-line no-new-func
147
+ new Function(
148
+ 'window', 'document', consentGateScript('window.__loaded = (window.__loaded || 0) + 1')
149
+ )(win, doc)
150
+
151
+ expect(win.__loaded).toBe(1)
152
+ })
153
+
154
+ test('only a signal-bearing, non-required category can satisfy the gate', () => {
155
+ const categories: ConsentCategory[] = [
156
+ { key: 'essential', required: true, labelKey: 'a', descriptionKey: 'b', signals: ['security_storage'] },
157
+ { key: 'analytics', labelKey: 'c', descriptionKey: 'd', signals: ['analytics_storage'] },
158
+ ]
159
+ const browser = makeBrowser({ record: { essential: true, analytics: false, v: 2 } })
160
+ browser.win.__loaded = 0
161
+ browser.run(consentGateScript('window.__loaded = (window.__loaded || 0) + 1', { categories }))
162
+
163
+ expect(browser.win.__loaded).toBe(0)
164
+
165
+ browser.dispatch({ record: { essential: true, analytics: true } })
166
+ expect(browser.win.__loaded).toBe(1)
167
+ })
168
+
169
+ test('the generated script carries both reachable code paths in its text', () => {
170
+ const script = consentGateScript('window.__loaded = (window.__loaded || 0) + 1')
171
+
172
+ // The immediate branch: `load()` called right after the stored-record check, before any
173
+ // listener is attached.
174
+ expect(script.indexOf('load()')).toBeLessThan(script.indexOf('addEventListener'))
175
+ // The event-listener branch: the same loader, reachable a second time from inside the handler
176
+ // — plus the `function load(){...}` declaration itself, three occurrences of `load()` in all.
177
+ expect(script.match(/load\(\)/g)?.length).toBe(3)
178
+ expect(script).toContain(JSON.stringify(CONSENT_EVENT))
179
+ expect(script).toContain(JSON.stringify(CONSENT_KEY))
180
+ })
181
+
182
+ test('a custom storage key is honoured, matching the bootstrap script\'s own lookup', () => {
183
+ const browser = makeBrowser({ key: 'custom_key', record: { essential: true, analytics: true, v: 2 } })
184
+ browser.win.__loaded = 0
185
+ browser.run(consentGateScript(
186
+ 'window.__loaded = (window.__loaded || 0) + 1', { storageKey: 'custom_key' }
187
+ ))
188
+
189
+ expect(browser.win.__loaded).toBe(1)
190
+ })
191
+ })