@structured-world/vue-privacy 1.1.2 → 1.2.1

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.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -1,4 +1,4 @@
1
- import type { ConsentConfig, StoredConsent, ConsentCategories } from "./types";
1
+ import type { ConsentConfig, StoredConsent, ConsentCategories, GeoDetectionResult } from "./types";
2
2
  /**
3
3
  * Consent Manager - orchestrates consent flow
4
4
  */
@@ -6,11 +6,17 @@ export declare class ConsentManager {
6
6
  private config;
7
7
  private initialized;
8
8
  private isEU;
9
+ private geoResult;
10
+ private userId;
11
+ private remoteStorage;
9
12
  private showBannerCallback;
10
13
  private hideBannerCallback;
14
+ private bannerPending;
11
15
  constructor(config?: ConsentConfig);
12
16
  /**
13
- * Register callback to show banner
17
+ * Register callback to show banner.
18
+ * If init() already requested the banner before this callback was registered,
19
+ * fires immediately (handles race condition with component mount timing).
14
20
  */
15
21
  onShowBanner(callback: () => void): void;
16
22
  /**
@@ -21,6 +27,12 @@ export declare class ConsentManager {
21
27
  * Initialize consent manager
22
28
  */
23
29
  init(): Promise<void>;
30
+ /**
31
+ * Persist consent locally and (if remote storage is configured) remotely.
32
+ * Sets cookies only when at least one non-necessary category is accepted.
33
+ * Fire-and-forget: remote push does not block UI.
34
+ */
35
+ private saveConsentWithRemote;
24
36
  /**
25
37
  * Apply consent settings
26
38
  */
@@ -62,6 +74,11 @@ export declare class ConsentManager {
62
74
  * Check if user is detected as EU
63
75
  */
64
76
  isEUUser(): boolean | null;
77
+ /**
78
+ * Get geo-detection result (country, method, isEU).
79
+ * Returns null if geo detection has not run yet (e.g., consent was restored from cookie).
80
+ */
81
+ getGeoResult(): GeoDetectionResult | null;
65
82
  /**
66
83
  * Get configuration
67
84
  */
@@ -1,4 +1,4 @@
1
- import type { StoredConsent, ConsentConfig } from "./types";
1
+ import type { StoredConsent, ConsentConfig, ConsentStorage } from "./types";
2
2
  /**
3
3
  * Get cookie value by name
4
4
  */
@@ -16,7 +16,7 @@ export declare function setCookie(name: string, value: string, options?: {
16
16
  /**
17
17
  * Delete cookie
18
18
  */
19
- export declare function deleteCookie(name: string, path?: string): void;
19
+ export declare function deleteCookie(name: string, path?: string, domain?: string): void;
20
20
  /**
21
21
  * Get stored consent from cookie
22
22
  */
@@ -29,3 +29,38 @@ export declare function storeConsent(consent: Omit<StoredConsent, "timestamp" |
29
29
  * Clear stored consent
30
30
  */
31
31
  export declare function clearConsent(config?: Partial<ConsentConfig>): void;
32
+ /**
33
+ * Get consent UID from cookie (used for KV re-identification)
34
+ */
35
+ export declare function getConsentUid(): string | null;
36
+ /**
37
+ * Set consent UID cookie for re-identification
38
+ */
39
+ export declare function setConsentUid(uid: string, config?: Partial<ConsentConfig>): void;
40
+ /**
41
+ * Clear consent UID cookie
42
+ */
43
+ export declare function clearConsentUid(config?: Partial<ConsentConfig>): void;
44
+ /**
45
+ * Fetch consent from remote KV storage
46
+ */
47
+ export declare function fetchRemoteConsent(storageUrl: string, uid: string, version: string): Promise<StoredConsent | null>;
48
+ /**
49
+ * Push consent to remote KV storage. Returns the user ID (generated by worker if not provided).
50
+ */
51
+ export declare function pushRemoteConsent(storageUrl: string, uid: string | null, consent: StoredConsent): Promise<string | null>;
52
+ /**
53
+ * Create a ConsentStorage backed by a Cloudflare KV Worker
54
+ * (vue-privacy-worker compatible API).
55
+ *
56
+ * @example
57
+ * ```ts
58
+ * import { createKVStorage } from '@structured-world/vue-privacy';
59
+ *
60
+ * const manager = createConsentManager({
61
+ * gaId: 'G-XXXXXXXXXX',
62
+ * storage: createKVStorage('/api/consent'),
63
+ * });
64
+ * ```
65
+ */
66
+ export declare function createKVStorage(url: string): ConsentStorage;
@@ -36,6 +36,17 @@ export interface StoredConsent {
36
36
  /** Version of the consent configuration */
37
37
  version: string;
38
38
  }
39
+ /**
40
+ * Remote consent storage interface.
41
+ * Implement this to use a custom backend (REST, gRPC, IndexedDB, etc.)
42
+ * instead of the default Cloudflare KV Worker.
43
+ */
44
+ export interface ConsentStorage {
45
+ /** Fetch stored consent by user ID. Return null if not found or version mismatch. */
46
+ get(uid: string, version: string): Promise<StoredConsent | null>;
47
+ /** Save consent. Return user ID (may generate a new one if uid is null). */
48
+ set(uid: string | null, consent: StoredConsent): Promise<string | null>;
49
+ }
39
50
  /**
40
51
  * Geo-detection result
41
52
  */
@@ -111,6 +122,15 @@ export interface ConsentConfig {
111
122
  * @default true
112
123
  */
113
124
  sendPageView?: boolean;
125
+ /**
126
+ * Remote consent storage implementation.
127
+ * When set, consent is persisted remotely and cookie is used
128
+ * only for re-identification. No cookies are set before consent.
129
+ *
130
+ * Use `createKVStorage('/api/consent')` for Cloudflare KV Worker,
131
+ * or implement ConsentStorage interface for custom backends.
132
+ */
133
+ storage?: ConsentStorage;
114
134
  /** Consent version (changing this resets consent for all users) */
115
135
  version?: string;
116
136
  /** Callback when consent changes */
package/dist/index.d.ts CHANGED
@@ -16,8 +16,8 @@
16
16
  * ```
17
17
  */
18
18
  export { ConsentManager, createConsentManager } from "./core/consent-manager";
19
- export { getStoredConsent, storeConsent, clearConsent } from "./core/storage";
19
+ export { getStoredConsent, storeConsent, clearConsent, getConsentUid, setConsentUid, clearConsentUid, fetchRemoteConsent, pushRemoteConsent, createKVStorage, } from "./core/storage";
20
20
  export { initGtag, setConsentDefaults, updateConsent, loadGtagScript, initGoogleAnalytics, categoriesToGoogleSignals, trackPageView, } from "./core/gtag";
21
21
  export { DEFAULT_CONFIG } from "./core/types";
22
22
  export { CloudflareGeoDetector, IPAPIGeoDetector, TimezoneGeoDetector, AutoGeoDetector, createGeoDetector, } from "./geo/index";
23
- export type { ConsentConfig, ConsentCategories, StoredConsent, GoogleConsentSignals, GeoDetector, GeoDetectionResult, BannerConfig, } from "./core/types";
23
+ export type { ConsentConfig, ConsentCategories, StoredConsent, ConsentStorage, GoogleConsentSignals, GeoDetector, GeoDetectionResult, BannerConfig, } from "./core/types";
package/dist/index.js CHANGED
@@ -54,9 +54,11 @@ function setCookie(name, value, options = {}) {
54
54
  }
55
55
  document.cookie = cookieString;
56
56
  }
57
- function deleteCookie(name, path = "/") {
57
+ function deleteCookie(name, path = "/", domain) {
58
58
  if (typeof document === "undefined") return;
59
- document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=${path}`;
59
+ let cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=${path}`;
60
+ if (domain) cookie += `; domain=${domain}`;
61
+ document.cookie = cookie;
60
62
  }
61
63
  function getStoredConsent(config = {}) {
62
64
  var _a;
@@ -92,10 +94,67 @@ function storeConsent(consent, config = {}) {
92
94
  });
93
95
  }
94
96
  function clearConsent(config = {}) {
95
- var _a, _b;
97
+ var _a, _b, _c;
96
98
  const cookieName = ((_a = config.cookie) == null ? void 0 : _a.name) ?? DEFAULT_CONFIG.cookie.name;
97
99
  const path = ((_b = config.cookie) == null ? void 0 : _b.path) ?? DEFAULT_CONFIG.cookie.path;
98
- deleteCookie(cookieName, path);
100
+ deleteCookie(cookieName, path, (_c = config.cookie) == null ? void 0 : _c.domain);
101
+ }
102
+ function getConsentUid() {
103
+ return getCookie("consent_uid");
104
+ }
105
+ function setConsentUid(uid, config = {}) {
106
+ var _a, _b, _c;
107
+ setCookie("consent_uid", uid, {
108
+ expiry: ((_a = config.cookie) == null ? void 0 : _a.expiry) ?? DEFAULT_CONFIG.cookie.expiry,
109
+ domain: (_b = config.cookie) == null ? void 0 : _b.domain,
110
+ path: ((_c = config.cookie) == null ? void 0 : _c.path) ?? DEFAULT_CONFIG.cookie.path
111
+ });
112
+ }
113
+ function clearConsentUid(config = {}) {
114
+ var _a, _b;
115
+ const path = ((_a = config.cookie) == null ? void 0 : _a.path) ?? DEFAULT_CONFIG.cookie.path;
116
+ deleteCookie("consent_uid", path, (_b = config.cookie) == null ? void 0 : _b.domain);
117
+ }
118
+ async function fetchRemoteConsent(storageUrl, uid, version) {
119
+ try {
120
+ const res = await fetch(`${storageUrl}?id=${encodeURIComponent(uid)}`);
121
+ if (!res.ok) return null;
122
+ const data = await res.json();
123
+ if (!data.found || !data.consent) return null;
124
+ if (data.consent.version !== version) return null;
125
+ return {
126
+ categories: data.consent.categories,
127
+ timestamp: data.consent.timestamp ?? Date.now(),
128
+ version
129
+ };
130
+ } catch {
131
+ return null;
132
+ }
133
+ }
134
+ async function pushRemoteConsent(storageUrl, uid, consent) {
135
+ try {
136
+ const body = {
137
+ categories: consent.categories,
138
+ version: consent.version
139
+ };
140
+ if (uid) body.id = uid;
141
+ const res = await fetch(storageUrl, {
142
+ method: "POST",
143
+ headers: { "Content-Type": "application/json" },
144
+ body: JSON.stringify(body)
145
+ });
146
+ if (!res.ok) return null;
147
+ const data = await res.json();
148
+ return data.id ?? null;
149
+ } catch {
150
+ return null;
151
+ }
152
+ }
153
+ function createKVStorage(url) {
154
+ return {
155
+ get: (uid, version) => fetchRemoteConsent(url, uid, version),
156
+ set: (uid, consent) => pushRemoteConsent(url, uid, consent)
157
+ };
99
158
  }
100
159
  function initGtag() {
101
160
  if (typeof window === "undefined") return;
@@ -331,20 +390,33 @@ class ConsentManager {
331
390
  __publicField(this, "config");
332
391
  __publicField(this, "initialized", false);
333
392
  __publicField(this, "isEU", null);
393
+ __publicField(this, "geoResult", null);
394
+ __publicField(this, "userId", null);
395
+ __publicField(this, "remoteStorage", null);
334
396
  __publicField(this, "showBannerCallback", null);
335
397
  __publicField(this, "hideBannerCallback", null);
398
+ __publicField(this, "bannerPending", false);
336
399
  this.config = {
337
400
  ...config,
338
401
  categories: { ...DEFAULT_CONFIG.categories, ...config.categories },
339
402
  banner: { ...DEFAULT_CONFIG.banner, ...config.banner },
340
403
  cookie: { ...DEFAULT_CONFIG.cookie, ...config.cookie }
341
404
  };
405
+ if (config.storage) {
406
+ this.remoteStorage = config.storage;
407
+ }
342
408
  }
343
409
  /**
344
- * Register callback to show banner
410
+ * Register callback to show banner.
411
+ * If init() already requested the banner before this callback was registered,
412
+ * fires immediately (handles race condition with component mount timing).
345
413
  */
346
414
  onShowBanner(callback) {
347
415
  this.showBannerCallback = callback;
416
+ if (this.bannerPending) {
417
+ this.bannerPending = false;
418
+ callback();
419
+ }
348
420
  }
349
421
  /**
350
422
  * Register callback to hide banner
@@ -356,7 +428,7 @@ class ConsentManager {
356
428
  * Initialize consent manager
357
429
  */
358
430
  async init() {
359
- var _a, _b, _c, _d;
431
+ var _a, _b, _c;
360
432
  if (this.initialized) return;
361
433
  this.initialized = true;
362
434
  const stored = getStoredConsent(this.config);
@@ -364,24 +436,67 @@ class ConsentManager {
364
436
  await this.applyConsent(stored.categories);
365
437
  return;
366
438
  }
439
+ if (this.remoteStorage) {
440
+ const uid = getConsentUid();
441
+ if (uid) {
442
+ this.userId = uid;
443
+ const version = this.config.version ?? DEFAULT_CONFIG.version;
444
+ try {
445
+ const remote = await this.remoteStorage.get(uid, version);
446
+ if (remote) {
447
+ storeConsent({ categories: remote.categories }, this.config);
448
+ await this.applyConsent(remote.categories);
449
+ return;
450
+ }
451
+ } catch {
452
+ }
453
+ }
454
+ }
367
455
  const detector = this.config.geoDetector ?? createGeoDetector(this.config.euDetection ?? "auto");
368
456
  const geoResult = await detector.detect();
369
457
  this.isEU = geoResult.isEU;
458
+ this.geoResult = geoResult;
370
459
  if (this.isEU) {
371
460
  if (this.config.gaId) {
372
461
  const sendPageView = this.config.sendPageView ?? true;
373
462
  await initGoogleAnalytics(this.config.gaId, true, sendPageView);
374
463
  }
375
- (_a = this.showBannerCallback) == null ? void 0 : _a.call(this);
376
- (_c = (_b = this.config).onBannerShow) == null ? void 0 : _c.call(_b);
464
+ if (this.showBannerCallback) {
465
+ this.showBannerCallback();
466
+ } else {
467
+ this.bannerPending = true;
468
+ }
469
+ (_b = (_a = this.config).onBannerShow) == null ? void 0 : _b.call(_a);
377
470
  } else {
378
471
  const grantedCategories = {
379
472
  analytics: true,
380
- marketing: ((_d = this.config.categories) == null ? void 0 : _d.marketing) ?? false,
473
+ marketing: ((_c = this.config.categories) == null ? void 0 : _c.marketing) ?? false,
381
474
  functional: true
382
475
  };
383
476
  await this.applyConsent(grantedCategories);
384
- storeConsent({ categories: grantedCategories }, this.config);
477
+ this.saveConsentWithRemote(grantedCategories);
478
+ }
479
+ }
480
+ /**
481
+ * Persist consent locally and (if remote storage is configured) remotely.
482
+ * Sets cookies only when at least one non-necessary category is accepted.
483
+ * Fire-and-forget: remote push does not block UI.
484
+ */
485
+ saveConsentWithRemote(categories) {
486
+ const hasNonNecessary = categories.analytics || categories.marketing;
487
+ if (hasNonNecessary) {
488
+ storeConsent({ categories }, this.config);
489
+ }
490
+ if (this.remoteStorage) {
491
+ const version = this.config.version ?? DEFAULT_CONFIG.version;
492
+ const consent = { categories, timestamp: Date.now(), version };
493
+ this.remoteStorage.set(this.userId, consent).then((id) => {
494
+ if (id && hasNonNecessary) {
495
+ this.userId = id;
496
+ setConsentUid(id, this.config);
497
+ }
498
+ }).catch(() => {
499
+ });
385
500
  }
386
501
  }
387
502
  /**
@@ -412,7 +527,7 @@ class ConsentManager {
412
527
  functional: true
413
528
  };
414
529
  await this.applyConsent(categories);
415
- storeConsent({ categories }, this.config);
530
+ this.saveConsentWithRemote(categories);
416
531
  (_a = this.hideBannerCallback) == null ? void 0 : _a.call(this);
417
532
  (_c = (_b = this.config).onBannerHide) == null ? void 0 : _c.call(_b);
418
533
  }
@@ -425,10 +540,9 @@ class ConsentManager {
425
540
  analytics: false,
426
541
  marketing: false,
427
542
  functional: true
428
- // Functional is always allowed
429
543
  };
430
544
  await this.applyConsent(categories);
431
- storeConsent({ categories }, this.config);
545
+ this.saveConsentWithRemote(categories);
432
546
  (_a = this.hideBannerCallback) == null ? void 0 : _a.call(this);
433
547
  (_c = (_b = this.config).onBannerHide) == null ? void 0 : _c.call(_b);
434
548
  }
@@ -443,7 +557,7 @@ class ConsentManager {
443
557
  functional: categories.functional ?? true
444
558
  };
445
559
  await this.applyConsent(finalCategories);
446
- storeConsent({ categories: finalCategories }, this.config);
560
+ this.saveConsentWithRemote(finalCategories);
447
561
  (_a = this.hideBannerCallback) == null ? void 0 : _a.call(this);
448
562
  (_c = (_b = this.config).onBannerHide) == null ? void 0 : _c.call(_b);
449
563
  }
@@ -465,6 +579,8 @@ class ConsentManager {
465
579
  resetConsent() {
466
580
  var _a, _b, _c;
467
581
  clearConsent(this.config);
582
+ clearConsentUid(this.config);
583
+ this.userId = null;
468
584
  (_a = this.showBannerCallback) == null ? void 0 : _a.call(this);
469
585
  (_c = (_b = this.config).onBannerShow) == null ? void 0 : _c.call(_b);
470
586
  }
@@ -491,6 +607,13 @@ class ConsentManager {
491
607
  isEUUser() {
492
608
  return this.isEU;
493
609
  }
610
+ /**
611
+ * Get geo-detection result (country, method, isEU).
612
+ * Returns null if geo detection has not run yet (e.g., consent was restored from cookie).
613
+ */
614
+ getGeoResult() {
615
+ return this.geoResult;
616
+ }
494
617
  /**
495
618
  * Get configuration
496
619
  */
@@ -510,13 +633,19 @@ export {
510
633
  TimezoneGeoDetector,
511
634
  categoriesToGoogleSignals,
512
635
  clearConsent,
636
+ clearConsentUid,
513
637
  createConsentManager,
514
638
  createGeoDetector,
639
+ createKVStorage,
640
+ fetchRemoteConsent,
641
+ getConsentUid,
515
642
  getStoredConsent,
516
643
  initGoogleAnalytics,
517
644
  initGtag,
518
645
  loadGtagScript,
646
+ pushRemoteConsent,
519
647
  setConsentDefaults,
648
+ setConsentUid,
520
649
  storeConsent,
521
650
  trackPageView,
522
651
  updateConsent
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../src/core/types.ts","../src/core/storage.ts","../src/core/gtag.ts","../src/geo/index.ts","../src/core/consent-manager.ts"],"sourcesContent":["/**\n * Consent categories that can be managed\n */\nexport interface ConsentCategories {\n /** Analytics cookies (e.g., Google Analytics) */\n analytics: boolean;\n /** Marketing/advertising cookies */\n marketing: boolean;\n /** Functional cookies (preferences, etc.) */\n functional: boolean;\n /** Strictly necessary cookies (always true, cannot be disabled) */\n necessary: true;\n}\n\n/**\n * Google Consent Mode v2 signals\n * @see https://developers.google.com/tag-platform/security/guides/consent\n */\nexport interface GoogleConsentSignals {\n /** Controls Google Analytics cookies */\n analytics_storage: \"granted\" | \"denied\";\n /** Controls advertising cookies */\n ad_storage: \"granted\" | \"denied\";\n /** Controls whether user data can be sent to Google for ads */\n ad_user_data: \"granted\" | \"denied\";\n /** Controls personalized advertising */\n ad_personalization: \"granted\" | \"denied\";\n}\n\n/**\n * Stored consent state\n */\nexport interface StoredConsent {\n /** Consent categories */\n categories: Omit<ConsentCategories, \"necessary\">;\n /** Timestamp when consent was given */\n timestamp: number;\n /** Version of the consent configuration */\n version: string;\n}\n\n/**\n * Geo-detection result\n */\nexport interface GeoDetectionResult {\n /** Whether the user is in the EU */\n isEU: boolean;\n /** Country code (ISO 3166-1 alpha-2) */\n countryCode?: string;\n /** Detection method used */\n method: \"cloudflare\" | \"api\" | \"fallback\" | \"manual\";\n}\n\n/**\n * Geo-detection provider interface\n */\nexport interface GeoDetector {\n /** Detect if user is in the EU */\n detect(): Promise<GeoDetectionResult>;\n}\n\n/**\n * Banner UI configuration\n */\nexport interface BannerConfig {\n /** Banner title */\n title: string;\n /** Main message text */\n message: string;\n /** Accept all button text */\n acceptAll: string;\n /** Reject all button text */\n rejectAll: string;\n /** Customize preferences button text */\n customize?: string;\n /** Privacy policy link */\n privacyLink?: string;\n /** Privacy policy link text */\n privacyLinkText?: string;\n}\n\n/**\n * Main plugin configuration\n */\nexport interface ConsentConfig {\n /** Google Analytics measurement ID (G-XXXXXXXXXX) */\n gaId?: string;\n\n /** Consent categories to manage */\n categories?: Partial<Omit<ConsentCategories, \"necessary\">>;\n\n /** Banner UI configuration */\n banner?: Partial<BannerConfig>;\n\n /** Cookie configuration */\n cookie?: {\n /** Cookie name for storing consent */\n name?: string;\n /** Cookie expiry in days */\n expiry?: number;\n /** Cookie domain */\n domain?: string;\n /** Cookie path */\n path?: string;\n };\n\n /**\n * EU detection mode:\n * - 'auto': Try Cloudflare header, fallback to IP API\n * - 'cloudflare': Only use Cloudflare header\n * - 'api': Only use IP API\n * - 'always': Always show banner (treat all as EU)\n * - 'never': Never show banner (treat all as non-EU)\n */\n euDetection?: \"auto\" | \"cloudflare\" | \"api\" | \"always\" | \"never\";\n\n /** Custom geo-detection provider */\n geoDetector?: GeoDetector;\n\n /**\n * Whether to send automatic page_view on GA initialization.\n * Set to false for SPA apps (VitePress, Vue Router) where you track navigation manually.\n * @default true\n */\n sendPageView?: boolean;\n\n /** Consent version (changing this resets consent for all users) */\n version?: string;\n\n /** Callback when consent changes */\n onConsentChange?: (consent: StoredConsent) => void;\n\n /** Callback when banner is shown */\n onBannerShow?: () => void;\n\n /** Callback when banner is hidden */\n onBannerHide?: () => void;\n}\n\n/**\n * Required cookie configuration (with defaults)\n */\nexport interface CookieConfigDefaults {\n name: string;\n expiry: number;\n path: string;\n domain?: string;\n}\n\n/**\n * Required banner configuration (with defaults)\n */\nexport interface BannerConfigDefaults {\n title: string;\n message: string;\n acceptAll: string;\n rejectAll: string;\n customize: string;\n privacyLink: string;\n privacyLinkText: string;\n}\n\n/**\n * Default configuration values\n */\nexport const DEFAULT_CONFIG: {\n categories: Omit<ConsentCategories, \"necessary\">;\n banner: BannerConfigDefaults;\n cookie: CookieConfigDefaults;\n euDetection: \"auto\" | \"cloudflare\" | \"api\" | \"always\" | \"never\";\n version: string;\n} = {\n categories: {\n analytics: false,\n marketing: false,\n functional: true,\n },\n banner: {\n title: \"Cookie Consent\",\n message:\n \"We use cookies to improve your experience. You can accept all cookies or customize your preferences.\",\n acceptAll: \"Accept All\",\n rejectAll: \"Reject All\",\n customize: \"Customize\",\n privacyLink: \"/privacy\",\n privacyLinkText: \"Privacy Policy\",\n },\n cookie: {\n name: \"consent_preferences\",\n expiry: 365,\n path: \"/\",\n },\n euDetection: \"auto\",\n version: \"1.0\",\n};\n","import type { StoredConsent, ConsentConfig, CookieConfigDefaults } from \"./types\";\nimport { DEFAULT_CONFIG } from \"./types\";\n\n/**\n * Get cookie value by name\n */\nexport function getCookie(name: string): string | null {\n if (typeof document === \"undefined\") return null;\n\n const cookies = document.cookie.split(\";\");\n for (const cookie of cookies) {\n const [key, value] = cookie.trim().split(\"=\");\n if (key === name) {\n return decodeURIComponent(value);\n }\n }\n return null;\n}\n\n/**\n * Set cookie with options\n */\nexport function setCookie(\n name: string,\n value: string,\n options: {\n expiry?: number;\n domain?: string;\n path?: string;\n sameSite?: \"Strict\" | \"Lax\" | \"None\";\n secure?: boolean;\n } = {}\n): void {\n if (typeof document === \"undefined\") return;\n\n const { expiry = 365, domain, path = \"/\", sameSite = \"Lax\", secure = false } = options;\n\n let cookieString = `${name}=${encodeURIComponent(value)}`;\n\n if (expiry) {\n const date = new Date();\n date.setTime(date.getTime() + expiry * 24 * 60 * 60 * 1000);\n cookieString += `; expires=${date.toUTCString()}`;\n }\n\n if (domain) {\n cookieString += `; domain=${domain}`;\n }\n\n cookieString += `; path=${path}`;\n cookieString += `; SameSite=${sameSite}`;\n\n if (secure || sameSite === \"None\") {\n cookieString += \"; Secure\";\n }\n\n document.cookie = cookieString;\n}\n\n/**\n * Delete cookie\n */\nexport function deleteCookie(name: string, path = \"/\"): void {\n if (typeof document === \"undefined\") return;\n document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=${path}`;\n}\n\n/**\n * Get stored consent from cookie\n */\nexport function getStoredConsent(config: Partial<ConsentConfig> = {}): StoredConsent | null {\n const cookieName = config.cookie?.name ?? DEFAULT_CONFIG.cookie.name;\n const version = config.version ?? DEFAULT_CONFIG.version;\n\n const raw = getCookie(cookieName);\n if (!raw) return null;\n\n try {\n const stored = JSON.parse(raw) as StoredConsent;\n\n // Check version - if different, consent is invalid\n if (stored.version !== version) {\n return null;\n }\n\n return stored;\n } catch {\n return null;\n }\n}\n\n/**\n * Store consent in cookie\n */\nexport function storeConsent(\n consent: Omit<StoredConsent, \"timestamp\" | \"version\">,\n config: Partial<ConsentConfig> = {}\n): void {\n const cookieConfig: CookieConfigDefaults = {\n ...DEFAULT_CONFIG.cookie,\n ...config.cookie,\n };\n const version = config.version ?? DEFAULT_CONFIG.version;\n\n const stored: StoredConsent = {\n categories: consent.categories,\n timestamp: Date.now(),\n version,\n };\n\n setCookie(cookieConfig.name, JSON.stringify(stored), {\n expiry: cookieConfig.expiry,\n domain: cookieConfig.domain,\n path: cookieConfig.path,\n });\n}\n\n/**\n * Clear stored consent\n */\nexport function clearConsent(config: Partial<ConsentConfig> = {}): void {\n const cookieName = config.cookie?.name ?? DEFAULT_CONFIG.cookie.name;\n const path = config.cookie?.path ?? DEFAULT_CONFIG.cookie.path;\n deleteCookie(cookieName, path);\n}\n","import type { GoogleConsentSignals, ConsentCategories } from \"./types\";\n\ndeclare global {\n interface Window {\n dataLayer: unknown[];\n gtag: (...args: unknown[]) => void;\n }\n}\n\n/**\n * Initialize gtag and dataLayer if not already present\n */\nexport function initGtag(): void {\n if (typeof window === \"undefined\") return;\n\n window.dataLayer = window.dataLayer || [];\n\n if (typeof window.gtag !== \"function\") {\n window.gtag = function gtag(...args: unknown[]) {\n window.dataLayer.push(args);\n };\n }\n}\n\n/**\n * Convert consent categories to Google Consent Mode signals\n */\nexport function categoriesToGoogleSignals(\n categories: Partial<Omit<ConsentCategories, \"necessary\">>\n): GoogleConsentSignals {\n return {\n analytics_storage: categories.analytics ? \"granted\" : \"denied\",\n ad_storage: categories.marketing ? \"granted\" : \"denied\",\n ad_user_data: categories.marketing ? \"granted\" : \"denied\",\n ad_personalization: categories.marketing ? \"granted\" : \"denied\",\n };\n}\n\n/**\n * Set default consent state (should be called BEFORE loading gtag.js)\n *\n * @param signals - Consent signals to set as defaults\n * @param waitForUpdate - Milliseconds to wait for consent update (for async CMPs)\n */\nexport function setConsentDefaults(\n signals: Partial<GoogleConsentSignals>,\n waitForUpdate = 500\n): void {\n initGtag();\n\n if (typeof window === \"undefined\") return;\n\n window.gtag(\"consent\", \"default\", {\n ...signals,\n wait_for_update: waitForUpdate,\n });\n}\n\n/**\n * Update consent state (after user makes a choice)\n *\n * @param signals - Consent signals to update\n */\nexport function updateConsent(signals: Partial<GoogleConsentSignals>): void {\n initGtag();\n\n if (typeof window === \"undefined\") return;\n\n window.gtag(\"consent\", \"update\", signals);\n}\n\n/**\n * Load Google Analytics gtag.js script\n *\n * @param gaId - Google Analytics measurement ID (G-XXXXXXXXXX)\n */\nexport function loadGtagScript(gaId: string): Promise<void> {\n return new Promise((resolve, reject) => {\n if (typeof document === \"undefined\") {\n resolve();\n return;\n }\n\n // Check if already loaded\n if (document.querySelector(`script[src*=\"googletagmanager.com/gtag/js?id=${gaId}\"]`)) {\n resolve();\n return;\n }\n\n const script = document.createElement(\"script\");\n script.async = true;\n script.src = `https://www.googletagmanager.com/gtag/js?id=${gaId}`;\n script.onload = () => resolve();\n script.onerror = () => reject(new Error(`Failed to load gtag.js for ${gaId}`));\n\n document.head.appendChild(script);\n });\n}\n\n/**\n * Track a page view manually (for SPA navigation)\n *\n * @param path - Page path (e.g., '/docs/guide')\n * @param title - Page title (defaults to document.title)\n */\nexport function trackPageView(path: string, title?: string): void {\n if (typeof window === \"undefined\" || typeof window.gtag !== \"function\") return;\n\n window.gtag(\"event\", \"page_view\", {\n page_path: path,\n page_location: window.location.href,\n page_title: title ?? document.title,\n });\n}\n\n/**\n * Initialize Google Analytics with consent defaults\n *\n * @param gaId - Google Analytics measurement ID\n * @param defaultDenied - Whether to default to denied consent (for EU users)\n * @param sendPageView - Whether to send automatic page_view (false for SPA)\n */\nexport async function initGoogleAnalytics(\n gaId: string,\n defaultDenied = true,\n sendPageView = true\n): Promise<void> {\n initGtag();\n\n // Set defaults BEFORE loading script\n if (defaultDenied) {\n setConsentDefaults({\n analytics_storage: \"denied\",\n ad_storage: \"denied\",\n ad_user_data: \"denied\",\n ad_personalization: \"denied\",\n });\n } else {\n setConsentDefaults({\n analytics_storage: \"granted\",\n ad_storage: \"granted\",\n ad_user_data: \"granted\",\n ad_personalization: \"granted\",\n });\n }\n\n // Load the script\n await loadGtagScript(gaId);\n\n // Initialize GA\n if (typeof window !== \"undefined\") {\n window.gtag(\"js\", new Date());\n window.gtag(\"config\", gaId, {\n send_page_view: sendPageView,\n });\n }\n}\n","import type { GeoDetector, GeoDetectionResult } from \"../core/types\";\n\n/**\n * Cloudflare geo-detection using headers\n *\n * Requires Cloudflare Worker or Transform Rule to set X-Is-EU-Country header\n */\nexport class CloudflareGeoDetector implements GeoDetector {\n private headerName: string;\n\n constructor(headerName = \"X-Is-EU-Country\") {\n this.headerName = headerName;\n }\n\n async detect(): Promise<GeoDetectionResult> {\n if (typeof document === \"undefined\") {\n return { isEU: false, method: \"cloudflare\" };\n }\n\n try {\n // Try to get the header by making a HEAD request to current page\n const response = await fetch(window.location.href, {\n method: \"HEAD\",\n cache: \"no-store\",\n });\n\n const isEUHeader = response.headers.get(this.headerName);\n const countryCode = response.headers.get(\"CF-IPCountry\") ?? undefined;\n\n if (isEUHeader !== null) {\n return {\n isEU: isEUHeader.toLowerCase() === \"true\",\n countryCode,\n method: \"cloudflare\",\n };\n }\n\n // Header not present - Cloudflare not configured\n throw new Error(\"Cloudflare header not present\");\n } catch {\n throw new Error(\"Cloudflare geo-detection failed\");\n }\n }\n}\n\n/**\n * IP API geo-detection using ipapi.co\n *\n * Free tier: 1000 requests/day\n * No API key required for basic usage\n */\nexport class IPAPIGeoDetector implements GeoDetector {\n private apiUrl: string;\n\n constructor(apiUrl = \"https://ipapi.co/json/\") {\n this.apiUrl = apiUrl;\n }\n\n async detect(): Promise<GeoDetectionResult> {\n try {\n const response = await fetch(this.apiUrl);\n const data = (await response.json()) as {\n in_eu?: boolean;\n country_code?: string;\n };\n\n return {\n isEU: data.in_eu === true,\n countryCode: data.country_code,\n method: \"api\",\n };\n } catch {\n throw new Error(\"IP API geo-detection failed\");\n }\n }\n}\n\n/**\n * Fallback detector that uses browser timezone heuristics\n *\n * Not 100% accurate but works without external requests\n */\nexport class TimezoneGeoDetector implements GeoDetector {\n // EU timezones (not exhaustive but covers most)\n private euTimezones = new Set([\n \"Europe/Amsterdam\",\n \"Europe/Andorra\",\n \"Europe/Athens\",\n \"Europe/Berlin\",\n \"Europe/Bratislava\",\n \"Europe/Brussels\",\n \"Europe/Bucharest\",\n \"Europe/Budapest\",\n \"Europe/Copenhagen\",\n \"Europe/Dublin\",\n \"Europe/Helsinki\",\n \"Europe/Lisbon\",\n \"Europe/Ljubljana\",\n \"Europe/Luxembourg\",\n \"Europe/Madrid\",\n \"Europe/Malta\",\n \"Europe/Monaco\",\n \"Europe/Oslo\",\n \"Europe/Paris\",\n \"Europe/Prague\",\n \"Europe/Riga\",\n \"Europe/Rome\",\n \"Europe/San_Marino\",\n \"Europe/Sarajevo\",\n \"Europe/Skopje\",\n \"Europe/Sofia\",\n \"Europe/Stockholm\",\n \"Europe/Tallinn\",\n \"Europe/Tirane\",\n \"Europe/Vaduz\",\n \"Europe/Vatican\",\n \"Europe/Vienna\",\n \"Europe/Vilnius\",\n \"Europe/Warsaw\",\n \"Europe/Zagreb\",\n \"Atlantic/Canary\",\n \"Atlantic/Faroe\",\n \"Atlantic/Madeira\",\n ]);\n\n async detect(): Promise<GeoDetectionResult> {\n try {\n const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;\n const isEU = this.euTimezones.has(timezone);\n\n return {\n isEU,\n method: \"fallback\",\n };\n } catch {\n // If we can't determine, assume EU for safety\n return {\n isEU: true,\n method: \"fallback\",\n };\n }\n }\n}\n\n/**\n * Auto-detection: tries Cloudflare first, then IP API, then timezone fallback\n */\nexport class AutoGeoDetector implements GeoDetector {\n private cloudflare: CloudflareGeoDetector;\n private ipapi: IPAPIGeoDetector;\n private timezone: TimezoneGeoDetector;\n\n constructor() {\n this.cloudflare = new CloudflareGeoDetector();\n this.ipapi = new IPAPIGeoDetector();\n this.timezone = new TimezoneGeoDetector();\n }\n\n async detect(): Promise<GeoDetectionResult> {\n // Try Cloudflare first (fastest, most reliable if available)\n try {\n return await this.cloudflare.detect();\n } catch {\n // Cloudflare not available, continue\n }\n\n // Try IP API\n try {\n return await this.ipapi.detect();\n } catch {\n // IP API failed, continue\n }\n\n // Fallback to timezone heuristics\n return await this.timezone.detect();\n }\n}\n\n/**\n * Create a geo-detector based on mode\n */\nexport function createGeoDetector(\n mode: \"auto\" | \"cloudflare\" | \"api\" | \"always\" | \"never\",\n): GeoDetector {\n switch (mode) {\n case \"cloudflare\":\n return new CloudflareGeoDetector();\n case \"api\":\n return new IPAPIGeoDetector();\n case \"always\":\n return {\n detect: async () => ({ isEU: true, method: \"manual\" as const }),\n };\n case \"never\":\n return {\n detect: async () => ({ isEU: false, method: \"manual\" as const }),\n };\n case \"auto\":\n default:\n return new AutoGeoDetector();\n }\n}\n","import type { ConsentConfig, StoredConsent, ConsentCategories } from \"./types\";\nimport { DEFAULT_CONFIG } from \"./types\";\nimport { getStoredConsent, storeConsent, clearConsent } from \"./storage\";\nimport {\n initGoogleAnalytics,\n updateConsent as updateGoogleConsent,\n categoriesToGoogleSignals,\n trackPageView as gtagTrackPageView,\n} from \"./gtag\";\nimport { createGeoDetector } from \"../geo/index\";\n\n/**\n * Consent Manager - orchestrates consent flow\n */\nexport class ConsentManager {\n private config: ConsentConfig;\n private initialized = false;\n private isEU: boolean | null = null;\n private showBannerCallback: (() => void) | null = null;\n private hideBannerCallback: (() => void) | null = null;\n\n constructor(config: ConsentConfig = {}) {\n this.config = {\n ...config,\n categories: { ...DEFAULT_CONFIG.categories, ...config.categories },\n banner: { ...DEFAULT_CONFIG.banner, ...config.banner },\n cookie: { ...DEFAULT_CONFIG.cookie, ...config.cookie },\n };\n }\n\n /**\n * Register callback to show banner\n */\n onShowBanner(callback: () => void): void {\n this.showBannerCallback = callback;\n }\n\n /**\n * Register callback to hide banner\n */\n onHideBanner(callback: () => void): void {\n this.hideBannerCallback = callback;\n }\n\n /**\n * Initialize consent manager\n */\n async init(): Promise<void> {\n if (this.initialized) return;\n this.initialized = true;\n\n // Check for existing consent\n const stored = getStoredConsent(this.config);\n\n if (stored) {\n // User has already made a choice\n await this.applyConsent(stored.categories);\n return;\n }\n\n // Detect if user is in EU\n const detector =\n this.config.geoDetector ?? createGeoDetector(this.config.euDetection ?? \"auto\");\n const geoResult = await detector.detect();\n this.isEU = geoResult.isEU;\n\n if (this.isEU) {\n // EU user: initialize GA with denied defaults, show banner\n if (this.config.gaId) {\n const sendPageView = this.config.sendPageView ?? true;\n await initGoogleAnalytics(this.config.gaId, true, sendPageView);\n }\n\n // Show banner\n this.showBannerCallback?.();\n this.config.onBannerShow?.();\n } else {\n // Non-EU user: grant consent silently\n const grantedCategories = {\n analytics: true,\n marketing: this.config.categories?.marketing ?? false,\n functional: true,\n };\n\n await this.applyConsent(grantedCategories);\n storeConsent({ categories: grantedCategories }, this.config);\n }\n }\n\n /**\n * Apply consent settings\n */\n private async applyConsent(categories: Omit<ConsentCategories, \"necessary\">): Promise<void> {\n // Initialize GA if configured\n if (this.config.gaId) {\n const sendPageView = this.config.sendPageView ?? true;\n await initGoogleAnalytics(this.config.gaId, !categories.analytics, sendPageView);\n }\n\n // Update Google Consent Mode\n const signals = categoriesToGoogleSignals(categories);\n updateGoogleConsent(signals);\n\n // Notify callback\n this.config.onConsentChange?.({\n categories,\n timestamp: Date.now(),\n version: this.config.version ?? DEFAULT_CONFIG.version,\n });\n }\n\n /**\n * Accept all cookies\n */\n async acceptAll(): Promise<void> {\n const categories = {\n analytics: true,\n marketing: true,\n functional: true,\n };\n\n await this.applyConsent(categories);\n storeConsent({ categories }, this.config);\n\n this.hideBannerCallback?.();\n this.config.onBannerHide?.();\n }\n\n /**\n * Reject all non-essential cookies\n */\n async rejectAll(): Promise<void> {\n const categories = {\n analytics: false,\n marketing: false,\n functional: true, // Functional is always allowed\n };\n\n await this.applyConsent(categories);\n storeConsent({ categories }, this.config);\n\n this.hideBannerCallback?.();\n this.config.onBannerHide?.();\n }\n\n /**\n * Save custom preferences\n */\n async savePreferences(categories: Partial<Omit<ConsentCategories, \"necessary\">>): Promise<void> {\n const finalCategories = {\n analytics: categories.analytics ?? false,\n marketing: categories.marketing ?? false,\n functional: categories.functional ?? true,\n };\n\n await this.applyConsent(finalCategories);\n storeConsent({ categories: finalCategories }, this.config);\n\n this.hideBannerCallback?.();\n this.config.onBannerHide?.();\n }\n\n /**\n * Get current consent state\n */\n getConsent(): StoredConsent | null {\n return getStoredConsent(this.config);\n }\n\n /**\n * Check if user has made a consent choice\n */\n hasConsent(): boolean {\n return getStoredConsent(this.config) !== null;\n }\n\n /**\n * Reset consent (show banner again)\n */\n resetConsent(): void {\n clearConsent(this.config);\n this.showBannerCallback?.();\n this.config.onBannerShow?.();\n }\n\n /**\n * Track a page view manually (for SPA navigation).\n * Skips sending if analytics consent has not been granted.\n */\n trackPageView(path: string, title?: string): void {\n const stored = getStoredConsent(this.config);\n if (stored && !stored.categories.analytics) {\n return;\n }\n gtagTrackPageView(path, title);\n }\n\n /**\n * Check if consent manager has been initialized\n */\n isInitialized(): boolean {\n return this.initialized;\n }\n\n /**\n * Check if user is detected as EU\n */\n isEUUser(): boolean | null {\n return this.isEU;\n }\n\n /**\n * Get configuration\n */\n getConfig(): ConsentConfig {\n return this.config;\n }\n}\n\n/**\n * Create a new ConsentManager instance\n */\nexport function createConsentManager(config: ConsentConfig = {}): ConsentManager {\n return new ConsentManager(config);\n}\n"],"names":["updateGoogleConsent","gtagTrackPageView"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sources":["../src/core/types.ts","../src/core/storage.ts","../src/core/gtag.ts","../src/geo/index.ts","../src/core/consent-manager.ts"],"sourcesContent":["/**\n * Consent categories that can be managed\n */\nexport interface ConsentCategories {\n /** Analytics cookies (e.g., Google Analytics) */\n analytics: boolean;\n /** Marketing/advertising cookies */\n marketing: boolean;\n /** Functional cookies (preferences, etc.) */\n functional: boolean;\n /** Strictly necessary cookies (always true, cannot be disabled) */\n necessary: true;\n}\n\n/**\n * Google Consent Mode v2 signals\n * @see https://developers.google.com/tag-platform/security/guides/consent\n */\nexport interface GoogleConsentSignals {\n /** Controls Google Analytics cookies */\n analytics_storage: \"granted\" | \"denied\";\n /** Controls advertising cookies */\n ad_storage: \"granted\" | \"denied\";\n /** Controls whether user data can be sent to Google for ads */\n ad_user_data: \"granted\" | \"denied\";\n /** Controls personalized advertising */\n ad_personalization: \"granted\" | \"denied\";\n}\n\n/**\n * Stored consent state\n */\nexport interface StoredConsent {\n /** Consent categories */\n categories: Omit<ConsentCategories, \"necessary\">;\n /** Timestamp when consent was given */\n timestamp: number;\n /** Version of the consent configuration */\n version: string;\n}\n\n/**\n * Remote consent storage interface.\n * Implement this to use a custom backend (REST, gRPC, IndexedDB, etc.)\n * instead of the default Cloudflare KV Worker.\n */\nexport interface ConsentStorage {\n /** Fetch stored consent by user ID. Return null if not found or version mismatch. */\n get(uid: string, version: string): Promise<StoredConsent | null>;\n /** Save consent. Return user ID (may generate a new one if uid is null). */\n set(uid: string | null, consent: StoredConsent): Promise<string | null>;\n}\n\n/**\n * Geo-detection result\n */\nexport interface GeoDetectionResult {\n /** Whether the user is in the EU */\n isEU: boolean;\n /** Country code (ISO 3166-1 alpha-2) */\n countryCode?: string;\n /** Detection method used */\n method: \"cloudflare\" | \"api\" | \"fallback\" | \"manual\";\n}\n\n/**\n * Geo-detection provider interface\n */\nexport interface GeoDetector {\n /** Detect if user is in the EU */\n detect(): Promise<GeoDetectionResult>;\n}\n\n/**\n * Banner UI configuration\n */\nexport interface BannerConfig {\n /** Banner title */\n title: string;\n /** Main message text */\n message: string;\n /** Accept all button text */\n acceptAll: string;\n /** Reject all button text */\n rejectAll: string;\n /** Customize preferences button text */\n customize?: string;\n /** Privacy policy link */\n privacyLink?: string;\n /** Privacy policy link text */\n privacyLinkText?: string;\n}\n\n/**\n * Main plugin configuration\n */\nexport interface ConsentConfig {\n /** Google Analytics measurement ID (G-XXXXXXXXXX) */\n gaId?: string;\n\n /** Consent categories to manage */\n categories?: Partial<Omit<ConsentCategories, \"necessary\">>;\n\n /** Banner UI configuration */\n banner?: Partial<BannerConfig>;\n\n /** Cookie configuration */\n cookie?: {\n /** Cookie name for storing consent */\n name?: string;\n /** Cookie expiry in days */\n expiry?: number;\n /** Cookie domain */\n domain?: string;\n /** Cookie path */\n path?: string;\n };\n\n /**\n * EU detection mode:\n * - 'auto': Try Cloudflare header, fallback to IP API\n * - 'cloudflare': Only use Cloudflare header\n * - 'api': Only use IP API\n * - 'always': Always show banner (treat all as EU)\n * - 'never': Never show banner (treat all as non-EU)\n */\n euDetection?: \"auto\" | \"cloudflare\" | \"api\" | \"always\" | \"never\";\n\n /** Custom geo-detection provider */\n geoDetector?: GeoDetector;\n\n /**\n * Whether to send automatic page_view on GA initialization.\n * Set to false for SPA apps (VitePress, Vue Router) where you track navigation manually.\n * @default true\n */\n sendPageView?: boolean;\n\n /**\n * Remote consent storage implementation.\n * When set, consent is persisted remotely and cookie is used\n * only for re-identification. No cookies are set before consent.\n *\n * Use `createKVStorage('/api/consent')` for Cloudflare KV Worker,\n * or implement ConsentStorage interface for custom backends.\n */\n storage?: ConsentStorage;\n\n /** Consent version (changing this resets consent for all users) */\n version?: string;\n\n /** Callback when consent changes */\n onConsentChange?: (consent: StoredConsent) => void;\n\n /** Callback when banner is shown */\n onBannerShow?: () => void;\n\n /** Callback when banner is hidden */\n onBannerHide?: () => void;\n}\n\n/**\n * Required cookie configuration (with defaults)\n */\nexport interface CookieConfigDefaults {\n name: string;\n expiry: number;\n path: string;\n domain?: string;\n}\n\n/**\n * Required banner configuration (with defaults)\n */\nexport interface BannerConfigDefaults {\n title: string;\n message: string;\n acceptAll: string;\n rejectAll: string;\n customize: string;\n privacyLink: string;\n privacyLinkText: string;\n}\n\n/**\n * Default configuration values\n */\nexport const DEFAULT_CONFIG: {\n categories: Omit<ConsentCategories, \"necessary\">;\n banner: BannerConfigDefaults;\n cookie: CookieConfigDefaults;\n euDetection: \"auto\" | \"cloudflare\" | \"api\" | \"always\" | \"never\";\n version: string;\n} = {\n categories: {\n analytics: false,\n marketing: false,\n functional: true,\n },\n banner: {\n title: \"Cookie Consent\",\n message:\n \"We use cookies to improve your experience. You can accept all cookies or customize your preferences.\",\n acceptAll: \"Accept All\",\n rejectAll: \"Reject All\",\n customize: \"Customize\",\n privacyLink: \"/privacy\",\n privacyLinkText: \"Privacy Policy\",\n },\n cookie: {\n name: \"consent_preferences\",\n expiry: 365,\n path: \"/\",\n },\n euDetection: \"auto\",\n version: \"1.0\",\n};\n","import type { StoredConsent, ConsentConfig, CookieConfigDefaults, ConsentStorage } from \"./types\";\nimport { DEFAULT_CONFIG } from \"./types\";\n\n/**\n * Get cookie value by name\n */\nexport function getCookie(name: string): string | null {\n if (typeof document === \"undefined\") return null;\n\n const cookies = document.cookie.split(\";\");\n for (const cookie of cookies) {\n const [key, value] = cookie.trim().split(\"=\");\n if (key === name) {\n return decodeURIComponent(value);\n }\n }\n return null;\n}\n\n/**\n * Set cookie with options\n */\nexport function setCookie(\n name: string,\n value: string,\n options: {\n expiry?: number;\n domain?: string;\n path?: string;\n sameSite?: \"Strict\" | \"Lax\" | \"None\";\n secure?: boolean;\n } = {}\n): void {\n if (typeof document === \"undefined\") return;\n\n const { expiry = 365, domain, path = \"/\", sameSite = \"Lax\", secure = false } = options;\n\n let cookieString = `${name}=${encodeURIComponent(value)}`;\n\n if (expiry) {\n const date = new Date();\n date.setTime(date.getTime() + expiry * 24 * 60 * 60 * 1000);\n cookieString += `; expires=${date.toUTCString()}`;\n }\n\n if (domain) {\n cookieString += `; domain=${domain}`;\n }\n\n cookieString += `; path=${path}`;\n cookieString += `; SameSite=${sameSite}`;\n\n if (secure || sameSite === \"None\") {\n cookieString += \"; Secure\";\n }\n\n document.cookie = cookieString;\n}\n\n/**\n * Delete cookie\n */\nexport function deleteCookie(name: string, path = \"/\", domain?: string): void {\n if (typeof document === \"undefined\") return;\n let cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=${path}`;\n if (domain) cookie += `; domain=${domain}`;\n document.cookie = cookie;\n}\n\n/**\n * Get stored consent from cookie\n */\nexport function getStoredConsent(config: Partial<ConsentConfig> = {}): StoredConsent | null {\n const cookieName = config.cookie?.name ?? DEFAULT_CONFIG.cookie.name;\n const version = config.version ?? DEFAULT_CONFIG.version;\n\n const raw = getCookie(cookieName);\n if (!raw) return null;\n\n try {\n const stored = JSON.parse(raw) as StoredConsent;\n\n // Check version - if different, consent is invalid\n if (stored.version !== version) {\n return null;\n }\n\n return stored;\n } catch {\n return null;\n }\n}\n\n/**\n * Store consent in cookie\n */\nexport function storeConsent(\n consent: Omit<StoredConsent, \"timestamp\" | \"version\">,\n config: Partial<ConsentConfig> = {}\n): void {\n const cookieConfig: CookieConfigDefaults = {\n ...DEFAULT_CONFIG.cookie,\n ...config.cookie,\n };\n const version = config.version ?? DEFAULT_CONFIG.version;\n\n const stored: StoredConsent = {\n categories: consent.categories,\n timestamp: Date.now(),\n version,\n };\n\n setCookie(cookieConfig.name, JSON.stringify(stored), {\n expiry: cookieConfig.expiry,\n domain: cookieConfig.domain,\n path: cookieConfig.path,\n });\n}\n\n/**\n * Clear stored consent\n */\nexport function clearConsent(config: Partial<ConsentConfig> = {}): void {\n const cookieName = config.cookie?.name ?? DEFAULT_CONFIG.cookie.name;\n const path = config.cookie?.path ?? DEFAULT_CONFIG.cookie.path;\n deleteCookie(cookieName, path, config.cookie?.domain);\n}\n\n/**\n * Get consent UID from cookie (used for KV re-identification)\n */\nexport function getConsentUid(): string | null {\n return getCookie(\"consent_uid\");\n}\n\n/**\n * Set consent UID cookie for re-identification\n */\nexport function setConsentUid(uid: string, config: Partial<ConsentConfig> = {}): void {\n setCookie(\"consent_uid\", uid, {\n expiry: config.cookie?.expiry ?? DEFAULT_CONFIG.cookie.expiry,\n domain: config.cookie?.domain,\n path: config.cookie?.path ?? DEFAULT_CONFIG.cookie.path,\n });\n}\n\n/**\n * Clear consent UID cookie\n */\nexport function clearConsentUid(config: Partial<ConsentConfig> = {}): void {\n const path = config.cookie?.path ?? DEFAULT_CONFIG.cookie.path;\n deleteCookie(\"consent_uid\", path, config.cookie?.domain);\n}\n\n/**\n * Fetch consent from remote KV storage\n */\nexport async function fetchRemoteConsent(\n storageUrl: string,\n uid: string,\n version: string\n): Promise<StoredConsent | null> {\n try {\n const res = await fetch(`${storageUrl}?id=${encodeURIComponent(uid)}`);\n if (!res.ok) return null;\n const data = (await res.json()) as { found?: boolean; consent?: StoredConsent };\n if (!data.found || !data.consent) return null;\n if (data.consent.version !== version) return null;\n return {\n categories: data.consent.categories,\n timestamp: data.consent.timestamp ?? Date.now(),\n version,\n };\n } catch {\n return null;\n }\n}\n\n/**\n * Push consent to remote KV storage. Returns the user ID (generated by worker if not provided).\n */\nexport async function pushRemoteConsent(\n storageUrl: string,\n uid: string | null,\n consent: StoredConsent\n): Promise<string | null> {\n try {\n // Only send categories and version — timestamp is generated server-side by the worker\n const body: Record<string, unknown> = {\n categories: consent.categories,\n version: consent.version,\n };\n if (uid) body.id = uid;\n const res = await fetch(storageUrl, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(body),\n });\n if (!res.ok) return null;\n const data = (await res.json()) as { id?: string };\n return data.id ?? null;\n } catch {\n return null;\n }\n}\n\n/**\n * Create a ConsentStorage backed by a Cloudflare KV Worker\n * (vue-privacy-worker compatible API).\n *\n * @example\n * ```ts\n * import { createKVStorage } from '@structured-world/vue-privacy';\n *\n * const manager = createConsentManager({\n * gaId: 'G-XXXXXXXXXX',\n * storage: createKVStorage('/api/consent'),\n * });\n * ```\n */\nexport function createKVStorage(url: string): ConsentStorage {\n return {\n get: (uid, version) => fetchRemoteConsent(url, uid, version),\n set: (uid, consent) => pushRemoteConsent(url, uid, consent),\n };\n}\n","import type { GoogleConsentSignals, ConsentCategories } from \"./types\";\n\ndeclare global {\n interface Window {\n dataLayer: unknown[];\n gtag: (...args: unknown[]) => void;\n }\n}\n\n/**\n * Initialize gtag and dataLayer if not already present\n */\nexport function initGtag(): void {\n if (typeof window === \"undefined\") return;\n\n window.dataLayer = window.dataLayer || [];\n\n if (typeof window.gtag !== \"function\") {\n window.gtag = function gtag(...args: unknown[]) {\n window.dataLayer.push(args);\n };\n }\n}\n\n/**\n * Convert consent categories to Google Consent Mode signals\n */\nexport function categoriesToGoogleSignals(\n categories: Partial<Omit<ConsentCategories, \"necessary\">>\n): GoogleConsentSignals {\n return {\n analytics_storage: categories.analytics ? \"granted\" : \"denied\",\n ad_storage: categories.marketing ? \"granted\" : \"denied\",\n ad_user_data: categories.marketing ? \"granted\" : \"denied\",\n ad_personalization: categories.marketing ? \"granted\" : \"denied\",\n };\n}\n\n/**\n * Set default consent state (should be called BEFORE loading gtag.js)\n *\n * @param signals - Consent signals to set as defaults\n * @param waitForUpdate - Milliseconds to wait for consent update (for async CMPs)\n */\nexport function setConsentDefaults(\n signals: Partial<GoogleConsentSignals>,\n waitForUpdate = 500\n): void {\n initGtag();\n\n if (typeof window === \"undefined\") return;\n\n window.gtag(\"consent\", \"default\", {\n ...signals,\n wait_for_update: waitForUpdate,\n });\n}\n\n/**\n * Update consent state (after user makes a choice)\n *\n * @param signals - Consent signals to update\n */\nexport function updateConsent(signals: Partial<GoogleConsentSignals>): void {\n initGtag();\n\n if (typeof window === \"undefined\") return;\n\n window.gtag(\"consent\", \"update\", signals);\n}\n\n/**\n * Load Google Analytics gtag.js script\n *\n * @param gaId - Google Analytics measurement ID (G-XXXXXXXXXX)\n */\nexport function loadGtagScript(gaId: string): Promise<void> {\n return new Promise((resolve, reject) => {\n if (typeof document === \"undefined\") {\n resolve();\n return;\n }\n\n // Check if already loaded\n if (document.querySelector(`script[src*=\"googletagmanager.com/gtag/js?id=${gaId}\"]`)) {\n resolve();\n return;\n }\n\n const script = document.createElement(\"script\");\n script.async = true;\n script.src = `https://www.googletagmanager.com/gtag/js?id=${gaId}`;\n script.onload = () => resolve();\n script.onerror = () => reject(new Error(`Failed to load gtag.js for ${gaId}`));\n\n document.head.appendChild(script);\n });\n}\n\n/**\n * Track a page view manually (for SPA navigation)\n *\n * @param path - Page path (e.g., '/docs/guide')\n * @param title - Page title (defaults to document.title)\n */\nexport function trackPageView(path: string, title?: string): void {\n if (typeof window === \"undefined\" || typeof window.gtag !== \"function\") return;\n\n window.gtag(\"event\", \"page_view\", {\n page_path: path,\n page_location: window.location.href,\n page_title: title ?? document.title,\n });\n}\n\n/**\n * Initialize Google Analytics with consent defaults\n *\n * @param gaId - Google Analytics measurement ID\n * @param defaultDenied - Whether to default to denied consent (for EU users)\n * @param sendPageView - Whether to send automatic page_view (false for SPA)\n */\nexport async function initGoogleAnalytics(\n gaId: string,\n defaultDenied = true,\n sendPageView = true\n): Promise<void> {\n initGtag();\n\n // Set defaults BEFORE loading script\n if (defaultDenied) {\n setConsentDefaults({\n analytics_storage: \"denied\",\n ad_storage: \"denied\",\n ad_user_data: \"denied\",\n ad_personalization: \"denied\",\n });\n } else {\n setConsentDefaults({\n analytics_storage: \"granted\",\n ad_storage: \"granted\",\n ad_user_data: \"granted\",\n ad_personalization: \"granted\",\n });\n }\n\n // Load the script\n await loadGtagScript(gaId);\n\n // Initialize GA\n if (typeof window !== \"undefined\") {\n window.gtag(\"js\", new Date());\n window.gtag(\"config\", gaId, {\n send_page_view: sendPageView,\n });\n }\n}\n","import type { GeoDetector, GeoDetectionResult } from \"../core/types\";\n\n/**\n * Cloudflare geo-detection using headers\n *\n * Requires Cloudflare Worker or Transform Rule to set X-Is-EU-Country header\n */\nexport class CloudflareGeoDetector implements GeoDetector {\n private headerName: string;\n\n constructor(headerName = \"X-Is-EU-Country\") {\n this.headerName = headerName;\n }\n\n async detect(): Promise<GeoDetectionResult> {\n if (typeof document === \"undefined\") {\n return { isEU: false, method: \"cloudflare\" };\n }\n\n try {\n // Try to get the header by making a HEAD request to current page\n const response = await fetch(window.location.href, {\n method: \"HEAD\",\n cache: \"no-store\",\n });\n\n const isEUHeader = response.headers.get(this.headerName);\n const countryCode = response.headers.get(\"CF-IPCountry\") ?? undefined;\n\n if (isEUHeader !== null) {\n return {\n isEU: isEUHeader.toLowerCase() === \"true\",\n countryCode,\n method: \"cloudflare\",\n };\n }\n\n // Header not present - Cloudflare not configured\n throw new Error(\"Cloudflare header not present\");\n } catch {\n throw new Error(\"Cloudflare geo-detection failed\");\n }\n }\n}\n\n/**\n * IP API geo-detection using ipapi.co\n *\n * Free tier: 1000 requests/day\n * No API key required for basic usage\n */\nexport class IPAPIGeoDetector implements GeoDetector {\n private apiUrl: string;\n\n constructor(apiUrl = \"https://ipapi.co/json/\") {\n this.apiUrl = apiUrl;\n }\n\n async detect(): Promise<GeoDetectionResult> {\n try {\n const response = await fetch(this.apiUrl);\n const data = (await response.json()) as {\n in_eu?: boolean;\n country_code?: string;\n };\n\n return {\n isEU: data.in_eu === true,\n countryCode: data.country_code,\n method: \"api\",\n };\n } catch {\n throw new Error(\"IP API geo-detection failed\");\n }\n }\n}\n\n/**\n * Fallback detector that uses browser timezone heuristics\n *\n * Not 100% accurate but works without external requests\n */\nexport class TimezoneGeoDetector implements GeoDetector {\n // EU timezones (not exhaustive but covers most)\n private euTimezones = new Set([\n \"Europe/Amsterdam\",\n \"Europe/Andorra\",\n \"Europe/Athens\",\n \"Europe/Berlin\",\n \"Europe/Bratislava\",\n \"Europe/Brussels\",\n \"Europe/Bucharest\",\n \"Europe/Budapest\",\n \"Europe/Copenhagen\",\n \"Europe/Dublin\",\n \"Europe/Helsinki\",\n \"Europe/Lisbon\",\n \"Europe/Ljubljana\",\n \"Europe/Luxembourg\",\n \"Europe/Madrid\",\n \"Europe/Malta\",\n \"Europe/Monaco\",\n \"Europe/Oslo\",\n \"Europe/Paris\",\n \"Europe/Prague\",\n \"Europe/Riga\",\n \"Europe/Rome\",\n \"Europe/San_Marino\",\n \"Europe/Sarajevo\",\n \"Europe/Skopje\",\n \"Europe/Sofia\",\n \"Europe/Stockholm\",\n \"Europe/Tallinn\",\n \"Europe/Tirane\",\n \"Europe/Vaduz\",\n \"Europe/Vatican\",\n \"Europe/Vienna\",\n \"Europe/Vilnius\",\n \"Europe/Warsaw\",\n \"Europe/Zagreb\",\n \"Atlantic/Canary\",\n \"Atlantic/Faroe\",\n \"Atlantic/Madeira\",\n ]);\n\n async detect(): Promise<GeoDetectionResult> {\n try {\n const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;\n const isEU = this.euTimezones.has(timezone);\n\n return {\n isEU,\n method: \"fallback\",\n };\n } catch {\n // If we can't determine, assume EU for safety\n return {\n isEU: true,\n method: \"fallback\",\n };\n }\n }\n}\n\n/**\n * Auto-detection: tries Cloudflare first, then IP API, then timezone fallback\n */\nexport class AutoGeoDetector implements GeoDetector {\n private cloudflare: CloudflareGeoDetector;\n private ipapi: IPAPIGeoDetector;\n private timezone: TimezoneGeoDetector;\n\n constructor() {\n this.cloudflare = new CloudflareGeoDetector();\n this.ipapi = new IPAPIGeoDetector();\n this.timezone = new TimezoneGeoDetector();\n }\n\n async detect(): Promise<GeoDetectionResult> {\n // Try Cloudflare first (fastest, most reliable if available)\n try {\n return await this.cloudflare.detect();\n } catch {\n // Cloudflare not available, continue\n }\n\n // Try IP API\n try {\n return await this.ipapi.detect();\n } catch {\n // IP API failed, continue\n }\n\n // Fallback to timezone heuristics\n return await this.timezone.detect();\n }\n}\n\n/**\n * Create a geo-detector based on mode\n */\nexport function createGeoDetector(\n mode: \"auto\" | \"cloudflare\" | \"api\" | \"always\" | \"never\",\n): GeoDetector {\n switch (mode) {\n case \"cloudflare\":\n return new CloudflareGeoDetector();\n case \"api\":\n return new IPAPIGeoDetector();\n case \"always\":\n return {\n detect: async () => ({ isEU: true, method: \"manual\" as const }),\n };\n case \"never\":\n return {\n detect: async () => ({ isEU: false, method: \"manual\" as const }),\n };\n case \"auto\":\n default:\n return new AutoGeoDetector();\n }\n}\n","import type {\n ConsentConfig,\n StoredConsent,\n ConsentCategories,\n ConsentStorage,\n GeoDetectionResult,\n} from \"./types\";\nimport { DEFAULT_CONFIG } from \"./types\";\nimport {\n getStoredConsent,\n storeConsent,\n clearConsent,\n getConsentUid,\n setConsentUid,\n clearConsentUid,\n} from \"./storage\";\nimport {\n initGoogleAnalytics,\n updateConsent as updateGoogleConsent,\n categoriesToGoogleSignals,\n trackPageView as gtagTrackPageView,\n} from \"./gtag\";\nimport { createGeoDetector } from \"../geo/index\";\n\n/**\n * Consent Manager - orchestrates consent flow\n */\nexport class ConsentManager {\n private config: ConsentConfig;\n private initialized = false;\n private isEU: boolean | null = null;\n private geoResult: GeoDetectionResult | null = null;\n private userId: string | null = null;\n private remoteStorage: ConsentStorage | null = null;\n private showBannerCallback: (() => void) | null = null;\n private hideBannerCallback: (() => void) | null = null;\n private bannerPending = false;\n\n constructor(config: ConsentConfig = {}) {\n this.config = {\n ...config,\n categories: { ...DEFAULT_CONFIG.categories, ...config.categories },\n banner: { ...DEFAULT_CONFIG.banner, ...config.banner },\n cookie: { ...DEFAULT_CONFIG.cookie, ...config.cookie },\n };\n\n if (config.storage) {\n this.remoteStorage = config.storage;\n }\n }\n\n /**\n * Register callback to show banner.\n * If init() already requested the banner before this callback was registered,\n * fires immediately (handles race condition with component mount timing).\n */\n onShowBanner(callback: () => void): void {\n this.showBannerCallback = callback;\n if (this.bannerPending) {\n this.bannerPending = false;\n callback();\n }\n }\n\n /**\n * Register callback to hide banner\n */\n onHideBanner(callback: () => void): void {\n this.hideBannerCallback = callback;\n }\n\n /**\n * Initialize consent manager\n */\n async init(): Promise<void> {\n if (this.initialized) return;\n this.initialized = true;\n\n // Fast-path: check consent_preferences cookie\n const stored = getStoredConsent(this.config);\n\n if (stored) {\n await this.applyConsent(stored.categories);\n return;\n }\n\n // Remote fallback: if storage is configured, try to restore consent\n if (this.remoteStorage) {\n const uid = getConsentUid();\n if (uid) {\n this.userId = uid;\n const version = this.config.version ?? DEFAULT_CONFIG.version;\n try {\n const remote = await this.remoteStorage.get(uid, version);\n if (remote) {\n // consent_uid cookie exists only for users who accepted — safe to restore cookie\n storeConsent({ categories: remote.categories }, this.config);\n await this.applyConsent(remote.categories);\n return;\n }\n } catch {\n // Remote storage failed — fall through to geo detection\n }\n }\n }\n\n // Detect if user is in EU\n const detector =\n this.config.geoDetector ?? createGeoDetector(this.config.euDetection ?? \"auto\");\n const geoResult = await detector.detect();\n this.isEU = geoResult.isEU;\n this.geoResult = geoResult;\n\n if (this.isEU) {\n // EU user: initialize GA with denied defaults, show banner\n if (this.config.gaId) {\n const sendPageView = this.config.sendPageView ?? true;\n await initGoogleAnalytics(this.config.gaId, true, sendPageView);\n }\n\n // Show banner (or defer if component hasn't mounted yet)\n if (this.showBannerCallback) {\n this.showBannerCallback();\n } else {\n this.bannerPending = true;\n }\n this.config.onBannerShow?.();\n } else {\n // Non-EU user: grant consent silently\n const grantedCategories = {\n analytics: true,\n marketing: this.config.categories?.marketing ?? false,\n functional: true,\n };\n\n await this.applyConsent(grantedCategories);\n this.saveConsentWithRemote(grantedCategories);\n }\n }\n\n /**\n * Persist consent locally and (if remote storage is configured) remotely.\n * Sets cookies only when at least one non-necessary category is accepted.\n * Fire-and-forget: remote push does not block UI.\n */\n private saveConsentWithRemote(categories: Omit<ConsentCategories, \"necessary\">): void {\n const hasNonNecessary = categories.analytics || categories.marketing;\n\n if (hasNonNecessary) {\n storeConsent({ categories }, this.config);\n }\n\n if (this.remoteStorage) {\n const version = this.config.version ?? DEFAULT_CONFIG.version;\n const consent: StoredConsent = { categories, timestamp: Date.now(), version };\n\n this.remoteStorage\n .set(this.userId, consent)\n .then((id) => {\n if (id && hasNonNecessary) {\n this.userId = id;\n setConsentUid(id, this.config);\n }\n })\n .catch(() => {\n // Silent fail — remote storage is best-effort, local cookies are primary\n });\n }\n }\n\n /**\n * Apply consent settings\n */\n private async applyConsent(categories: Omit<ConsentCategories, \"necessary\">): Promise<void> {\n // Initialize GA if configured\n if (this.config.gaId) {\n const sendPageView = this.config.sendPageView ?? true;\n await initGoogleAnalytics(this.config.gaId, !categories.analytics, sendPageView);\n }\n\n // Update Google Consent Mode\n const signals = categoriesToGoogleSignals(categories);\n updateGoogleConsent(signals);\n\n // Notify callback\n this.config.onConsentChange?.({\n categories,\n timestamp: Date.now(),\n version: this.config.version ?? DEFAULT_CONFIG.version,\n });\n }\n\n /**\n * Accept all cookies\n */\n async acceptAll(): Promise<void> {\n const categories = {\n analytics: true,\n marketing: true,\n functional: true,\n };\n\n await this.applyConsent(categories);\n this.saveConsentWithRemote(categories);\n\n this.hideBannerCallback?.();\n this.config.onBannerHide?.();\n }\n\n /**\n * Reject all non-essential cookies\n */\n async rejectAll(): Promise<void> {\n const categories = {\n analytics: false,\n marketing: false,\n functional: true,\n };\n\n await this.applyConsent(categories);\n this.saveConsentWithRemote(categories);\n\n this.hideBannerCallback?.();\n this.config.onBannerHide?.();\n }\n\n /**\n * Save custom preferences\n */\n async savePreferences(categories: Partial<Omit<ConsentCategories, \"necessary\">>): Promise<void> {\n const finalCategories = {\n analytics: categories.analytics ?? false,\n marketing: categories.marketing ?? false,\n functional: categories.functional ?? true,\n };\n\n await this.applyConsent(finalCategories);\n this.saveConsentWithRemote(finalCategories);\n\n this.hideBannerCallback?.();\n this.config.onBannerHide?.();\n }\n\n /**\n * Get current consent state\n */\n getConsent(): StoredConsent | null {\n return getStoredConsent(this.config);\n }\n\n /**\n * Check if user has made a consent choice\n */\n hasConsent(): boolean {\n return getStoredConsent(this.config) !== null;\n }\n\n /**\n * Reset consent (show banner again)\n */\n resetConsent(): void {\n clearConsent(this.config);\n clearConsentUid(this.config);\n this.userId = null;\n this.showBannerCallback?.();\n this.config.onBannerShow?.();\n }\n\n /**\n * Track a page view manually (for SPA navigation).\n * Skips sending if analytics consent has not been granted.\n */\n trackPageView(path: string, title?: string): void {\n const stored = getStoredConsent(this.config);\n if (stored && !stored.categories.analytics) {\n return;\n }\n gtagTrackPageView(path, title);\n }\n\n /**\n * Check if consent manager has been initialized\n */\n isInitialized(): boolean {\n return this.initialized;\n }\n\n /**\n * Check if user is detected as EU\n */\n isEUUser(): boolean | null {\n return this.isEU;\n }\n\n /**\n * Get geo-detection result (country, method, isEU).\n * Returns null if geo detection has not run yet (e.g., consent was restored from cookie).\n */\n getGeoResult(): GeoDetectionResult | null {\n return this.geoResult;\n }\n\n /**\n * Get configuration\n */\n getConfig(): ConsentConfig {\n return this.config;\n }\n}\n\n/**\n * Create a new ConsentManager instance\n */\nexport function createConsentManager(config: ConsentConfig = {}): ConsentManager {\n return new ConsentManager(config);\n}\n"],"names":["updateGoogleConsent","gtagTrackPageView"],"mappings":"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"}
@@ -1,8 +1,14 @@
1
1
  import type { App } from "vue";
2
+ import type { Router } from "vue-router";
2
3
  import type { ConsentConfig } from "../core/types";
3
4
  import { createConsentPlugin as createBasePlugin, ConsentBanner } from "../vue/index";
4
5
  /**
5
- * Quasar boot file for cookie consent
6
+ * Quasar boot file for cookie consent with automatic SPA page tracking
7
+ *
8
+ * Automatically:
9
+ * - Disables automatic page_view (SPA mode)
10
+ * - Tracks initial page view after init
11
+ * - Watches Vue Router for SPA navigation
6
12
  *
7
13
  * @example
8
14
  * ```ts
@@ -15,19 +21,15 @@ import { createConsentPlugin as createBasePlugin, ConsentBanner } from "../vue/i
15
21
  * }));
16
22
  * ```
17
23
  */
18
- export declare function consentBoot(config: ConsentConfig): ({ app }: {
24
+ export declare function consentBoot(config: ConsentConfig): ({ app, router }: {
19
25
  app: App;
26
+ router?: Router;
20
27
  }) => void;
21
28
  /**
22
- * Quasar plugin (alternative to boot file)
29
+ * Quasar plugin (alternative to boot file, without automatic SPA tracking)
23
30
  *
24
31
  * @example
25
32
  * ```ts
26
- * // quasar.config.js
27
- * framework: {
28
- * plugins: ['Notify', 'Dialog']
29
- * }
30
- *
31
33
  * // main.ts
32
34
  * import { createConsentPlugin } from '@structured-world/vue-privacy/quasar';
33
35
  * app.use(createConsentPlugin({ gaId: 'G-XXX' }));
@@ -1,17 +1,41 @@
1
- import { createConsentPlugin } from "../vue/index.js";
2
- import { ConsentBanner, useConsent } from "../vue/index.js";
1
+ import { nextTick, watch } from "vue";
2
+ import { createConsentManager } from "../index.js";
3
+ import { CONSENT_MANAGER_KEY, ConsentBanner as _sfc_main } from "../vue/index.js";
4
+ import { createConsentPlugin, useConsent } from "../vue/index.js";
3
5
  function consentBoot(config) {
4
- return ({ app }) => {
5
- app.use(
6
- createConsentPlugin({
7
- ...config,
8
- autoInit: true
9
- })
10
- );
6
+ return ({ app, router }) => {
7
+ const manager = createConsentManager({
8
+ ...config,
9
+ sendPageView: !router
10
+ });
11
+ app.provide("consentManager", manager);
12
+ app.provide(CONSENT_MANAGER_KEY, manager);
13
+ app.component("ConsentBanner", _sfc_main);
14
+ if (router) {
15
+ manager.init().then(() => {
16
+ nextTick(() => {
17
+ manager.trackPageView(router.currentRoute.value.fullPath);
18
+ });
19
+ }).catch((err) => {
20
+ console.error("[@structured-world/vue-privacy] Failed to initialize:", err);
21
+ });
22
+ watch(
23
+ () => router.currentRoute.value.fullPath,
24
+ (path) => {
25
+ nextTick(() => {
26
+ manager.trackPageView(path, document.title);
27
+ });
28
+ }
29
+ );
30
+ } else {
31
+ manager.init().catch((err) => {
32
+ console.error("[@structured-world/vue-privacy] Failed to initialize:", err);
33
+ });
34
+ }
11
35
  };
12
36
  }
13
37
  export {
14
- ConsentBanner,
38
+ _sfc_main as ConsentBanner,
15
39
  consentBoot,
16
40
  createConsentPlugin,
17
41
  useConsent
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../../src/quasar/index.ts"],"sourcesContent":["import type { App } from \"vue\";\nimport type { ConsentConfig } from \"../core/types\";\nimport { createConsentPlugin as createBasePlugin, ConsentBanner } from \"../vue/index\";\n\n/**\n * Quasar boot file for cookie consent\n *\n * @example\n * ```ts\n * // src/boot/consent.ts\n * import { boot } from 'quasar/wrappers';\n * import { consentBoot } from '@structured-world/vue-privacy/quasar';\n *\n * export default boot(consentBoot({\n * gaId: 'G-XXXXXXXXXX',\n * }));\n * ```\n */\nexport function consentBoot(config: ConsentConfig) {\n return ({ app }: { app: App }) => {\n app.use(\n createBasePlugin({\n ...config,\n autoInit: true,\n })\n );\n };\n}\n\n/**\n * Quasar plugin (alternative to boot file)\n *\n * @example\n * ```ts\n * // quasar.config.js\n * framework: {\n * plugins: ['Notify', 'Dialog']\n * }\n *\n * // main.ts\n * import { createConsentPlugin } from '@structured-world/vue-privacy/quasar';\n * app.use(createConsentPlugin({ gaId: 'G-XXX' }));\n * ```\n */\nexport { createBasePlugin as createConsentPlugin };\n\n// Re-export component\nexport { ConsentBanner };\n\n// Re-export composable\nexport { useConsent } from \"../vue/index\";\n\n// Re-export types\nexport type { ConsentConfig } from \"../core/types\";\n"],"names":["createBasePlugin"],"mappings":";;AAkBO,SAAS,YAAY,QAAuB;AACjD,SAAO,CAAC,EAAE,UAAwB;AAChC,QAAI;AAAA,MACFA,oBAAiB;AAAA,QACf,GAAG;AAAA,QACH,UAAU;AAAA,MAAA,CACX;AAAA,IAAA;AAAA,EAEL;AACF;"}
1
+ {"version":3,"file":"index.js","sources":["../../src/quasar/index.ts"],"sourcesContent":["import type { App } from \"vue\";\nimport { watch, nextTick } from \"vue\";\nimport type { Router } from \"vue-router\";\nimport type { ConsentConfig } from \"../core/types\";\nimport { createConsentManager } from \"../core/consent-manager\";\nimport {\n createConsentPlugin as createBasePlugin,\n ConsentBanner,\n CONSENT_MANAGER_KEY,\n} from \"../vue/index\";\n\n/**\n * Quasar boot file for cookie consent with automatic SPA page tracking\n *\n * Automatically:\n * - Disables automatic page_view (SPA mode)\n * - Tracks initial page view after init\n * - Watches Vue Router for SPA navigation\n *\n * @example\n * ```ts\n * // src/boot/consent.ts\n * import { boot } from 'quasar/wrappers';\n * import { consentBoot } from '@structured-world/vue-privacy/quasar';\n *\n * export default boot(consentBoot({\n * gaId: 'G-XXXXXXXXXX',\n * }));\n * ```\n */\nexport function consentBoot(config: ConsentConfig) {\n return ({ app, router }: { app: App; router?: Router }) => {\n const manager = createConsentManager({\n ...config,\n sendPageView: !router,\n });\n\n // Provide manager for injection\n app.provide(\"consentManager\", manager);\n app.provide(CONSENT_MANAGER_KEY, manager);\n\n // Register global component\n app.component(\"ConsentBanner\", ConsentBanner);\n\n if (router) {\n // SPA mode: track initial page view after init, watch route changes\n manager\n .init()\n .then(() => {\n nextTick(() => {\n manager.trackPageView(router.currentRoute.value.fullPath);\n });\n })\n .catch((err) => {\n console.error(\"[@structured-world/vue-privacy] Failed to initialize:\", err);\n });\n\n // Vue watch does NOT fire on initial value by default — no duplicate tracking.\n // No race condition: watch fires only on subsequent navigations (after init),\n // and trackPageView() safely checks consent state on each call.\n watch(\n () => router.currentRoute.value.fullPath,\n (path) => {\n nextTick(() => {\n manager.trackPageView(path, document.title);\n });\n }\n );\n } else {\n // No router — fallback to automatic page_view from gtag\n manager.init().catch((err) => {\n console.error(\"[@structured-world/vue-privacy] Failed to initialize:\", err);\n });\n }\n };\n}\n\n/**\n * Quasar plugin (alternative to boot file, without automatic SPA tracking)\n *\n * @example\n * ```ts\n * // main.ts\n * import { createConsentPlugin } from '@structured-world/vue-privacy/quasar';\n * app.use(createConsentPlugin({ gaId: 'G-XXX' }));\n * ```\n */\nexport { createBasePlugin as createConsentPlugin };\n\n// Re-export component\nexport { ConsentBanner };\n\n// Re-export composable\nexport { useConsent } from \"../vue/index\";\n\n// Re-export types\nexport type { ConsentConfig } from \"../core/types\";\n"],"names":["ConsentBanner"],"mappings":";;;;AA8BO,SAAS,YAAY,QAAuB;AACjD,SAAO,CAAC,EAAE,KAAK,aAA4C;AACzD,UAAM,UAAU,qBAAqB;AAAA,MACnC,GAAG;AAAA,MACH,cAAc,CAAC;AAAA,IAAA,CAChB;AAGD,QAAI,QAAQ,kBAAkB,OAAO;AACrC,QAAI,QAAQ,qBAAqB,OAAO;AAGxC,QAAI,UAAU,iBAAiBA,SAAa;AAE5C,QAAI,QAAQ;AAEV,cACG,OACA,KAAK,MAAM;AACV,iBAAS,MAAM;AACb,kBAAQ,cAAc,OAAO,aAAa,MAAM,QAAQ;AAAA,QAC1D,CAAC;AAAA,MACH,CAAC,EACA,MAAM,CAAC,QAAQ;AACd,gBAAQ,MAAM,yDAAyD,GAAG;AAAA,MAC5E,CAAC;AAKH;AAAA,QACE,MAAM,OAAO,aAAa,MAAM;AAAA,QAChC,CAAC,SAAS;AACR,mBAAS,MAAM;AACb,oBAAQ,cAAc,MAAM,SAAS,KAAK;AAAA,UAC5C,CAAC;AAAA,QACH;AAAA,MAAA;AAAA,IAEJ,OAAO;AAEL,cAAQ,KAAA,EAAO,MAAM,CAAC,QAAQ;AAC5B,gBAAQ,MAAM,yDAAyD,GAAG;AAAA,MAC5E,CAAC;AAAA,IACH;AAAA,EACF;AACF;"}
@@ -58,6 +58,8 @@ export declare function useConsent(): {
58
58
  trackPageView: (path: string, title?: string) => void;
59
59
  /** Check if user is detected as EU */
60
60
  isEUUser: () => boolean | null;
61
+ /** Get geo-detection result (country, method, isEU) */
62
+ getGeoResult: () => import("..").GeoDetectionResult | null;
61
63
  /** Get the underlying manager instance */
62
64
  manager: ConsentManager;
63
65
  };
package/dist/vue/index.js CHANGED
@@ -164,6 +164,8 @@ function useConsent() {
164
164
  trackPageView: (path, title) => manager.trackPageView(path, title),
165
165
  /** Check if user is detected as EU */
166
166
  isEUUser: () => manager.isEUUser(),
167
+ /** Get geo-detection result (country, method, isEU) */
168
+ getGeoResult: () => manager.getGeoResult(),
167
169
  /** Get the underlying manager instance */
168
170
  manager
169
171
  };
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../../src/vue/ConsentBanner.vue","../../src/vue/index.ts"],"sourcesContent":["<script setup lang=\"ts\">\nimport { ref, computed, onMounted, inject } from \"vue\";\nimport type { ConsentManager } from \"../core/consent-manager\";\nimport type { BannerConfig, BannerConfigDefaults } from \"../core/types\";\nimport { DEFAULT_CONFIG } from \"../core/types\";\n\nconst props = defineProps<{\n /** Custom banner configuration */\n config?: Partial<BannerConfig>;\n /** Position of the banner */\n position?: \"bottom\" | \"top\" | \"center\";\n}>();\n\nconst emit = defineEmits<{\n accept: [];\n reject: [];\n customize: [];\n}>();\n\n// Inject consent manager from Vue plugin\nconst consentManager = inject<ConsentManager>(\"consentManager\");\n\n// State\nconst visible = ref(false);\n\n// Merged config with defaults\nconst bannerConfig = computed<BannerConfigDefaults>(() => {\n const managerConfig = consentManager?.getConfig().banner;\n const propsConfig = props.config;\n return {\n title: propsConfig?.title ?? managerConfig?.title ?? DEFAULT_CONFIG.banner.title,\n message: propsConfig?.message ?? managerConfig?.message ?? DEFAULT_CONFIG.banner.message,\n acceptAll:\n propsConfig?.acceptAll ?? managerConfig?.acceptAll ?? DEFAULT_CONFIG.banner.acceptAll,\n rejectAll:\n propsConfig?.rejectAll ?? managerConfig?.rejectAll ?? DEFAULT_CONFIG.banner.rejectAll,\n customize:\n propsConfig?.customize ?? managerConfig?.customize ?? DEFAULT_CONFIG.banner.customize,\n privacyLink:\n propsConfig?.privacyLink ?? managerConfig?.privacyLink ?? DEFAULT_CONFIG.banner.privacyLink,\n privacyLinkText:\n propsConfig?.privacyLinkText ??\n managerConfig?.privacyLinkText ??\n DEFAULT_CONFIG.banner.privacyLinkText,\n };\n});\n\n// Position classes\nconst positionClasses = computed(() => {\n switch (props.position ?? \"bottom\") {\n case \"top\":\n return \"consent-banner--top\";\n case \"center\":\n return \"consent-banner--center\";\n default:\n return \"consent-banner--bottom\";\n }\n});\n\n// Register show/hide callbacks with manager\nonMounted(() => {\n if (consentManager) {\n consentManager.onShowBanner(() => {\n visible.value = true;\n });\n\n consentManager.onHideBanner(() => {\n visible.value = false;\n });\n }\n});\n\n// Actions\nasync function handleAccept() {\n await consentManager?.acceptAll();\n emit(\"accept\");\n}\n\nasync function handleReject() {\n await consentManager?.rejectAll();\n emit(\"reject\");\n}\n\nfunction handleCustomize() {\n emit(\"customize\");\n}\n</script>\n\n<template>\n <Teleport to=\"body\">\n <Transition name=\"consent-banner\">\n <div\n v-if=\"visible\"\n class=\"consent-banner\"\n :class=\"positionClasses\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"consent-banner-title\"\n aria-describedby=\"consent-banner-message\"\n >\n <div class=\"consent-banner__content\">\n <h2 id=\"consent-banner-title\" class=\"consent-banner__title\">\n {{ bannerConfig.title }}\n </h2>\n <p id=\"consent-banner-message\" class=\"consent-banner__message\">\n {{ bannerConfig.message }}\n <a\n v-if=\"bannerConfig.privacyLink\"\n :href=\"bannerConfig.privacyLink\"\n class=\"consent-banner__privacy-link\"\n target=\"_blank\"\n rel=\"noopener\"\n >\n {{ bannerConfig.privacyLinkText }}\n </a>\n </p>\n </div>\n\n <div class=\"consent-banner__actions\">\n <button\n type=\"button\"\n class=\"consent-banner__btn consent-banner__btn--reject\"\n @click=\"handleReject\"\n >\n {{ bannerConfig.rejectAll }}\n </button>\n\n <button\n v-if=\"bannerConfig.customize\"\n type=\"button\"\n class=\"consent-banner__btn consent-banner__btn--customize\"\n @click=\"handleCustomize\"\n >\n {{ bannerConfig.customize }}\n </button>\n\n <button\n type=\"button\"\n class=\"consent-banner__btn consent-banner__btn--accept\"\n @click=\"handleAccept\"\n >\n {{ bannerConfig.acceptAll }}\n </button>\n </div>\n </div>\n </Transition>\n </Teleport>\n</template>\n\n<style>\n.consent-banner {\n position: fixed;\n left: 0;\n right: 0;\n z-index: 9999;\n padding: 1rem;\n background: var(--consent-bg, #ffffff);\n color: var(--consent-text, #1a1a1a);\n box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);\n font-family: var(--consent-font, system-ui, -apple-system, sans-serif);\n}\n\n.consent-banner--bottom {\n bottom: 0;\n}\n\n.consent-banner--top {\n top: 0;\n box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);\n}\n\n.consent-banner--center {\n top: 50%;\n left: 50%;\n right: auto;\n transform: translate(-50%, -50%);\n max-width: 500px;\n border-radius: 8px;\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);\n}\n\n.consent-banner__content {\n max-width: 1200px;\n margin: 0 auto;\n}\n\n.consent-banner__title {\n margin: 0 0 0.5rem;\n font-size: 1.125rem;\n font-weight: 600;\n}\n\n.consent-banner__message {\n margin: 0 0 1rem;\n font-size: 0.875rem;\n line-height: 1.5;\n color: var(--consent-text-secondary, #666666);\n}\n\n.consent-banner__privacy-link {\n color: var(--consent-link, #0066cc);\n text-decoration: underline;\n}\n\n.consent-banner__actions {\n display: flex;\n flex-wrap: wrap;\n gap: 0.5rem;\n justify-content: flex-end;\n max-width: 1200px;\n margin: 0 auto;\n}\n\n.consent-banner__btn {\n padding: 0.5rem 1rem;\n font-size: 0.875rem;\n font-weight: 500;\n border: none;\n border-radius: 4px;\n cursor: pointer;\n transition:\n background-color 0.2s,\n opacity 0.2s;\n}\n\n.consent-banner__btn:hover {\n opacity: 0.9;\n}\n\n.consent-banner__btn--accept {\n background: var(--consent-btn-accept-bg, #0066cc);\n color: var(--consent-btn-accept-text, #ffffff);\n}\n\n.consent-banner__btn--reject {\n background: var(--consent-btn-reject-bg, #e0e0e0);\n color: var(--consent-btn-reject-text, #1a1a1a);\n}\n\n.consent-banner__btn--customize {\n background: transparent;\n color: var(--consent-link, #0066cc);\n border: 1px solid currentColor;\n}\n\n/* Transitions */\n.consent-banner-enter-active,\n.consent-banner-leave-active {\n transition:\n transform 0.3s ease,\n opacity 0.3s ease;\n}\n\n.consent-banner--bottom.consent-banner-enter-from,\n.consent-banner--bottom.consent-banner-leave-to {\n transform: translateY(100%);\n opacity: 0;\n}\n\n.consent-banner--top.consent-banner-enter-from,\n.consent-banner--top.consent-banner-leave-to {\n transform: translateY(-100%);\n opacity: 0;\n}\n\n.consent-banner--center.consent-banner-enter-from,\n.consent-banner--center.consent-banner-leave-to {\n transform: translate(-50%, -50%) scale(0.9);\n opacity: 0;\n}\n\n/* Dark mode support */\n@media (prefers-color-scheme: dark) {\n .consent-banner {\n --consent-bg: #1a1a1a;\n --consent-text: #ffffff;\n --consent-text-secondary: #a0a0a0;\n --consent-btn-reject-bg: #333333;\n --consent-btn-reject-text: #ffffff;\n }\n}\n\n/* Mobile responsiveness */\n@media (max-width: 640px) {\n .consent-banner__actions {\n flex-direction: column;\n }\n\n .consent-banner__btn {\n width: 100%;\n justify-content: center;\n }\n}\n</style>\n","import type { App, Plugin } from \"vue\";\nimport type { ConsentConfig } from \"../core/types\";\nimport { ConsentManager, createConsentManager } from \"../core/consent-manager\";\nimport ConsentBanner from \"./ConsentBanner.vue\";\n\n/**\n * Vue plugin options\n */\nexport interface ConsentPluginOptions extends ConsentConfig {\n /** Auto-initialize on plugin install */\n autoInit?: boolean;\n}\n\n/**\n * Symbol for injection\n */\nexport const CONSENT_MANAGER_KEY = Symbol(\"consentManager\");\n\n/**\n * Create Vue plugin for cookie consent\n *\n * @example\n * ```ts\n * import { createApp } from 'vue';\n * import { createConsentPlugin } from '@structured-world/vue-privacy/vue';\n *\n * const app = createApp(App);\n * app.use(createConsentPlugin({\n * gaId: 'G-XXXXXXXXXX',\n * autoInit: true,\n * }));\n * ```\n */\nexport function createConsentPlugin(options: ConsentPluginOptions = {}): Plugin {\n const { autoInit = true, ...config } = options;\n\n return {\n install(app: App) {\n const manager = createConsentManager(config);\n\n // Provide manager for injection\n app.provide(\"consentManager\", manager);\n app.provide(CONSENT_MANAGER_KEY, manager);\n\n // Register global component\n app.component(\"ConsentBanner\", ConsentBanner);\n\n // Auto-initialize if requested\n if (autoInit) {\n // Wait for app to mount, then initialize\n const originalMount = app.mount.bind(app);\n app.mount = (rootContainer, ...args) => {\n const result = originalMount(rootContainer, ...args);\n\n // Initialize after mount\n manager.init().catch((err) => {\n console.error(\"[@structured-world/vue-privacy] Failed to initialize:\", err);\n });\n\n return result;\n };\n }\n },\n };\n}\n\n/**\n * Composable to access consent manager\n *\n * @example\n * ```vue\n * <script setup>\n * import { useConsent } from '@structured-world/vue-privacy/vue';\n *\n * const { acceptAll, rejectAll, hasConsent } = useConsent();\n * </script>\n * ```\n */\nexport function useConsent() {\n const manager = inject<ConsentManager>(\"consentManager\");\n\n if (!manager) {\n throw new Error(\n \"[@structured-world/vue-privacy] useConsent() called without plugin. \" +\n \"Did you forget to app.use(createConsentPlugin())?\"\n );\n }\n\n return {\n /** Accept all cookies */\n acceptAll: () => manager.acceptAll(),\n /** Reject all non-essential cookies */\n rejectAll: () => manager.rejectAll(),\n /** Save custom preferences */\n savePreferences: (categories: Parameters<typeof manager.savePreferences>[0]) =>\n manager.savePreferences(categories),\n /** Get current consent state */\n getConsent: () => manager.getConsent(),\n /** Check if user has made a consent choice */\n hasConsent: () => manager.hasConsent(),\n /** Reset consent and show banner again */\n resetConsent: () => manager.resetConsent(),\n /** Track a page view manually (for SPA navigation) */\n trackPageView: (path: string, title?: string) => manager.trackPageView(path, title),\n /** Check if user is detected as EU */\n isEUUser: () => manager.isEUUser(),\n /** Get the underlying manager instance */\n manager,\n };\n}\n\n// Need to import inject for useConsent\nimport { inject } from \"vue\";\n\n// Re-export component\nexport { ConsentBanner };\n\n// Re-export types\nexport type { ConsentConfig, ConsentManager };\n"],"names":["_createBlock","_Teleport","_createVNode","_Transition","_createElementBlock","_normalizeClass","_createElementVNode","_toDisplayString","ConsentBanner"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAMA,UAAM,QAAQ;AAOd,UAAM,OAAO;AAOb,UAAM,iBAAiB,OAAuB,gBAAgB;AAG9D,UAAM,UAAU,IAAI,KAAK;AAGzB,UAAM,eAAe,SAA+B,MAAM;AACxD,YAAM,gBAAgB,iDAAgB,YAAY;AAClD,YAAM,cAAc,MAAM;AAC1B,aAAO;AAAA,QACL,QAAO,2CAAa,WAAS,+CAAe,UAAS,eAAe,OAAO;AAAA,QAC3E,UAAS,2CAAa,aAAW,+CAAe,YAAW,eAAe,OAAO;AAAA,QACjF,YACE,2CAAa,eAAa,+CAAe,cAAa,eAAe,OAAO;AAAA,QAC9E,YACE,2CAAa,eAAa,+CAAe,cAAa,eAAe,OAAO;AAAA,QAC9E,YACE,2CAAa,eAAa,+CAAe,cAAa,eAAe,OAAO;AAAA,QAC9E,cACE,2CAAa,iBAAe,+CAAe,gBAAe,eAAe,OAAO;AAAA,QAClF,kBACE,2CAAa,qBACb,+CAAe,oBACf,eAAe,OAAO;AAAA,MAAA;AAAA,IAE5B,CAAC;AAGD,UAAM,kBAAkB,SAAS,MAAM;AACrC,cAAQ,MAAM,YAAY,UAAA;AAAA,QACxB,KAAK;AACH,iBAAO;AAAA,QACT,KAAK;AACH,iBAAO;AAAA,QACT;AACE,iBAAO;AAAA,MAAA;AAAA,IAEb,CAAC;AAGD,cAAU,MAAM;AACd,UAAI,gBAAgB;AAClB,uBAAe,aAAa,MAAM;AAChC,kBAAQ,QAAQ;AAAA,QAClB,CAAC;AAED,uBAAe,aAAa,MAAM;AAChC,kBAAQ,QAAQ;AAAA,QAClB,CAAC;AAAA,MACH;AAAA,IACF,CAAC;AAGD,mBAAe,eAAe;AAC5B,aAAM,iDAAgB;AACtB,WAAK,QAAQ;AAAA,IACf;AAEA,mBAAe,eAAe;AAC5B,aAAM,iDAAgB;AACtB,WAAK,QAAQ;AAAA,IACf;AAEA,aAAS,kBAAkB;AACzB,WAAK,WAAW;AAAA,IAClB;;0BAIEA,YAyDWC,UAAA,EAzDD,IAAG,UAAM;AAAA,QACjBC,YAuDaC,YAAA,EAvDD,MAAK,oBAAgB;AAAA,2BAC/B,MAqDM;AAAA,YApDE,QAAA,sBADRC,mBAqDM,OAAA;AAAA;cAnDJ,OAAKC,eAAA,CAAC,kBACE,gBAAA,KAAe,CAAA;AAAA,cACvB,MAAK;AAAA,cACL,cAAW;AAAA,cACX,mBAAgB;AAAA,cAChB,oBAAiB;AAAA,YAAA;cAEjBC,mBAgBM,OAhBN,YAgBM;AAAA,gBAfJA,mBAEK,MAFL,YAEKC,gBADA,aAAA,MAAa,KAAK,GAAA,CAAA;AAAA,gBAEvBD,mBAWI,KAXJ,YAWI;AAAA,kDAVC,aAAA,MAAa,OAAO,IAAG,KAC1B,CAAA;AAAA,kBACQ,aAAA,MAAa,4BADrBF,mBAQI,KAAA;AAAA;oBAND,MAAM,aAAA,MAAa;AAAA,oBACpB,OAAM;AAAA,oBACN,QAAO;AAAA,oBACP,KAAI;AAAA,kBAAA,GAEDG,gBAAA,aAAA,MAAa,eAAe,GAAA,GAAA,UAAA;;;cAKrCD,mBAyBM,OAzBN,YAyBM;AAAA,gBAxBJA,mBAMS,UAAA;AAAA,kBALP,MAAK;AAAA,kBACL,OAAM;AAAA,kBACL,SAAO;AAAA,gBAAA,GAELC,gBAAA,aAAA,MAAa,SAAS,GAAA,CAAA;AAAA,gBAInB,aAAA,MAAa,0BADrBH,mBAOS,UAAA;AAAA;kBALP,MAAK;AAAA,kBACL,OAAM;AAAA,kBACL,SAAO;AAAA,gBAAA,GAELG,gBAAA,aAAA,MAAa,SAAS,GAAA,CAAA;gBAG3BD,mBAMS,UAAA;AAAA,kBALP,MAAK;AAAA,kBACL,OAAM;AAAA,kBACL,SAAO;AAAA,gBAAA,GAELC,gBAAA,aAAA,MAAa,SAAS,GAAA,CAAA;AAAA,cAAA;;;;;;;;;AC7H9B,MAAM,sBAAsB,OAAO,gBAAgB;AAiBnD,SAAS,oBAAoB,UAAgC,IAAY;AAC9E,QAAM,EAAE,WAAW,MAAM,GAAG,WAAW;AAEvC,SAAO;AAAA,IACL,QAAQ,KAAU;AAChB,YAAM,UAAU,qBAAqB,MAAM;AAG3C,UAAI,QAAQ,kBAAkB,OAAO;AACrC,UAAI,QAAQ,qBAAqB,OAAO;AAGxC,UAAI,UAAU,iBAAiBC,SAAa;AAG5C,UAAI,UAAU;AAEZ,cAAM,gBAAgB,IAAI,MAAM,KAAK,GAAG;AACxC,YAAI,QAAQ,CAAC,kBAAkB,SAAS;AACtC,gBAAM,SAAS,cAAc,eAAe,GAAG,IAAI;AAGnD,kBAAQ,KAAA,EAAO,MAAM,CAAC,QAAQ;AAC5B,oBAAQ,MAAM,yDAAyD,GAAG;AAAA,UAC5E,CAAC;AAED,iBAAO;AAAA,QACT;AAAA,MACF;AAAA,IACF;AAAA,EAAA;AAEJ;AAcO,SAAS,aAAa;AAC3B,QAAM,UAAU,OAAuB,gBAAgB;AAEvD,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI;AAAA,MACR;AAAA,IAAA;AAAA,EAGJ;AAEA,SAAO;AAAA;AAAA,IAEL,WAAW,MAAM,QAAQ,UAAA;AAAA;AAAA,IAEzB,WAAW,MAAM,QAAQ,UAAA;AAAA;AAAA,IAEzB,iBAAiB,CAAC,eAChB,QAAQ,gBAAgB,UAAU;AAAA;AAAA,IAEpC,YAAY,MAAM,QAAQ,WAAA;AAAA;AAAA,IAE1B,YAAY,MAAM,QAAQ,WAAA;AAAA;AAAA,IAE1B,cAAc,MAAM,QAAQ,aAAA;AAAA;AAAA,IAE5B,eAAe,CAAC,MAAc,UAAmB,QAAQ,cAAc,MAAM,KAAK;AAAA;AAAA,IAElF,UAAU,MAAM,QAAQ,SAAA;AAAA;AAAA,IAExB;AAAA,EAAA;AAEJ;"}
1
+ {"version":3,"file":"index.js","sources":["../../src/vue/ConsentBanner.vue","../../src/vue/index.ts"],"sourcesContent":["<script setup lang=\"ts\">\nimport { ref, computed, onMounted, inject } from \"vue\";\nimport type { ConsentManager } from \"../core/consent-manager\";\nimport type { BannerConfig, BannerConfigDefaults } from \"../core/types\";\nimport { DEFAULT_CONFIG } from \"../core/types\";\n\nconst props = defineProps<{\n /** Custom banner configuration */\n config?: Partial<BannerConfig>;\n /** Position of the banner */\n position?: \"bottom\" | \"top\" | \"center\";\n}>();\n\nconst emit = defineEmits<{\n accept: [];\n reject: [];\n customize: [];\n}>();\n\n// Inject consent manager from Vue plugin\nconst consentManager = inject<ConsentManager>(\"consentManager\");\n\n// State\nconst visible = ref(false);\n\n// Merged config with defaults\nconst bannerConfig = computed<BannerConfigDefaults>(() => {\n const managerConfig = consentManager?.getConfig().banner;\n const propsConfig = props.config;\n return {\n title: propsConfig?.title ?? managerConfig?.title ?? DEFAULT_CONFIG.banner.title,\n message: propsConfig?.message ?? managerConfig?.message ?? DEFAULT_CONFIG.banner.message,\n acceptAll:\n propsConfig?.acceptAll ?? managerConfig?.acceptAll ?? DEFAULT_CONFIG.banner.acceptAll,\n rejectAll:\n propsConfig?.rejectAll ?? managerConfig?.rejectAll ?? DEFAULT_CONFIG.banner.rejectAll,\n customize:\n propsConfig?.customize ?? managerConfig?.customize ?? DEFAULT_CONFIG.banner.customize,\n privacyLink:\n propsConfig?.privacyLink ?? managerConfig?.privacyLink ?? DEFAULT_CONFIG.banner.privacyLink,\n privacyLinkText:\n propsConfig?.privacyLinkText ??\n managerConfig?.privacyLinkText ??\n DEFAULT_CONFIG.banner.privacyLinkText,\n };\n});\n\n// Position classes\nconst positionClasses = computed(() => {\n switch (props.position ?? \"bottom\") {\n case \"top\":\n return \"consent-banner--top\";\n case \"center\":\n return \"consent-banner--center\";\n default:\n return \"consent-banner--bottom\";\n }\n});\n\n// Register show/hide callbacks with manager\nonMounted(() => {\n if (consentManager) {\n consentManager.onShowBanner(() => {\n visible.value = true;\n });\n\n consentManager.onHideBanner(() => {\n visible.value = false;\n });\n }\n});\n\n// Actions\nasync function handleAccept() {\n await consentManager?.acceptAll();\n emit(\"accept\");\n}\n\nasync function handleReject() {\n await consentManager?.rejectAll();\n emit(\"reject\");\n}\n\nfunction handleCustomize() {\n emit(\"customize\");\n}\n</script>\n\n<template>\n <Teleport to=\"body\">\n <Transition name=\"consent-banner\">\n <div\n v-if=\"visible\"\n class=\"consent-banner\"\n :class=\"positionClasses\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"consent-banner-title\"\n aria-describedby=\"consent-banner-message\"\n >\n <div class=\"consent-banner__content\">\n <h2 id=\"consent-banner-title\" class=\"consent-banner__title\">\n {{ bannerConfig.title }}\n </h2>\n <p id=\"consent-banner-message\" class=\"consent-banner__message\">\n {{ bannerConfig.message }}\n <a\n v-if=\"bannerConfig.privacyLink\"\n :href=\"bannerConfig.privacyLink\"\n class=\"consent-banner__privacy-link\"\n target=\"_blank\"\n rel=\"noopener\"\n >\n {{ bannerConfig.privacyLinkText }}\n </a>\n </p>\n </div>\n\n <div class=\"consent-banner__actions\">\n <button\n type=\"button\"\n class=\"consent-banner__btn consent-banner__btn--reject\"\n @click=\"handleReject\"\n >\n {{ bannerConfig.rejectAll }}\n </button>\n\n <button\n v-if=\"bannerConfig.customize\"\n type=\"button\"\n class=\"consent-banner__btn consent-banner__btn--customize\"\n @click=\"handleCustomize\"\n >\n {{ bannerConfig.customize }}\n </button>\n\n <button\n type=\"button\"\n class=\"consent-banner__btn consent-banner__btn--accept\"\n @click=\"handleAccept\"\n >\n {{ bannerConfig.acceptAll }}\n </button>\n </div>\n </div>\n </Transition>\n </Teleport>\n</template>\n\n<style>\n.consent-banner {\n position: fixed;\n left: 0;\n right: 0;\n z-index: 9999;\n padding: 1rem;\n background: var(--consent-bg, #ffffff);\n color: var(--consent-text, #1a1a1a);\n box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);\n font-family: var(--consent-font, system-ui, -apple-system, sans-serif);\n}\n\n.consent-banner--bottom {\n bottom: 0;\n}\n\n.consent-banner--top {\n top: 0;\n box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);\n}\n\n.consent-banner--center {\n top: 50%;\n left: 50%;\n right: auto;\n transform: translate(-50%, -50%);\n max-width: 500px;\n border-radius: 8px;\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);\n}\n\n.consent-banner__content {\n max-width: 1200px;\n margin: 0 auto;\n}\n\n.consent-banner__title {\n margin: 0 0 0.5rem;\n font-size: 1.125rem;\n font-weight: 600;\n}\n\n.consent-banner__message {\n margin: 0 0 1rem;\n font-size: 0.875rem;\n line-height: 1.5;\n color: var(--consent-text-secondary, #666666);\n}\n\n.consent-banner__privacy-link {\n color: var(--consent-link, #0066cc);\n text-decoration: underline;\n}\n\n.consent-banner__actions {\n display: flex;\n flex-wrap: wrap;\n gap: 0.5rem;\n justify-content: flex-end;\n max-width: 1200px;\n margin: 0 auto;\n}\n\n.consent-banner__btn {\n padding: 0.5rem 1rem;\n font-size: 0.875rem;\n font-weight: 500;\n border: none;\n border-radius: 4px;\n cursor: pointer;\n transition:\n background-color 0.2s,\n opacity 0.2s;\n}\n\n.consent-banner__btn:hover {\n opacity: 0.9;\n}\n\n.consent-banner__btn--accept {\n background: var(--consent-btn-accept-bg, #0066cc);\n color: var(--consent-btn-accept-text, #ffffff);\n}\n\n.consent-banner__btn--reject {\n background: var(--consent-btn-reject-bg, #e0e0e0);\n color: var(--consent-btn-reject-text, #1a1a1a);\n}\n\n.consent-banner__btn--customize {\n background: transparent;\n color: var(--consent-link, #0066cc);\n border: 1px solid currentColor;\n}\n\n/* Transitions */\n.consent-banner-enter-active,\n.consent-banner-leave-active {\n transition:\n transform 0.3s ease,\n opacity 0.3s ease;\n}\n\n.consent-banner--bottom.consent-banner-enter-from,\n.consent-banner--bottom.consent-banner-leave-to {\n transform: translateY(100%);\n opacity: 0;\n}\n\n.consent-banner--top.consent-banner-enter-from,\n.consent-banner--top.consent-banner-leave-to {\n transform: translateY(-100%);\n opacity: 0;\n}\n\n.consent-banner--center.consent-banner-enter-from,\n.consent-banner--center.consent-banner-leave-to {\n transform: translate(-50%, -50%) scale(0.9);\n opacity: 0;\n}\n\n/* Dark mode support */\n@media (prefers-color-scheme: dark) {\n .consent-banner {\n --consent-bg: #1a1a1a;\n --consent-text: #ffffff;\n --consent-text-secondary: #a0a0a0;\n --consent-btn-reject-bg: #333333;\n --consent-btn-reject-text: #ffffff;\n }\n}\n\n/* Mobile responsiveness */\n@media (max-width: 640px) {\n .consent-banner__actions {\n flex-direction: column;\n }\n\n .consent-banner__btn {\n width: 100%;\n justify-content: center;\n }\n}\n</style>\n","import type { App, Plugin } from \"vue\";\nimport type { ConsentConfig } from \"../core/types\";\nimport { ConsentManager, createConsentManager } from \"../core/consent-manager\";\nimport ConsentBanner from \"./ConsentBanner.vue\";\n\n/**\n * Vue plugin options\n */\nexport interface ConsentPluginOptions extends ConsentConfig {\n /** Auto-initialize on plugin install */\n autoInit?: boolean;\n}\n\n/**\n * Symbol for injection\n */\nexport const CONSENT_MANAGER_KEY = Symbol(\"consentManager\");\n\n/**\n * Create Vue plugin for cookie consent\n *\n * @example\n * ```ts\n * import { createApp } from 'vue';\n * import { createConsentPlugin } from '@structured-world/vue-privacy/vue';\n *\n * const app = createApp(App);\n * app.use(createConsentPlugin({\n * gaId: 'G-XXXXXXXXXX',\n * autoInit: true,\n * }));\n * ```\n */\nexport function createConsentPlugin(options: ConsentPluginOptions = {}): Plugin {\n const { autoInit = true, ...config } = options;\n\n return {\n install(app: App) {\n const manager = createConsentManager(config);\n\n // Provide manager for injection\n app.provide(\"consentManager\", manager);\n app.provide(CONSENT_MANAGER_KEY, manager);\n\n // Register global component\n app.component(\"ConsentBanner\", ConsentBanner);\n\n // Auto-initialize if requested\n if (autoInit) {\n // Wait for app to mount, then initialize\n const originalMount = app.mount.bind(app);\n app.mount = (rootContainer, ...args) => {\n const result = originalMount(rootContainer, ...args);\n\n // Initialize after mount\n manager.init().catch((err) => {\n console.error(\"[@structured-world/vue-privacy] Failed to initialize:\", err);\n });\n\n return result;\n };\n }\n },\n };\n}\n\n/**\n * Composable to access consent manager\n *\n * @example\n * ```vue\n * <script setup>\n * import { useConsent } from '@structured-world/vue-privacy/vue';\n *\n * const { acceptAll, rejectAll, hasConsent } = useConsent();\n * </script>\n * ```\n */\nexport function useConsent() {\n const manager = inject<ConsentManager>(\"consentManager\");\n\n if (!manager) {\n throw new Error(\n \"[@structured-world/vue-privacy] useConsent() called without plugin. \" +\n \"Did you forget to app.use(createConsentPlugin())?\"\n );\n }\n\n return {\n /** Accept all cookies */\n acceptAll: () => manager.acceptAll(),\n /** Reject all non-essential cookies */\n rejectAll: () => manager.rejectAll(),\n /** Save custom preferences */\n savePreferences: (categories: Parameters<typeof manager.savePreferences>[0]) =>\n manager.savePreferences(categories),\n /** Get current consent state */\n getConsent: () => manager.getConsent(),\n /** Check if user has made a consent choice */\n hasConsent: () => manager.hasConsent(),\n /** Reset consent and show banner again */\n resetConsent: () => manager.resetConsent(),\n /** Track a page view manually (for SPA navigation) */\n trackPageView: (path: string, title?: string) => manager.trackPageView(path, title),\n /** Check if user is detected as EU */\n isEUUser: () => manager.isEUUser(),\n /** Get geo-detection result (country, method, isEU) */\n getGeoResult: () => manager.getGeoResult(),\n /** Get the underlying manager instance */\n manager,\n };\n}\n\n// Need to import inject for useConsent\nimport { inject } from \"vue\";\n\n// Re-export component\nexport { ConsentBanner };\n\n// Re-export types\nexport type { ConsentConfig, ConsentManager };\n"],"names":["_createBlock","_Teleport","_createVNode","_Transition","_createElementBlock","_normalizeClass","_createElementVNode","_toDisplayString","ConsentBanner"],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@structured-world/vue-privacy",
3
- "version": "1.1.2",
4
- "description": "Privacy-first consent & analytics for Vue 3, Nuxt 3, VitePress, and Quasar. GDPR/CCPA compliant with Google Consent Mode v2.",
3
+ "version": "1.2.1",
4
+ "description": "Add Google Analytics (GA4) to Vue 3, VitePress, and Quasar with one line of code. Works with Nuxt 3 via the Vue plugin. GDPR/CCPA compliant cookie consent with Google Consent Mode v2, EU auto-detection, and SPA page tracking.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Dmitry Prudnikov <mail@polaz.com>",
7
7
  "packageManager": "yarn@4.12.0",
@@ -43,18 +43,26 @@
43
43
  "url": "https://github.com/structured-world/vue-privacy/issues"
44
44
  },
45
45
  "keywords": [
46
+ "google-analytics",
47
+ "ga4",
48
+ "gtag",
49
+ "google-tag-manager",
50
+ "analytics",
51
+ "vue",
52
+ "vue3",
53
+ "vitepress",
54
+ "nuxt",
55
+ "quasar",
46
56
  "cookie-consent",
47
57
  "gdpr",
48
58
  "ccpa",
49
59
  "google-consent-mode",
50
60
  "consent-mode-v2",
51
- "vue",
52
- "vue3",
53
- "quasar",
54
- "vitepress",
55
- "privacy",
56
61
  "cookie-banner",
57
- "eu-cookie-law"
62
+ "eu-cookie-law",
63
+ "privacy",
64
+ "spa-tracking",
65
+ "page-view"
58
66
  ],
59
67
  "engines": {
60
68
  "node": ">=18"
@@ -77,23 +85,29 @@
77
85
  "semantic-release": "semantic-release"
78
86
  },
79
87
  "peerDependencies": {
80
- "vue": "^3.3.0"
88
+ "vue": "^3.3.0",
89
+ "vue-router": "^4.0.0"
81
90
  },
82
91
  "peerDependenciesMeta": {
83
92
  "vue": {
84
93
  "optional": true
94
+ },
95
+ "vue-router": {
96
+ "optional": true
85
97
  }
86
98
  },
87
99
  "devDependencies": {
88
100
  "@eslint/js": "^9.0.0",
89
101
  "@semantic-release/changelog": "^6.0.0",
90
102
  "@semantic-release/git": "^10.0.0",
103
+ "@types/jsdom": "^27",
91
104
  "@types/node": "^22.0.0",
92
105
  "@vitejs/plugin-vue": "^5.0.0",
93
106
  "@vitest/coverage-v8": "^2.0.0",
94
107
  "conventional-changelog-conventionalcommits": "^8.0.0",
95
108
  "eslint": "^9.0.0",
96
109
  "eslint-plugin-vue": "^9.0.0",
110
+ "jsdom": "^27.4.0",
97
111
  "prettier": "^3.0.0",
98
112
  "semantic-release": "^25.0.0",
99
113
  "typescript": "^5.5.0",
@@ -104,6 +118,7 @@
104
118
  "vitest": "^2.0.0",
105
119
  "vue": "^3.5.0",
106
120
  "vue-eslint-parser": "^9.0.0",
121
+ "vue-router": "^4",
107
122
  "vue-tsc": "^2.0.0"
108
123
  }
109
124
  }