@freshjuice/zest 0.1.0 → 1.0.0
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/README.md +46 -0
- package/dist/zest.de.js +104 -1
- package/dist/zest.de.js.map +1 -1
- package/dist/zest.de.min.js +1 -1
- package/dist/zest.en.js +104 -1
- package/dist/zest.en.js.map +1 -1
- package/dist/zest.en.min.js +1 -1
- package/dist/zest.es.js +104 -1
- package/dist/zest.es.js.map +1 -1
- package/dist/zest.es.min.js +1 -1
- package/dist/zest.esm.js +104 -1
- package/dist/zest.esm.js.map +1 -1
- package/dist/zest.esm.min.js +1 -1
- package/dist/zest.fr.js +104 -1
- package/dist/zest.fr.js.map +1 -1
- package/dist/zest.fr.min.js +1 -1
- package/dist/zest.it.js +104 -1
- package/dist/zest.it.js.map +1 -1
- package/dist/zest.it.min.js +1 -1
- package/dist/zest.ja.js +104 -1
- package/dist/zest.ja.js.map +1 -1
- package/dist/zest.ja.min.js +1 -1
- package/dist/zest.js +104 -1
- package/dist/zest.js.map +1 -1
- package/dist/zest.min.js +1 -1
- package/dist/zest.nl.js +104 -1
- package/dist/zest.nl.js.map +1 -1
- package/dist/zest.nl.min.js +1 -1
- package/dist/zest.pl.js +104 -1
- package/dist/zest.pl.js.map +1 -1
- package/dist/zest.pl.min.js +1 -1
- package/dist/zest.pt.js +104 -1
- package/dist/zest.pt.js.map +1 -1
- package/dist/zest.pt.min.js +1 -1
- package/dist/zest.ru.js +104 -1
- package/dist/zest.ru.js.map +1 -1
- package/dist/zest.ru.min.js +1 -1
- package/dist/zest.uk.js +104 -1
- package/dist/zest.uk.js.map +1 -1
- package/dist/zest.uk.min.js +1 -1
- package/dist/zest.zh.js +104 -1
- package/dist/zest.zh.js.map +1 -1
- package/dist/zest.zh.min.js +1 -1
- package/package.json +5 -4
- package/src/api/public-api.js +97 -0
- package/src/config/defaults.js +150 -0
- package/src/config/parser.js +104 -0
- package/src/core/categories.js +52 -0
- package/src/core/cookie-interceptor.js +116 -0
- package/src/core/dnt.js +56 -0
- package/src/core/known-trackers.js +168 -0
- package/src/core/pattern-matcher.js +96 -0
- package/src/core/script-blocker.js +308 -0
- package/src/core/storage-interceptor.js +169 -0
- package/src/i18n/lang-en.js +54 -0
- package/src/i18n/single/lang-de.js +55 -0
- package/src/i18n/single/lang-en.js +55 -0
- package/src/i18n/single/lang-es.js +55 -0
- package/src/i18n/single/lang-fr.js +55 -0
- package/src/i18n/single/lang-it.js +55 -0
- package/src/i18n/single/lang-ja.js +55 -0
- package/src/i18n/single/lang-nl.js +55 -0
- package/src/i18n/single/lang-pl.js +55 -0
- package/src/i18n/single/lang-pt.js +55 -0
- package/src/i18n/single/lang-ru.js +55 -0
- package/src/i18n/single/lang-uk.js +55 -0
- package/src/i18n/single/lang-zh.js +55 -0
- package/src/i18n/translations.js +546 -0
- package/src/index.js +377 -0
- package/src/integrations/consent-signals.js +71 -0
- package/src/storage/consent-store.js +177 -0
- package/src/storage/events.js +84 -0
- package/src/ui/banner.js +130 -0
- package/src/ui/modal.js +211 -0
- package/src/ui/styles.js +498 -0
- package/src/ui/widget.js +103 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zest - Lightweight Cookie Consent Toolkit
|
|
3
|
+
* Main entry point
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Core
|
|
7
|
+
import { interceptCookies, setConsentChecker as setCookieChecker, replayCookies, getOriginalCookieDescriptor } from './core/cookie-interceptor.js';
|
|
8
|
+
import { interceptStorage, setConsentChecker as setStorageChecker, replayStorage } from './core/storage-interceptor.js';
|
|
9
|
+
import { startScriptBlocking, setConsentChecker as setScriptChecker, replayScripts } from './core/script-blocker.js';
|
|
10
|
+
import { setPatterns } from './core/pattern-matcher.js';
|
|
11
|
+
import { getCategoryIds } from './core/categories.js';
|
|
12
|
+
import { isDoNotTrackEnabled, getDNTDetails } from './core/dnt.js';
|
|
13
|
+
|
|
14
|
+
// Integrations
|
|
15
|
+
import { applyConsentSignals } from './integrations/consent-signals.js';
|
|
16
|
+
|
|
17
|
+
// Config
|
|
18
|
+
import { getConfig, setConfig, getCurrentConfig } from './config/parser.js';
|
|
19
|
+
|
|
20
|
+
// Storage
|
|
21
|
+
import {
|
|
22
|
+
loadConsent,
|
|
23
|
+
getConsent,
|
|
24
|
+
hasConsent,
|
|
25
|
+
updateConsent,
|
|
26
|
+
acceptAll as storeAcceptAll,
|
|
27
|
+
rejectAll as storeRejectAll,
|
|
28
|
+
resetConsent,
|
|
29
|
+
hasConsentDecision,
|
|
30
|
+
getConsentProof
|
|
31
|
+
} from './storage/consent-store.js';
|
|
32
|
+
import { emitReady, emitConsent, emitReject, emitChange, emitShow, emitHide, EVENTS } from './storage/events.js';
|
|
33
|
+
|
|
34
|
+
// UI
|
|
35
|
+
import { showBanner, hideBanner, isBannerVisible } from './ui/banner.js';
|
|
36
|
+
import { showModal, hideModal, isModalVisible } from './ui/modal.js';
|
|
37
|
+
import { showWidget, hideWidget, removeWidget, isWidgetVisible } from './ui/widget.js';
|
|
38
|
+
|
|
39
|
+
// State
|
|
40
|
+
let initialized = false;
|
|
41
|
+
let config = null;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Consent checker function shared across interceptors
|
|
45
|
+
*/
|
|
46
|
+
function checkConsent(category) {
|
|
47
|
+
return hasConsent(category);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Replay all queued items for newly allowed categories
|
|
52
|
+
*/
|
|
53
|
+
function replayAll(allowedCategories) {
|
|
54
|
+
replayCookies(allowedCategories);
|
|
55
|
+
replayStorage(allowedCategories);
|
|
56
|
+
replayScripts(allowedCategories);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Handle accept all
|
|
61
|
+
*/
|
|
62
|
+
function handleAcceptAll() {
|
|
63
|
+
const result = storeAcceptAll(config.expiration);
|
|
64
|
+
const categories = getCategoryIds();
|
|
65
|
+
|
|
66
|
+
applyConsentSignals(result.current, config, false);
|
|
67
|
+
|
|
68
|
+
hideBanner();
|
|
69
|
+
hideModal();
|
|
70
|
+
|
|
71
|
+
replayAll(categories);
|
|
72
|
+
|
|
73
|
+
if (config.showWidget) {
|
|
74
|
+
showWidget({ onClick: handleShowSettings });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
emitConsent(result.current, result.previous);
|
|
78
|
+
emitChange(result.current, result.previous);
|
|
79
|
+
config.callbacks?.onAccept?.(result.current);
|
|
80
|
+
config.callbacks?.onChange?.(result.current);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Handle reject all
|
|
85
|
+
*/
|
|
86
|
+
function handleRejectAll() {
|
|
87
|
+
const result = storeRejectAll(config.expiration);
|
|
88
|
+
|
|
89
|
+
applyConsentSignals(result.current, config, false);
|
|
90
|
+
|
|
91
|
+
hideBanner();
|
|
92
|
+
hideModal();
|
|
93
|
+
|
|
94
|
+
if (config.showWidget) {
|
|
95
|
+
showWidget({ onClick: handleShowSettings });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
emitReject(result.current);
|
|
99
|
+
emitChange(result.current, result.previous);
|
|
100
|
+
config.callbacks?.onReject?.();
|
|
101
|
+
config.callbacks?.onChange?.(result.current);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Handle save preferences from modal
|
|
106
|
+
*/
|
|
107
|
+
function handleSavePreferences(selections) {
|
|
108
|
+
const result = updateConsent(selections, config.expiration);
|
|
109
|
+
|
|
110
|
+
applyConsentSignals(result.current, config, false);
|
|
111
|
+
|
|
112
|
+
// Find newly allowed categories
|
|
113
|
+
const newlyAllowed = Object.keys(result.current).filter(
|
|
114
|
+
cat => result.current[cat] && !result.previous[cat]
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
if (newlyAllowed.length > 0) {
|
|
118
|
+
replayAll(newlyAllowed);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
hideModal();
|
|
122
|
+
|
|
123
|
+
if (config.showWidget) {
|
|
124
|
+
showWidget({ onClick: handleShowSettings });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Determine if this was acceptance or rejection based on selections
|
|
128
|
+
const hasNonEssential = Object.entries(selections)
|
|
129
|
+
.some(([cat, val]) => cat !== 'essential' && val);
|
|
130
|
+
|
|
131
|
+
if (hasNonEssential) {
|
|
132
|
+
emitConsent(result.current, result.previous);
|
|
133
|
+
} else {
|
|
134
|
+
emitReject(result.current);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
emitChange(result.current, result.previous);
|
|
138
|
+
config.callbacks?.onChange?.(result.current);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Handle show settings
|
|
143
|
+
*/
|
|
144
|
+
function handleShowSettings() {
|
|
145
|
+
hideBanner();
|
|
146
|
+
hideWidget();
|
|
147
|
+
|
|
148
|
+
showModal(getConsent(), {
|
|
149
|
+
onSave: handleSavePreferences,
|
|
150
|
+
onAcceptAll: handleAcceptAll,
|
|
151
|
+
onRejectAll: handleRejectAll,
|
|
152
|
+
onClose: handleCloseModal
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
emitShow('modal');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Handle close modal
|
|
160
|
+
*/
|
|
161
|
+
function handleCloseModal() {
|
|
162
|
+
hideModal();
|
|
163
|
+
emitHide('modal');
|
|
164
|
+
|
|
165
|
+
// Show widget if consent was already given
|
|
166
|
+
if (hasConsentDecision() && config.showWidget) {
|
|
167
|
+
showWidget({ onClick: handleShowSettings });
|
|
168
|
+
} else {
|
|
169
|
+
// Show banner again if no decision made
|
|
170
|
+
showBanner({
|
|
171
|
+
onAcceptAll: handleAcceptAll,
|
|
172
|
+
onRejectAll: handleRejectAll,
|
|
173
|
+
onSettings: handleShowSettings
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Initialize Zest
|
|
180
|
+
*/
|
|
181
|
+
function init(userConfig = {}) {
|
|
182
|
+
if (initialized) {
|
|
183
|
+
console.warn('[Zest] Already initialized');
|
|
184
|
+
return Zest;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Merge config
|
|
188
|
+
config = setConfig(userConfig);
|
|
189
|
+
|
|
190
|
+
// Push default denied state to vendor consent mode APIs (must happen before scripts load)
|
|
191
|
+
applyConsentSignals(
|
|
192
|
+
{ essential: true, functional: false, analytics: false, marketing: false },
|
|
193
|
+
config,
|
|
194
|
+
true
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
// Set patterns if provided
|
|
198
|
+
if (config.patterns) {
|
|
199
|
+
setPatterns(config.patterns);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Set up consent checkers
|
|
203
|
+
setCookieChecker(checkConsent);
|
|
204
|
+
setStorageChecker(checkConsent);
|
|
205
|
+
setScriptChecker(checkConsent);
|
|
206
|
+
|
|
207
|
+
// Start interception
|
|
208
|
+
interceptCookies();
|
|
209
|
+
interceptStorage();
|
|
210
|
+
startScriptBlocking(config.mode, config.blockedDomains);
|
|
211
|
+
|
|
212
|
+
// Load saved consent
|
|
213
|
+
const consent = loadConsent();
|
|
214
|
+
|
|
215
|
+
initialized = true;
|
|
216
|
+
|
|
217
|
+
// Push update for returning visitors with saved consent
|
|
218
|
+
if (hasConsentDecision()) {
|
|
219
|
+
applyConsentSignals(consent, config, false);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Check Do Not Track / Global Privacy Control
|
|
223
|
+
const dntEnabled = isDoNotTrackEnabled();
|
|
224
|
+
let dntApplied = false;
|
|
225
|
+
|
|
226
|
+
if (dntEnabled && config.respectDNT && config.dntBehavior !== 'ignore') {
|
|
227
|
+
if (config.dntBehavior === 'reject' && !hasConsentDecision()) {
|
|
228
|
+
// Auto-reject non-essential cookies silently
|
|
229
|
+
const result = storeRejectAll(config.expiration);
|
|
230
|
+
dntApplied = true;
|
|
231
|
+
|
|
232
|
+
applyConsentSignals(result.current, config, false);
|
|
233
|
+
|
|
234
|
+
// Emit events
|
|
235
|
+
emitReject(result.current);
|
|
236
|
+
emitChange(result.current, result.previous);
|
|
237
|
+
config.callbacks?.onReject?.();
|
|
238
|
+
config.callbacks?.onChange?.(result.current);
|
|
239
|
+
}
|
|
240
|
+
// 'preselect' behavior is handled by default (banner shows with defaults off)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Emit ready event
|
|
244
|
+
emitReady(consent);
|
|
245
|
+
config.callbacks?.onReady?.(consent);
|
|
246
|
+
|
|
247
|
+
// Show UI based on consent state
|
|
248
|
+
if (!hasConsentDecision() && !dntApplied) {
|
|
249
|
+
// No consent decision yet - show banner
|
|
250
|
+
showBanner({
|
|
251
|
+
onAcceptAll: handleAcceptAll,
|
|
252
|
+
onRejectAll: handleRejectAll,
|
|
253
|
+
onSettings: handleShowSettings
|
|
254
|
+
});
|
|
255
|
+
emitShow('banner');
|
|
256
|
+
} else {
|
|
257
|
+
// Consent already given (or DNT auto-rejected) - show widget for reopening settings
|
|
258
|
+
if (config.showWidget) {
|
|
259
|
+
showWidget({ onClick: handleShowSettings });
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return Zest;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Public API
|
|
268
|
+
*/
|
|
269
|
+
const Zest = {
|
|
270
|
+
// Initialization
|
|
271
|
+
init,
|
|
272
|
+
|
|
273
|
+
// Banner control
|
|
274
|
+
show() {
|
|
275
|
+
if (!initialized) {
|
|
276
|
+
console.warn('[Zest] Not initialized. Call Zest.init() first.');
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
hideModal();
|
|
280
|
+
hideWidget();
|
|
281
|
+
showBanner({
|
|
282
|
+
onAcceptAll: handleAcceptAll,
|
|
283
|
+
onRejectAll: handleRejectAll,
|
|
284
|
+
onSettings: handleShowSettings
|
|
285
|
+
});
|
|
286
|
+
emitShow('banner');
|
|
287
|
+
},
|
|
288
|
+
|
|
289
|
+
hide() {
|
|
290
|
+
hideBanner();
|
|
291
|
+
emitHide('banner');
|
|
292
|
+
},
|
|
293
|
+
|
|
294
|
+
// Settings modal
|
|
295
|
+
showSettings() {
|
|
296
|
+
if (!initialized) {
|
|
297
|
+
console.warn('[Zest] Not initialized. Call Zest.init() first.');
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
handleShowSettings();
|
|
301
|
+
},
|
|
302
|
+
|
|
303
|
+
hideSettings() {
|
|
304
|
+
hideModal();
|
|
305
|
+
emitHide('modal');
|
|
306
|
+
},
|
|
307
|
+
|
|
308
|
+
// Consent management
|
|
309
|
+
getConsent,
|
|
310
|
+
hasConsent,
|
|
311
|
+
hasConsentDecision,
|
|
312
|
+
getConsentProof,
|
|
313
|
+
|
|
314
|
+
// DNT detection
|
|
315
|
+
isDoNotTrackEnabled,
|
|
316
|
+
getDNTDetails,
|
|
317
|
+
|
|
318
|
+
// Accept/Reject programmatically
|
|
319
|
+
acceptAll() {
|
|
320
|
+
if (!initialized) {
|
|
321
|
+
console.warn('[Zest] Not initialized. Call Zest.init() first.');
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
handleAcceptAll();
|
|
325
|
+
},
|
|
326
|
+
|
|
327
|
+
rejectAll() {
|
|
328
|
+
if (!initialized) {
|
|
329
|
+
console.warn('[Zest] Not initialized. Call Zest.init() first.');
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
handleRejectAll();
|
|
333
|
+
},
|
|
334
|
+
|
|
335
|
+
// Reset and show banner again
|
|
336
|
+
reset() {
|
|
337
|
+
resetConsent();
|
|
338
|
+
hideModal();
|
|
339
|
+
removeWidget();
|
|
340
|
+
|
|
341
|
+
if (initialized) {
|
|
342
|
+
showBanner({
|
|
343
|
+
onAcceptAll: handleAcceptAll,
|
|
344
|
+
onRejectAll: handleRejectAll,
|
|
345
|
+
onSettings: handleShowSettings
|
|
346
|
+
});
|
|
347
|
+
emitShow('banner');
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
|
|
351
|
+
// Config
|
|
352
|
+
getConfig: getCurrentConfig,
|
|
353
|
+
|
|
354
|
+
// Events
|
|
355
|
+
EVENTS
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
// Auto-init if config present
|
|
359
|
+
if (typeof window !== 'undefined') {
|
|
360
|
+
// Make Zest available globally
|
|
361
|
+
window.Zest = Zest;
|
|
362
|
+
|
|
363
|
+
const autoInit = () => {
|
|
364
|
+
const cfg = getConfig();
|
|
365
|
+
if (cfg.autoInit !== false) {
|
|
366
|
+
init(window.ZestConfig);
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
if (document.readyState === 'loading') {
|
|
371
|
+
document.addEventListener('DOMContentLoaded', autoInit);
|
|
372
|
+
} else {
|
|
373
|
+
autoInit();
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export default Zest;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consent Signals - Optional vendor consent mode integrations
|
|
3
|
+
*
|
|
4
|
+
* Pushes consent state to Google Consent Mode v2 and/or Microsoft UET
|
|
5
|
+
* Consent Mode when enabled via config.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Map Zest consent state to Google Consent Mode v2 signals
|
|
10
|
+
*/
|
|
11
|
+
function toGoogleSignals(consent) {
|
|
12
|
+
const g = (val) => val ? 'granted' : 'denied';
|
|
13
|
+
return {
|
|
14
|
+
ad_storage: g(consent.marketing),
|
|
15
|
+
ad_user_data: g(consent.marketing),
|
|
16
|
+
ad_personalization: g(consent.marketing),
|
|
17
|
+
analytics_storage: g(consent.analytics),
|
|
18
|
+
functionality_storage: 'granted', // essential is always true
|
|
19
|
+
personalization_storage: g(consent.functional)
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Push consent signal to Google via gtag or dataLayer fallback.
|
|
25
|
+
* Uses a local function to preserve the `arguments` object shape
|
|
26
|
+
* that gtag/dataLayer expects (not an array).
|
|
27
|
+
*/
|
|
28
|
+
function pushGoogle(type, signals) {
|
|
29
|
+
window.dataLayer = window.dataLayer || [];
|
|
30
|
+
if (typeof window.gtag === 'function') {
|
|
31
|
+
window.gtag('consent', type, signals);
|
|
32
|
+
} else {
|
|
33
|
+
function gtagFallback() { window.dataLayer.push(arguments); }
|
|
34
|
+
gtagFallback('consent', type, signals);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Map Zest consent state to Microsoft UET signal.
|
|
40
|
+
* Microsoft UET only exposes ad_storage.
|
|
41
|
+
*/
|
|
42
|
+
function toMicrosoftSignals(consent) {
|
|
43
|
+
return { ad_storage: consent.marketing ? 'granted' : 'denied' };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Push consent signal to Microsoft UET
|
|
48
|
+
*/
|
|
49
|
+
function pushMicrosoft(type, signals) {
|
|
50
|
+
window.uetq = window.uetq || [];
|
|
51
|
+
window.uetq.push('consent', type, signals);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Apply consent signals to enabled vendor integrations.
|
|
56
|
+
*
|
|
57
|
+
* @param {Object} consent Current Zest consent state
|
|
58
|
+
* @param {Object} config Merged Zest config
|
|
59
|
+
* @param {boolean} isDefault true on first call (pushes 'default'), false for updates
|
|
60
|
+
*/
|
|
61
|
+
export function applyConsentSignals(consent, config, isDefault) {
|
|
62
|
+
const type = isDefault ? 'default' : 'update';
|
|
63
|
+
|
|
64
|
+
if (config.consentModeGoogle) {
|
|
65
|
+
pushGoogle(type, toGoogleSignals(consent));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (config.consentModeMicrosoft) {
|
|
69
|
+
pushMicrosoft(type, toMicrosoftSignals(consent));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consent Store - Manages consent state persistence
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { getDefaultConsent } from '../core/categories.js';
|
|
6
|
+
import { getOriginalCookieDescriptor } from '../core/cookie-interceptor.js';
|
|
7
|
+
|
|
8
|
+
const COOKIE_NAME = 'zest_consent';
|
|
9
|
+
const CONSENT_VERSION = '1.0';
|
|
10
|
+
|
|
11
|
+
// Current consent state
|
|
12
|
+
let consent = null;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get the original cookie setter (bypasses interception)
|
|
16
|
+
*/
|
|
17
|
+
function setRawCookie(value) {
|
|
18
|
+
const descriptor = getOriginalCookieDescriptor();
|
|
19
|
+
if (descriptor?.set) {
|
|
20
|
+
descriptor.set.call(document, value);
|
|
21
|
+
} else {
|
|
22
|
+
// Fallback if interceptor not initialized yet
|
|
23
|
+
document.cookie = value;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get the original cookie getter
|
|
29
|
+
*/
|
|
30
|
+
function getRawCookie() {
|
|
31
|
+
const descriptor = getOriginalCookieDescriptor();
|
|
32
|
+
if (descriptor?.get) {
|
|
33
|
+
return descriptor.get.call(document);
|
|
34
|
+
}
|
|
35
|
+
return document.cookie;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Load consent from cookie
|
|
40
|
+
*/
|
|
41
|
+
export function loadConsent() {
|
|
42
|
+
try {
|
|
43
|
+
const cookies = getRawCookie();
|
|
44
|
+
const match = cookies.match(new RegExp(`${COOKIE_NAME}=([^;]+)`));
|
|
45
|
+
|
|
46
|
+
if (match) {
|
|
47
|
+
const data = JSON.parse(decodeURIComponent(match[1]));
|
|
48
|
+
consent = data.categories || getDefaultConsent();
|
|
49
|
+
return { ...consent };
|
|
50
|
+
}
|
|
51
|
+
} catch (e) {
|
|
52
|
+
// Invalid or missing cookie
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
consent = getDefaultConsent();
|
|
56
|
+
return { ...consent };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Save consent to cookie
|
|
61
|
+
*/
|
|
62
|
+
export function saveConsent(expirationDays = 365) {
|
|
63
|
+
if (!consent) {
|
|
64
|
+
consent = getDefaultConsent();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const data = {
|
|
68
|
+
version: CONSENT_VERSION,
|
|
69
|
+
timestamp: Date.now(),
|
|
70
|
+
categories: consent
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const expires = new Date(Date.now() + expirationDays * 24 * 60 * 60 * 1000).toUTCString();
|
|
74
|
+
const cookieValue = `${COOKIE_NAME}=${encodeURIComponent(JSON.stringify(data))}; expires=${expires}; path=/; SameSite=Lax`;
|
|
75
|
+
|
|
76
|
+
setRawCookie(cookieValue);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get current consent state
|
|
81
|
+
*/
|
|
82
|
+
export function getConsent() {
|
|
83
|
+
if (!consent) {
|
|
84
|
+
consent = loadConsent();
|
|
85
|
+
}
|
|
86
|
+
return { ...consent };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Update consent state
|
|
91
|
+
*/
|
|
92
|
+
export function updateConsent(newConsent, expirationDays = 365) {
|
|
93
|
+
const previous = consent ? { ...consent } : getDefaultConsent();
|
|
94
|
+
|
|
95
|
+
consent = {
|
|
96
|
+
essential: true, // Always true
|
|
97
|
+
functional: !!newConsent.functional,
|
|
98
|
+
analytics: !!newConsent.analytics,
|
|
99
|
+
marketing: !!newConsent.marketing
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
saveConsent(expirationDays);
|
|
103
|
+
|
|
104
|
+
return { current: { ...consent }, previous };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Check if specific category is allowed
|
|
109
|
+
*/
|
|
110
|
+
export function hasConsent(category) {
|
|
111
|
+
if (!consent) {
|
|
112
|
+
consent = loadConsent();
|
|
113
|
+
}
|
|
114
|
+
return consent[category] === true;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Accept all categories
|
|
119
|
+
*/
|
|
120
|
+
export function acceptAll(expirationDays = 365) {
|
|
121
|
+
return updateConsent({
|
|
122
|
+
essential: true,
|
|
123
|
+
functional: true,
|
|
124
|
+
analytics: true,
|
|
125
|
+
marketing: true
|
|
126
|
+
}, expirationDays);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Reject all (except essential)
|
|
131
|
+
*/
|
|
132
|
+
export function rejectAll(expirationDays = 365) {
|
|
133
|
+
return updateConsent({
|
|
134
|
+
essential: true,
|
|
135
|
+
functional: false,
|
|
136
|
+
analytics: false,
|
|
137
|
+
marketing: false
|
|
138
|
+
}, expirationDays);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Reset consent (clear cookie)
|
|
143
|
+
*/
|
|
144
|
+
export function resetConsent() {
|
|
145
|
+
setRawCookie(`${COOKIE_NAME}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`);
|
|
146
|
+
consent = null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Check if consent has been given (any decision made)
|
|
151
|
+
*/
|
|
152
|
+
export function hasConsentDecision() {
|
|
153
|
+
try {
|
|
154
|
+
const cookies = getRawCookie();
|
|
155
|
+
return cookies.includes(COOKIE_NAME);
|
|
156
|
+
} catch (e) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Get consent proof for compliance
|
|
163
|
+
*/
|
|
164
|
+
export function getConsentProof() {
|
|
165
|
+
try {
|
|
166
|
+
const cookies = getRawCookie();
|
|
167
|
+
const match = cookies.match(new RegExp(`${COOKIE_NAME}=([^;]+)`));
|
|
168
|
+
|
|
169
|
+
if (match) {
|
|
170
|
+
return JSON.parse(decodeURIComponent(match[1]));
|
|
171
|
+
}
|
|
172
|
+
} catch (e) {
|
|
173
|
+
// Invalid cookie
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Events - Custom event dispatching for consent changes
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Event names
|
|
6
|
+
export const EVENTS = {
|
|
7
|
+
READY: 'zest:ready',
|
|
8
|
+
CONSENT: 'zest:consent',
|
|
9
|
+
REJECT: 'zest:reject',
|
|
10
|
+
CHANGE: 'zest:change',
|
|
11
|
+
SHOW: 'zest:show',
|
|
12
|
+
HIDE: 'zest:hide'
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Dispatch a custom event
|
|
17
|
+
*/
|
|
18
|
+
export function emit(eventName, detail = {}) {
|
|
19
|
+
const event = new CustomEvent(eventName, {
|
|
20
|
+
detail,
|
|
21
|
+
bubbles: true,
|
|
22
|
+
cancelable: true
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
document.dispatchEvent(event);
|
|
26
|
+
return event;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Emit ready event
|
|
31
|
+
*/
|
|
32
|
+
export function emitReady(consent) {
|
|
33
|
+
return emit(EVENTS.READY, { consent });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Emit consent event (user accepted)
|
|
38
|
+
*/
|
|
39
|
+
export function emitConsent(consent, previous) {
|
|
40
|
+
return emit(EVENTS.CONSENT, { consent, previous });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Emit reject event (user rejected all)
|
|
45
|
+
*/
|
|
46
|
+
export function emitReject(consent) {
|
|
47
|
+
return emit(EVENTS.REJECT, { consent });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Emit change event (any consent change)
|
|
52
|
+
*/
|
|
53
|
+
export function emitChange(consent, previous) {
|
|
54
|
+
return emit(EVENTS.CHANGE, { consent, previous });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Emit show event (banner/modal shown)
|
|
59
|
+
*/
|
|
60
|
+
export function emitShow(type = 'banner') {
|
|
61
|
+
return emit(EVENTS.SHOW, { type });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Emit hide event (banner/modal hidden)
|
|
66
|
+
*/
|
|
67
|
+
export function emitHide(type = 'banner') {
|
|
68
|
+
return emit(EVENTS.HIDE, { type });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Subscribe to an event
|
|
73
|
+
*/
|
|
74
|
+
export function on(eventName, callback) {
|
|
75
|
+
document.addEventListener(eventName, callback);
|
|
76
|
+
return () => document.removeEventListener(eventName, callback);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Subscribe to an event once
|
|
81
|
+
*/
|
|
82
|
+
export function once(eventName, callback) {
|
|
83
|
+
document.addEventListener(eventName, callback, { once: true });
|
|
84
|
+
}
|