@owlmeans/consent 0.1.18-rc.2

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.
Files changed (53) hide show
  1. package/build/.gitkeep +0 -0
  2. package/build/consts.d.ts +40 -0
  3. package/build/consts.d.ts.map +1 -0
  4. package/build/consts.js +65 -0
  5. package/build/consts.js.map +1 -0
  6. package/build/gtm.d.ts +39 -0
  7. package/build/gtm.d.ts.map +1 -0
  8. package/build/gtm.js +114 -0
  9. package/build/gtm.js.map +1 -0
  10. package/build/i18n/be.json +25 -0
  11. package/build/i18n/de.json +25 -0
  12. package/build/i18n/en.json +25 -0
  13. package/build/i18n/es.json +25 -0
  14. package/build/i18n/pl.json +25 -0
  15. package/build/i18n/ru.json +25 -0
  16. package/build/i18n/uk.json +25 -0
  17. package/build/i18n.d.ts +16 -0
  18. package/build/i18n.d.ts.map +1 -0
  19. package/build/i18n.js +28 -0
  20. package/build/i18n.js.map +1 -0
  21. package/build/index.d.ts +7 -0
  22. package/build/index.d.ts.map +1 -0
  23. package/build/index.js +6 -0
  24. package/build/index.js.map +1 -0
  25. package/build/storage.d.ts +27 -0
  26. package/build/storage.d.ts.map +1 -0
  27. package/build/storage.js +86 -0
  28. package/build/storage.js.map +1 -0
  29. package/build/store.d.ts +16 -0
  30. package/build/store.d.ts.map +1 -0
  31. package/build/store.js +70 -0
  32. package/build/store.js.map +1 -0
  33. package/build/types.d.ts +77 -0
  34. package/build/types.d.ts.map +1 -0
  35. package/build/types.js +2 -0
  36. package/build/types.js.map +1 -0
  37. package/package.json +33 -0
  38. package/src/consts.ts +75 -0
  39. package/src/gtm.ts +141 -0
  40. package/src/i18n/be.json +25 -0
  41. package/src/i18n/de.json +25 -0
  42. package/src/i18n/en.json +25 -0
  43. package/src/i18n/es.json +25 -0
  44. package/src/i18n/pl.json +25 -0
  45. package/src/i18n/ru.json +25 -0
  46. package/src/i18n/uk.json +25 -0
  47. package/src/i18n.ts +38 -0
  48. package/src/index.ts +6 -0
  49. package/src/storage.ts +96 -0
  50. package/src/store.ts +94 -0
  51. package/src/types.ts +83 -0
  52. package/tests/consent.spec.ts +208 -0
  53. package/tsconfig.json +11 -0
@@ -0,0 +1,86 @@
1
+ import { CONSENT_COOKIE_DAYS, CONSENT_ESSENTIAL, CONSENT_KEY, CONSENT_SCHEMA_VERSION, } from './consts.js';
2
+ const key = (opts) => opts?.storageKey ?? CONSENT_KEY;
3
+ const parse = (raw) => {
4
+ if (raw == null || raw === '') {
5
+ return null;
6
+ }
7
+ try {
8
+ const value = JSON.parse(raw);
9
+ return value != null && typeof value === 'object' ? value : null;
10
+ }
11
+ catch {
12
+ return null;
13
+ }
14
+ };
15
+ /**
16
+ * Bring a stored record up to the current shape.
17
+ *
18
+ * A record with no `v` was written before the essential category existed, when it was implicitly
19
+ * always granted. The visitor DID choose; upgrading in place honours that choice, where treating
20
+ * the record as unusable would re-prompt everyone who has ever visited.
21
+ */
22
+ export const migrateConsent = (raw) => raw == null ? null
23
+ : raw.v === CONSENT_SCHEMA_VERSION ? raw
24
+ : { ...raw, [CONSENT_ESSENTIAL]: true, v: CONSENT_SCHEMA_VERSION };
25
+ const fromCookie = (name) => {
26
+ if (typeof document === 'undefined') {
27
+ return null;
28
+ }
29
+ const parts = `; ${document.cookie}`.split(`; ${name}=`);
30
+ return parts.length === 2 ? parts.pop()?.split(';').shift() ?? null : null;
31
+ };
32
+ /**
33
+ * What this browser has already chosen, or null.
34
+ *
35
+ * localStorage first and the cookie second, because that is the order the widget this generalises
36
+ * used and the two can disagree: a visitor who cleared site data but kept cookies still has an
37
+ * answer, and asking them again would be wrong.
38
+ */
39
+ export const readConsent = (opts) => {
40
+ const name = key(opts);
41
+ let raw = null;
42
+ try {
43
+ raw = typeof localStorage !== 'undefined' ? localStorage.getItem(name) : null;
44
+ }
45
+ catch { /* private modes and blocked storage both throw; the cookie may still answer */ }
46
+ return migrateConsent(parse(raw) ?? parse(fromCookie(name)));
47
+ };
48
+ /**
49
+ * Record the choice in both places.
50
+ *
51
+ * Two stores rather than one because neither is reliable alone: localStorage is cleared by "clear
52
+ * site data" while the cookie survives it, and the cookie is refused where third-party storage is
53
+ * blocked while localStorage may not be.
54
+ */
55
+ export const writeConsent = (record, opts) => {
56
+ const name = key(opts);
57
+ const value = JSON.stringify({ ...record, v: CONSENT_SCHEMA_VERSION });
58
+ try {
59
+ if (typeof localStorage !== 'undefined') {
60
+ localStorage.setItem(name, value);
61
+ }
62
+ }
63
+ catch { /* the cookie below is the fallback */ }
64
+ if (typeof document === 'undefined') {
65
+ return;
66
+ }
67
+ const days = opts?.cookieDays ?? CONSENT_COOKIE_DAYS;
68
+ const expires = new Date(Date.now() + days * 24 * 60 * 60 * 1000).toUTCString();
69
+ const domain = opts?.cookieDomain != null ? `;domain=${opts.cookieDomain}` : '';
70
+ // `SameSite=Lax` is stated rather than left to the browser default, which differs between them.
71
+ // This cookie is never sent cross-site on purpose — it records a preference, not a session.
72
+ document.cookie = `${name}=${value};expires=${expires};path=/;SameSite=Lax${domain}`;
73
+ };
74
+ export const clearConsent = (opts) => {
75
+ const name = key(opts);
76
+ try {
77
+ if (typeof localStorage !== 'undefined') {
78
+ localStorage.removeItem(name);
79
+ }
80
+ }
81
+ catch { /* nothing to remove if storage was never available */ }
82
+ if (typeof document !== 'undefined') {
83
+ document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/`;
84
+ }
85
+ };
86
+ //# sourceMappingURL=storage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage.js","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import type { ConsentReason, ConsentStore } from './types.js';
2
+ /**
3
+ * The consent state of this DOCUMENT.
4
+ *
5
+ * A module singleton rather than something hung off a context, because that is what consent
6
+ * actually is — a property of the browser and the page, not of one component tree. It also has to
7
+ * be reachable from places that are not React at all: the sign-in precondition runs inside a click
8
+ * handler, and a tag-manager helper runs before any component has mounted.
9
+ */
10
+ export declare const makeConsentStore: () => ConsentStore;
11
+ export declare const consentStore: ConsentStore;
12
+ /** Open the preferences dialog from anywhere — a footer link, a policy page, a login gate. */
13
+ export declare const openConsent: (reason?: ConsentReason) => void;
14
+ /** Whether a category is granted, for the callers that are not components. */
15
+ export declare const isConsented: (key: string) => boolean;
16
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACuB,aAAa,EAA+B,YAAY,EAC1F,MAAM,YAAY,CAAA;AAEnB;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB,QAAO,YAsEnC,CAAA;AAED,eAAO,MAAM,YAAY,EAAE,YAAiC,CAAA;AAE5D,8FAA8F;AAC9F,eAAO,MAAM,WAAW,YAAa,aAAa,KAAG,IAAiC,CAAA;AAEtF,8EAA8E;AAC9E,eAAO,MAAM,WAAW,QAAS,MAAM,KAAG,OAAoC,CAAA"}
package/build/store.js ADDED
@@ -0,0 +1,70 @@
1
+ import { CONSENT_KEY, DEFAULT_CONSENT_CATEGORIES } from './consts.js';
2
+ import { applyConsent, pushConsentDefaults } from './gtm.js';
3
+ import { readConsent, writeConsent } from './storage.js';
4
+ /**
5
+ * The consent state of this DOCUMENT.
6
+ *
7
+ * A module singleton rather than something hung off a context, because that is what consent
8
+ * actually is — a property of the browser and the page, not of one component tree. It also has to
9
+ * be reachable from places that are not React at all: the sign-in precondition runs inside a click
10
+ * handler, and a tag-manager helper runs before any component has mounted.
11
+ */
12
+ export const makeConsentStore = () => {
13
+ const listeners = new Set();
14
+ let state = { record: null, open: false, reason: null };
15
+ let options = {};
16
+ const publish = (next) => {
17
+ state = { ...state, ...next };
18
+ listeners.forEach(listener => listener(state));
19
+ };
20
+ const resolved = () => ({
21
+ ...options,
22
+ categories: options.categories ?? DEFAULT_CONSENT_CATEGORIES,
23
+ storageKey: options.storageKey ?? CONSENT_KEY,
24
+ });
25
+ const store = {
26
+ get: () => state,
27
+ subscribe: listener => {
28
+ listeners.add(listener);
29
+ return () => { listeners.delete(listener); };
30
+ },
31
+ init: opts => {
32
+ options = { ...options, ...opts };
33
+ // Before anything is read, so a page with no stored answer still declares what is denied.
34
+ pushConsentDefaults(options);
35
+ const record = readConsent(options);
36
+ if (record != null) {
37
+ applyConsent(record, options);
38
+ publish({ record, open: false, reason: null });
39
+ return;
40
+ }
41
+ publish({ record: null, open: true, reason: 'initial' });
42
+ },
43
+ save: record => {
44
+ writeConsent(record, options);
45
+ applyConsent(record, options);
46
+ publish({ record, open: false, reason: null });
47
+ },
48
+ acceptAll: () => {
49
+ const record = Object.fromEntries(resolved().categories.map(category => [category.key, true]));
50
+ store.save(record);
51
+ },
52
+ open: (reason) => { publish({ open: true, reason: reason ?? 'reopen' }); },
53
+ close: () => { publish({ open: false, reason: null }); },
54
+ granted: key => {
55
+ const category = resolved().categories.find(candidate => candidate.key === key);
56
+ if (category?.required === true) {
57
+ return true;
58
+ }
59
+ return state.record?.[key] === true;
60
+ },
61
+ options: resolved,
62
+ };
63
+ return store;
64
+ };
65
+ export const consentStore = makeConsentStore();
66
+ /** Open the preferences dialog from anywhere — a footer link, a policy page, a login gate. */
67
+ export const openConsent = (reason) => consentStore.open(reason);
68
+ /** Whether a category is granted, for the callers that are not components. */
69
+ export const isConsented = (key) => consentStore.granted(key);
70
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Google Consent Mode v2 signals.
3
+ *
4
+ * Named here rather than left as free strings because a typo in a signal name is silent: the tag
5
+ * manager simply never sees the update, and the only symptom is analytics that stay switched off
6
+ * for everyone who agreed to them.
7
+ */
8
+ export type ConsentSignal = 'ad_storage' | 'ad_user_data' | 'ad_personalization' | 'analytics_storage' | 'functionality_storage' | 'personalization_storage' | 'security_storage';
9
+ export interface ConsentCategory {
10
+ /** Stable storage key. `essential` is reserved and always required. */
11
+ key: string;
12
+ /**
13
+ * Always granted, rendered locked and labelled as required.
14
+ *
15
+ * Strictly-necessary storage does not need consent, and a dialog that presents it as a choice is
16
+ * itself a dark pattern. A required category is disclosure, not a question.
17
+ */
18
+ required?: boolean;
19
+ /** Resolved through `translate`; the built-in bundle supplies the default set in 7 languages. */
20
+ labelKey: string;
21
+ descriptionKey: string;
22
+ /**
23
+ * The window global this category yields to: `window[globalVar] = granted`, written on every
24
+ * apply and BEFORE the tag-manager push.
25
+ *
26
+ * This is the seam for anything that cannot subscribe — a custom-HTML tag reading a flag, a
27
+ * hand-placed pixel, a script that only ever runs once. Without it, integrating a tag that is
28
+ * not Consent-Mode-aware means reaching into this package.
29
+ */
30
+ globalVar?: string;
31
+ /** Consent Mode v2 signals this category drives. */
32
+ signals?: ConsentSignal[];
33
+ /** dataLayer event pushed when this category transitions from denied to granted. */
34
+ event?: string;
35
+ }
36
+ /** `v` is the schema version; every other key is a category key. */
37
+ export interface ConsentRecord {
38
+ v?: number;
39
+ [category: string]: boolean | number | undefined;
40
+ }
41
+ export interface ConsentOptions {
42
+ categories?: ConsentCategory[];
43
+ storageKey?: string;
44
+ cookieDays?: number;
45
+ /**
46
+ * Left undefined by default, and that is deliberate: setting a domain orphans the existing
47
+ * host-only cookie, so every visitor who has already chosen would be asked again.
48
+ */
49
+ cookieDomain?: string;
50
+ /** Skip every dataLayer and global write. For tests, and for an app that runs no tags. */
51
+ silent?: boolean;
52
+ }
53
+ /** Why the dialog is open. `login` is what the sign-in precondition raises. */
54
+ export type ConsentReason = 'initial' | 'reopen' | 'login' | string;
55
+ export interface ConsentState {
56
+ record: ConsentRecord | null;
57
+ open: boolean;
58
+ reason: ConsentReason | null;
59
+ }
60
+ export type ConsentListener = (state: ConsentState) => void;
61
+ export interface ConsentStore {
62
+ get: () => ConsentState;
63
+ subscribe: (listener: ConsentListener) => () => void;
64
+ /** Push the defaults, load and migrate any stored record, apply it, and open when there is none. */
65
+ init: (opts?: ConsentOptions) => void;
66
+ save: (record: ConsentRecord) => void;
67
+ acceptAll: () => void;
68
+ open: (reason?: ConsentReason) => void;
69
+ close: () => void;
70
+ /** Imperative reader, for the callers that are not React. */
71
+ granted: (key: string) => boolean;
72
+ options: () => ConsentOptions & {
73
+ categories: ConsentCategory[];
74
+ storageKey: string;
75
+ };
76
+ }
77
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,MAAM,aAAa,GACrB,YAAY,GAAG,cAAc,GAAG,oBAAoB,GACpD,mBAAmB,GAAG,uBAAuB,GAC7C,yBAAyB,GAAG,kBAAkB,CAAA;AAElD,MAAM,WAAW,eAAe;IAC9B,uEAAuE;IACvE,GAAG,EAAE,MAAM,CAAA;IACX;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,iGAAiG;IACjG,QAAQ,EAAE,MAAM,CAAA;IAChB,cAAc,EAAE,MAAM,CAAA;IACtB;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,oDAAoD;IACpD,OAAO,CAAC,EAAE,aAAa,EAAE,CAAA;IACzB,oFAAoF;IACpF,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,oEAAoE;AACpE,MAAM,WAAW,aAAa;IAC5B,CAAC,CAAC,EAAE,MAAM,CAAA;IACV,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,CAAA;CACjD;AAED,MAAM,WAAW,cAAc;IAC7B,UAAU,CAAC,EAAE,eAAe,EAAE,CAAA;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,0FAA0F;IAC1F,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB;AAED,+EAA+E;AAC/E,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,QAAQ,GAAG,OAAO,GAAG,MAAM,CAAA;AAEnE,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,aAAa,GAAG,IAAI,CAAA;IAC5B,IAAI,EAAE,OAAO,CAAA;IACb,MAAM,EAAE,aAAa,GAAG,IAAI,CAAA;CAC7B;AAED,MAAM,MAAM,eAAe,GAAG,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAA;AAE3D,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,YAAY,CAAA;IACvB,SAAS,EAAE,CAAC,QAAQ,EAAE,eAAe,KAAK,MAAM,IAAI,CAAA;IACpD,oGAAoG;IACpG,IAAI,EAAE,CAAC,IAAI,CAAC,EAAE,cAAc,KAAK,IAAI,CAAA;IACrC,IAAI,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,IAAI,CAAA;IACrC,SAAS,EAAE,MAAM,IAAI,CAAA;IACrB,IAAI,EAAE,CAAC,MAAM,CAAC,EAAE,aAAa,KAAK,IAAI,CAAA;IACtC,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB,6DAA6D;IAC7D,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAA;IACjC,OAAO,EAAE,MAAM,cAAc,GAAG;QAAE,UAAU,EAAE,eAAe,EAAE,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAA;CACtF"}
package/build/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@owlmeans/consent",
3
+ "version": "0.1.18-rc.2",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "scripts": {
7
+ "build": "tsc -b",
8
+ "dev": "sleep 2 && nodemon -e ts,json --watch src --exec \"tsc -p ./tsconfig.json\"",
9
+ "watch": "tsc -b -w --preserveWatchOutput --pretty",
10
+ "test": "bun test ./tests"
11
+ },
12
+ "main": "build/index.js",
13
+ "module": "build/index.js",
14
+ "types": "build/index.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "import": "./build/index.js",
18
+ "require": "./build/index.js",
19
+ "default": "./build/index.js",
20
+ "module": "./build/index.js",
21
+ "types": "./build/index.d.ts"
22
+ }
23
+ },
24
+ "devDependencies": {
25
+ "@owlmeans/dep-config": "workspace:*",
26
+ "@types/bun": "^1.4.0",
27
+ "nodemon": "^3.1.14",
28
+ "typescript": "^7.0.2"
29
+ },
30
+ "publishConfig": {
31
+ "access": "public"
32
+ }
33
+ }
package/src/consts.ts ADDED
@@ -0,0 +1,75 @@
1
+ import type { ConsentCategory } from './types.js'
2
+
3
+ /**
4
+ * Where a visitor's choice is stored.
5
+ *
6
+ * Unchanged from the widget this package generalises, and it must stay unchanged: owlmeans.com has
7
+ * visitors who have already chosen, and a new key would ask every one of them again.
8
+ */
9
+ export const CONSENT_KEY = 'site_cookie_consent'
10
+
11
+ export const CONSENT_COOKIE_DAYS = 365
12
+
13
+ /**
14
+ * The stored record's shape version.
15
+ *
16
+ * A record with no version predates the explicit `essential` category, when essential storage was
17
+ * implicitly always on. Those records are migrated in place rather than discarded — the visitor
18
+ * made a decision, and re-asking would be a regression they experience as the site forgetting.
19
+ */
20
+ export const CONSENT_SCHEMA_VERSION = 2
21
+
22
+ /** Idempotence flag for the Consent Mode defaults. Fixed: a page may carry two bundles. */
23
+ export const CONSENT_SETUP_FLAG = 'cookieConsentSetup'
24
+
25
+ export const CONSENT_ESSENTIAL = 'essential'
26
+ export const CONSENT_ANALYTICS = 'analytics'
27
+ export const CONSENT_MARKETING = 'marketing'
28
+
29
+ export const CONSENT_LOCALES = ['en', 'pl', 'ru', 'be', 'uk', 'es', 'de'] as const
30
+
31
+ export type ConsentLocale = (typeof CONSENT_LOCALES)[number]
32
+
33
+ /**
34
+ * The categories every OwlMeans surface starts with.
35
+ *
36
+ * Three, matching what owlmeans.com already asks — plus the essential row, which the original
37
+ * widget left implicit. Making it explicit is what lets a flow require an acknowledgement before
38
+ * it sets a session cookie, and what tells a visitor what is being stored regardless.
39
+ */
40
+ export const DEFAULT_CONSENT_CATEGORIES: ConsentCategory[] = [
41
+ {
42
+ key: CONSENT_ESSENTIAL, required: true,
43
+ labelKey: 'essential', descriptionKey: 'essentialDesc',
44
+ globalVar: 'owlConsentEssential',
45
+ signals: ['security_storage', 'functionality_storage'],
46
+ },
47
+ {
48
+ key: CONSENT_ANALYTICS,
49
+ labelKey: 'analytics', descriptionKey: 'analyticsDesc',
50
+ globalVar: 'owlConsentAnalytics',
51
+ signals: ['analytics_storage'],
52
+ },
53
+ {
54
+ key: CONSENT_MARKETING,
55
+ labelKey: 'marketing', descriptionKey: 'marketingDesc',
56
+ globalVar: 'owlConsentMarketing',
57
+ signals: ['ad_storage', 'ad_user_data', 'ad_personalization'],
58
+ },
59
+ ]
60
+
61
+ /**
62
+ * Every signal Consent Mode v2 knows, and what it defaults to before anyone has chosen.
63
+ *
64
+ * `security_storage` is granted by Google's own documented recommendation — it covers things like
65
+ * fraud prevention, which are strictly necessary. Everything else starts denied.
66
+ */
67
+ export const CONSENT_SIGNAL_DEFAULTS: Record<string, 'granted' | 'denied'> = {
68
+ ad_storage: 'denied',
69
+ ad_user_data: 'denied',
70
+ ad_personalization: 'denied',
71
+ analytics_storage: 'denied',
72
+ functionality_storage: 'denied',
73
+ personalization_storage: 'denied',
74
+ security_storage: 'granted',
75
+ }
package/src/gtm.ts ADDED
@@ -0,0 +1,141 @@
1
+ import {
2
+ CONSENT_KEY, CONSENT_SETUP_FLAG, CONSENT_SIGNAL_DEFAULTS, DEFAULT_CONSENT_CATEGORIES,
3
+ } from './consts.js'
4
+ import type { ConsentCategory, ConsentOptions, ConsentRecord } from './types.js'
5
+
6
+ interface ConsentWindow {
7
+ dataLayer?: unknown[]
8
+ [flag: string]: unknown
9
+ }
10
+
11
+ const global = (): ConsentWindow | null =>
12
+ typeof window !== 'undefined' ? window as unknown as ConsentWindow : null
13
+
14
+ /**
15
+ * Push onto the tag-manager queue in the shape Google's own snippet uses.
16
+ *
17
+ * `arguments`, not an array literal. Both work with today's Consent API, but `arguments` is what
18
+ * `gtag.js` itself emits, and a page that carries both this and Google's snippet should not have
19
+ * two shapes in one queue.
20
+ */
21
+ export function gtagConsent(..._args: unknown[]): void {
22
+ const win = global()
23
+ if (win == null) {
24
+ return
25
+ }
26
+ win.dataLayer = win.dataLayer ?? []
27
+ // `arguments`, not `_args`. The rest parameter is only here to type the call sites; what goes on
28
+ // the queue must be the arguments object itself.
29
+ // eslint-disable-next-line prefer-rest-params
30
+ win.dataLayer.push(arguments)
31
+ }
32
+
33
+ const categoriesOf = (opts?: ConsentOptions): ConsentCategory[] =>
34
+ opts?.categories ?? DEFAULT_CONSENT_CATEGORIES
35
+
36
+ /** Every signal the configured categories drive, at its pre-consent value. */
37
+ export const consentDefaults = (
38
+ categories?: ConsentCategory[]
39
+ ): Record<string, 'granted' | 'denied'> => {
40
+ const used = new Set((categories ?? DEFAULT_CONSENT_CATEGORIES)
41
+ .flatMap(category => category.signals ?? []))
42
+
43
+ return Object.fromEntries(Object.entries(CONSENT_SIGNAL_DEFAULTS)
44
+ .filter(([signal]) => used.has(signal as never)))
45
+ }
46
+
47
+ /** Every signal, at the value this record implies. */
48
+ export const consentUpdate = (
49
+ record: ConsentRecord, categories?: ConsentCategory[]
50
+ ): Record<string, 'granted' | 'denied'> => {
51
+ const update: Record<string, 'granted' | 'denied'> = {}
52
+ for (const category of categories ?? DEFAULT_CONSENT_CATEGORIES) {
53
+ const granted = category.required === true || record[category.key] === true
54
+ for (const signal of category.signals ?? []) {
55
+ // A signal named by two categories is granted only when every one of them is.
56
+ update[signal] = granted && update[signal] !== 'denied' ? 'granted' : 'denied'
57
+ }
58
+ }
59
+
60
+ return update
61
+ }
62
+
63
+ /**
64
+ * Declare what is denied, before any tag can act.
65
+ *
66
+ * Idempotent through a window flag, because the page may carry this call twice — once inline in
67
+ * the document head, once from whatever bundle mounts the dialog — and a second `default` after a
68
+ * tag has loaded is worse than none: it can widen what was already narrowed.
69
+ */
70
+ export const pushConsentDefaults = (opts?: ConsentOptions): void => {
71
+ const win = global()
72
+ if (win == null || opts?.silent === true || win[CONSENT_SETUP_FLAG] === true) {
73
+ return
74
+ }
75
+ gtagConsent('consent', 'default', consentDefaults(categoriesOf(opts)))
76
+ win[CONSENT_SETUP_FLAG] = true
77
+ }
78
+
79
+ /**
80
+ * Apply a decision: globals first, then the signal update, then any per-category events.
81
+ *
82
+ * The order is load-bearing. A tag that fires on the update event reads the globals in the same
83
+ * turn, so writing them afterwards would let the first firing see the previous answer.
84
+ */
85
+ export const applyConsent = (record: ConsentRecord, opts?: ConsentOptions): void => {
86
+ const win = global()
87
+ if (win == null || opts?.silent === true) {
88
+ return
89
+ }
90
+ const categories = categoriesOf(opts)
91
+
92
+ for (const category of categories) {
93
+ if (category.globalVar != null) {
94
+ win[category.globalVar] = category.required === true || record[category.key] === true
95
+ }
96
+ }
97
+
98
+ gtagConsent('consent', 'update', consentUpdate(record, categories))
99
+
100
+ for (const category of categories) {
101
+ if (category.event != null && (category.required === true || record[category.key] === true)) {
102
+ win.dataLayer = win.dataLayer ?? []
103
+ win.dataLayer.push({ event: category.event })
104
+ }
105
+ }
106
+ }
107
+
108
+ /**
109
+ * The inline script a document stamps ABOVE its tag-manager snippet.
110
+ *
111
+ * It exists because the defaults have to be on the queue before `gtm.js` loads, and a React bundle
112
+ * cannot be: by the time an island mounts, the container has been running for hundreds of
113
+ * milliseconds and has already decided what it may do. Emitting it from the HTML is the only
114
+ * ordering that holds — and it reads the stored record too, so a returning visitor's tags are not
115
+ * denied for the first paint of every page.
116
+ */
117
+ export const consentBootstrapScript = (opts?: ConsentOptions): string => {
118
+ const defaults = JSON.stringify(consentDefaults(categoriesOf(opts)))
119
+ const categories = JSON.stringify(categoriesOf(opts).map(category => ({
120
+ key: category.key,
121
+ required: category.required === true,
122
+ signals: category.signals ?? [],
123
+ globalVar: category.globalVar ?? null,
124
+ })))
125
+ const storageKey = JSON.stringify(opts?.storageKey ?? CONSENT_KEY)
126
+ const flag = JSON.stringify(CONSENT_SETUP_FLAG)
127
+
128
+ return `(function(w,d){` +
129
+ `w.dataLayer=w.dataLayer||[];function g(){w.dataLayer.push(arguments)}` +
130
+ `if(w[${flag}])return;g('consent','default',${defaults});w[${flag}]=true;` +
131
+ `var raw=null;try{raw=w.localStorage.getItem(${storageKey})}catch(e){}` +
132
+ `if(!raw){var p=('; '+d.cookie).split('; '+${storageKey}+'=');` +
133
+ `if(p.length===2){raw=p.pop().split(';').shift()}}` +
134
+ `if(!raw)return;var r;try{r=JSON.parse(raw)}catch(e){return}` +
135
+ `var cs=${categories},u={};` +
136
+ `for(var i=0;i<cs.length;i++){var c=cs[i];var ok=c.required||r[c.key]===true;` +
137
+ `if(c.globalVar){w[c.globalVar]=ok}` +
138
+ `for(var j=0;j<c.signals.length;j++){var s=c.signals[j];` +
139
+ `u[s]=ok&&u[s]!=='denied'?'granted':'denied'}}` +
140
+ `g('consent','update',u)})(window,document)`
141
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "title": "Налады файлаў cookie",
3
+ "description": "Мы выкарыстоўваем файлы cookie, каб палепшыць ваш досвед прагляду, паказваць персаналізаваную рэкламу ці змесціва і аналізаваць наш трафік. Націскаючы «Прыняць усе», вы пагаджаецеся на выкарыстанне файлаў cookie.",
4
+ "essential": "Неабходныя файлы cookie",
5
+ "essentialDesc": "Патрэбныя для працы сайта — уваход, бяспека і захаванне гэтых налад. Іх нельга адключыць.",
6
+ "analytics": "Аналітычныя файлы cookie",
7
+ "analyticsDesc": "Дапамагаюць нам паляпшаць наш вэб-сайт, збіраючы і паведамляючы інфармацыю пра яго выкарыстанне.",
8
+ "marketing": "Маркетынгавыя файлы cookie",
9
+ "marketingDesc": "Выкарыстоўваюцца для адсочвання наведвальнікаў на розных вэб-сайтах дзеля паказу актуальнай і цікавай рэкламы.",
10
+ "required": "Абавязкова",
11
+ "acceptAll": "Прыняць усе",
12
+ "acceptAndContinue": "Прыняць і працягнуць",
13
+ "savePreferences": "Захаваць налады",
14
+ "policyLink": "Палітыка файлаў cookie",
15
+ "openPreferences": "Налады файлаў cookie",
16
+ "close": "Закрыць",
17
+ "loginReason": "Уваход захоўвае сеансавы файл cookie. Прыміце неабходныя файлы cookie, каб працягнуць.",
18
+ "manage": "Змяніць налады",
19
+ "policyTitle": "Палітыка файлаў cookie",
20
+ "policyIntro": "Гэтая старонка апісвае файлы cookie і падобныя сховішчы, якія выкарыстоўвае гэтая праграма, і як імі кіраваць.",
21
+ "policyStorage": "Ваш выбар захоўваецца ў гэтым браўзеры пад ключом «{{key}}» — і ў лакальным сховішчы, і ў файле cookie.",
22
+ "policyRetention": "Запіс захоўваецца {{days}} дзён, пасля чаго мы спытаем зноў.",
23
+ "policyOperator": "Гэтая праграма належыць {{operator}}.",
24
+ "policyContact": "Азнаёмцеся з іх палітыкай прыватнасці і ўмовамі карыстання, каб даведацца, як яны апрацоўваюць вашы даныя."
25
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "title": "Cookie-Einstellungen",
3
+ "description": "Wir verwenden Cookies, um Ihr Surferlebnis zu verbessern, personalisierte Anzeigen oder Inhalte bereitzustellen und unseren Datenverkehr zu analysieren. Indem Sie auf „Alle akzeptieren\" klicken, stimmen Sie der Verwendung von Cookies zu.",
4
+ "essential": "Notwendige Cookies",
5
+ "essentialDesc": "Erforderlich, damit die Website funktioniert — Anmeldung, Sicherheit und das Speichern dieser Einstellungen. Sie lassen sich nicht abschalten.",
6
+ "analytics": "Analyse-Cookies",
7
+ "analyticsDesc": "Helfen uns, unsere Website zu verbessern, indem sie Informationen zur Nutzung sammeln und melden.",
8
+ "marketing": "Marketing-Cookies",
9
+ "marketingDesc": "Werden verwendet, um Besucher über Websites hinweg zu verfolgen und relevante, ansprechende Werbung anzuzeigen.",
10
+ "required": "Erforderlich",
11
+ "acceptAll": "Alle akzeptieren",
12
+ "acceptAndContinue": "Akzeptieren und fortfahren",
13
+ "savePreferences": "Einstellungen speichern",
14
+ "policyLink": "Cookie-Richtlinie",
15
+ "openPreferences": "Cookie-Einstellungen",
16
+ "close": "Schließen",
17
+ "loginReason": "Bei der Anmeldung wird ein Sitzungs-Cookie gespeichert. Akzeptieren Sie die notwendigen Cookies, um fortzufahren.",
18
+ "manage": "Einstellungen verwalten",
19
+ "policyTitle": "Cookie-Richtlinie",
20
+ "policyIntro": "Diese Seite beschreibt die Cookies und ähnliche Speicher, die diese Anwendung verwendet, und wie Sie sie steuern können.",
21
+ "policyStorage": "Ihre Auswahl wird in diesem Browser unter „{{key}}\" gespeichert — sowohl im lokalen Speicher als auch als Cookie.",
22
+ "policyRetention": "Der Eintrag wird {{days}} Tage aufbewahrt, danach werden Sie erneut gefragt.",
23
+ "policyOperator": "Diese Anwendung wird von {{operator}} betrieben.",
24
+ "policyContact": "Sehen Sie in deren Datenschutzerklärung und Bedingungen nach, wie Ihre Daten verarbeitet werden."
25
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "title": "Cookie Preferences",
3
+ "description": "We use cookies to enhance your browsing experience, serve personalized ads or content, and analyze our traffic. By clicking \"Accept All\", you consent to our use of cookies.",
4
+ "essential": "Essential Cookies",
5
+ "essentialDesc": "Required for the site to work — signing in, security and remembering these preferences. They cannot be switched off.",
6
+ "analytics": "Analytics Cookies",
7
+ "analyticsDesc": "Help us improve our website by collecting and reporting information on its usage.",
8
+ "marketing": "Marketing Cookies",
9
+ "marketingDesc": "Used to track visitors across websites to display relevant and engaging advertisements.",
10
+ "required": "Required",
11
+ "acceptAll": "Accept All",
12
+ "acceptAndContinue": "Accept & continue",
13
+ "savePreferences": "Save Preferences",
14
+ "policyLink": "Cookie Policy",
15
+ "openPreferences": "Cookie preferences",
16
+ "close": "Close",
17
+ "loginReason": "Signing in stores a session cookie. Accept essential cookies to continue.",
18
+ "manage": "Manage preferences",
19
+ "policyTitle": "Cookie Policy",
20
+ "policyIntro": "This page describes the cookies and similar storage this application uses, and how you can control them.",
21
+ "policyStorage": "Your choice is stored in this browser under \"{{key}}\", both in local storage and as a cookie.",
22
+ "policyRetention": "The record is kept for {{days}} days, after which you will be asked again.",
23
+ "policyOperator": "This application is operated by {{operator}}.",
24
+ "policyContact": "See their privacy policy and terms for how they handle your data."
25
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "title": "Preferencias de cookies",
3
+ "description": "Utilizamos cookies para mejorar tu experiencia de navegación, ofrecer anuncios o contenido personalizados y analizar nuestro tráfico. Al hacer clic en «Aceptar todo», aceptas nuestro uso de cookies.",
4
+ "essential": "Cookies esenciales",
5
+ "essentialDesc": "Necesarias para que el sitio funcione: inicio de sesión, seguridad y recordar estas preferencias. No se pueden desactivar.",
6
+ "analytics": "Cookies analíticas",
7
+ "analyticsDesc": "Nos ayudan a mejorar nuestro sitio web recopilando e informando sobre su uso.",
8
+ "marketing": "Cookies de marketing",
9
+ "marketingDesc": "Se utilizan para rastrear a los visitantes en distintos sitios web y mostrar anuncios relevantes y atractivos.",
10
+ "required": "Obligatorias",
11
+ "acceptAll": "Aceptar todo",
12
+ "acceptAndContinue": "Aceptar y continuar",
13
+ "savePreferences": "Guardar preferencias",
14
+ "policyLink": "Política de cookies",
15
+ "openPreferences": "Preferencias de cookies",
16
+ "close": "Cerrar",
17
+ "loginReason": "Iniciar sesión guarda una cookie de sesión. Acepta las cookies esenciales para continuar.",
18
+ "manage": "Gestionar preferencias",
19
+ "policyTitle": "Política de cookies",
20
+ "policyIntro": "Esta página describe las cookies y el almacenamiento similar que utiliza esta aplicación, y cómo puedes controlarlos.",
21
+ "policyStorage": "Tu elección se guarda en este navegador bajo «{{key}}», tanto en el almacenamiento local como en una cookie.",
22
+ "policyRetention": "El registro se conserva {{days}} días, tras los cuales se te preguntará de nuevo.",
23
+ "policyOperator": "Esta aplicación está gestionada por {{operator}}.",
24
+ "policyContact": "Consulta su política de privacidad y sus términos para saber cómo tratan tus datos."
25
+ }