@structured-world/vue-privacy 0.0.0-development → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -0
- package/dist/core/consent-manager.d.ts +9 -0
- package/dist/core/gtag.d.ts +9 -1
- package/dist/core/types.d.ts +6 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +35 -7
- package/dist/index.js.map +1 -1
- package/dist/vitepress/index.d.ts +7 -2
- package/dist/vitepress/index.js +34 -9
- package/dist/vitepress/index.js.map +1 -1
- package/dist/vue/index.d.ts +2 -0
- package/dist/vue/index.js +2 -0
- package/dist/vue/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -217,6 +217,35 @@ The banner uses CSS custom properties for theming:
|
|
|
217
217
|
|
|
218
218
|
Dark mode is automatically supported via `prefers-color-scheme`.
|
|
219
219
|
|
|
220
|
+
## Status & Roadmap
|
|
221
|
+
|
|
222
|
+
### Current (v1.0)
|
|
223
|
+
|
|
224
|
+
| Feature | Status |
|
|
225
|
+
|---------|--------|
|
|
226
|
+
| Consent banner component | Done |
|
|
227
|
+
| Google Consent Mode v2 | Done |
|
|
228
|
+
| GA4 integration | Done |
|
|
229
|
+
| EU geo-detection | Done |
|
|
230
|
+
| Vue 3 / VitePress / Quasar adapters | Done |
|
|
231
|
+
| Local storage (cookie/localStorage) | Done |
|
|
232
|
+
| Dark mode support | Done |
|
|
233
|
+
|
|
234
|
+
### Planned
|
|
235
|
+
|
|
236
|
+
| Feature | Description |
|
|
237
|
+
|---------|-------------|
|
|
238
|
+
| Preference center modal | Full-featured modal with category toggles (OneTrust-style) |
|
|
239
|
+
| Script blocking | Block 3rd-party scripts until consent given |
|
|
240
|
+
| Multi-language (i18n) | Built-in translations for 20+ languages |
|
|
241
|
+
| CCPA support | California Consumer Privacy Act compliance |
|
|
242
|
+
| Server-side storage | Optional backend via [vue-privacy-worker](https://github.com/structured-world/vue-privacy-worker) |
|
|
243
|
+
| Analytics dashboard | Opt-in rates, banner interactions (via privacy.structured.world) |
|
|
244
|
+
|
|
245
|
+
### Related Projects
|
|
246
|
+
|
|
247
|
+
- [vue-privacy-worker](https://github.com/structured-world/vue-privacy-worker) - Cloudflare Worker for server-side consent storage
|
|
248
|
+
|
|
220
249
|
## License
|
|
221
250
|
|
|
222
251
|
Apache 2.0 - see [LICENSE](LICENSE)
|
|
@@ -49,6 +49,15 @@ export declare class ConsentManager {
|
|
|
49
49
|
* Reset consent (show banner again)
|
|
50
50
|
*/
|
|
51
51
|
resetConsent(): void;
|
|
52
|
+
/**
|
|
53
|
+
* Track a page view manually (for SPA navigation).
|
|
54
|
+
* Skips sending if analytics consent has not been granted.
|
|
55
|
+
*/
|
|
56
|
+
trackPageView(path: string, title?: string): void;
|
|
57
|
+
/**
|
|
58
|
+
* Check if consent manager has been initialized
|
|
59
|
+
*/
|
|
60
|
+
isInitialized(): boolean;
|
|
52
61
|
/**
|
|
53
62
|
* Check if user is detected as EU
|
|
54
63
|
*/
|
package/dist/core/gtag.d.ts
CHANGED
|
@@ -32,10 +32,18 @@ export declare function updateConsent(signals: Partial<GoogleConsentSignals>): v
|
|
|
32
32
|
* @param gaId - Google Analytics measurement ID (G-XXXXXXXXXX)
|
|
33
33
|
*/
|
|
34
34
|
export declare function loadGtagScript(gaId: string): Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* Track a page view manually (for SPA navigation)
|
|
37
|
+
*
|
|
38
|
+
* @param path - Page path (e.g., '/docs/guide')
|
|
39
|
+
* @param title - Page title (defaults to document.title)
|
|
40
|
+
*/
|
|
41
|
+
export declare function trackPageView(path: string, title?: string): void;
|
|
35
42
|
/**
|
|
36
43
|
* Initialize Google Analytics with consent defaults
|
|
37
44
|
*
|
|
38
45
|
* @param gaId - Google Analytics measurement ID
|
|
39
46
|
* @param defaultDenied - Whether to default to denied consent (for EU users)
|
|
47
|
+
* @param sendPageView - Whether to send automatic page_view (false for SPA)
|
|
40
48
|
*/
|
|
41
|
-
export declare function initGoogleAnalytics(gaId: string, defaultDenied?: boolean): Promise<void>;
|
|
49
|
+
export declare function initGoogleAnalytics(gaId: string, defaultDenied?: boolean, sendPageView?: boolean): Promise<void>;
|
package/dist/core/types.d.ts
CHANGED
|
@@ -105,6 +105,12 @@ export interface ConsentConfig {
|
|
|
105
105
|
euDetection?: "auto" | "cloudflare" | "api" | "always" | "never";
|
|
106
106
|
/** Custom geo-detection provider */
|
|
107
107
|
geoDetector?: GeoDetector;
|
|
108
|
+
/**
|
|
109
|
+
* Whether to send automatic page_view on GA initialization.
|
|
110
|
+
* Set to false for SPA apps (VitePress, Vue Router) where you track navigation manually.
|
|
111
|
+
* @default true
|
|
112
|
+
*/
|
|
113
|
+
sendPageView?: boolean;
|
|
108
114
|
/** Consent version (changing this resets consent for all users) */
|
|
109
115
|
version?: string;
|
|
110
116
|
/** Callback when consent changes */
|
package/dist/index.d.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
export { ConsentManager, createConsentManager } from "./core/consent-manager";
|
|
19
19
|
export { getStoredConsent, storeConsent, clearConsent } from "./core/storage";
|
|
20
|
-
export { initGtag, setConsentDefaults, updateConsent, loadGtagScript, initGoogleAnalytics, categoriesToGoogleSignals, } from "./core/gtag";
|
|
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
23
|
export type { ConsentConfig, ConsentCategories, StoredConsent, GoogleConsentSignals, GeoDetector, GeoDetectionResult, BannerConfig, } from "./core/types";
|
package/dist/index.js
CHANGED
|
@@ -133,9 +133,7 @@ function loadGtagScript(gaId) {
|
|
|
133
133
|
resolve();
|
|
134
134
|
return;
|
|
135
135
|
}
|
|
136
|
-
if (document.querySelector(
|
|
137
|
-
`script[src*="googletagmanager.com/gtag/js?id=${gaId}"]`
|
|
138
|
-
)) {
|
|
136
|
+
if (document.querySelector(`script[src*="googletagmanager.com/gtag/js?id=${gaId}"]`)) {
|
|
139
137
|
resolve();
|
|
140
138
|
return;
|
|
141
139
|
}
|
|
@@ -147,7 +145,15 @@ function loadGtagScript(gaId) {
|
|
|
147
145
|
document.head.appendChild(script);
|
|
148
146
|
});
|
|
149
147
|
}
|
|
150
|
-
|
|
148
|
+
function trackPageView(path, title) {
|
|
149
|
+
if (typeof window === "undefined" || typeof window.gtag !== "function") return;
|
|
150
|
+
window.gtag("event", "page_view", {
|
|
151
|
+
page_path: path,
|
|
152
|
+
page_location: window.location.href,
|
|
153
|
+
page_title: title ?? document.title
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
async function initGoogleAnalytics(gaId, defaultDenied = true, sendPageView = true) {
|
|
151
157
|
initGtag();
|
|
152
158
|
if (defaultDenied) {
|
|
153
159
|
setConsentDefaults({
|
|
@@ -167,7 +173,9 @@ async function initGoogleAnalytics(gaId, defaultDenied = true) {
|
|
|
167
173
|
await loadGtagScript(gaId);
|
|
168
174
|
if (typeof window !== "undefined") {
|
|
169
175
|
window.gtag("js", /* @__PURE__ */ new Date());
|
|
170
|
-
window.gtag("config", gaId
|
|
176
|
+
window.gtag("config", gaId, {
|
|
177
|
+
send_page_view: sendPageView
|
|
178
|
+
});
|
|
171
179
|
}
|
|
172
180
|
}
|
|
173
181
|
class CloudflareGeoDetector {
|
|
@@ -361,7 +369,8 @@ class ConsentManager {
|
|
|
361
369
|
this.isEU = geoResult.isEU;
|
|
362
370
|
if (this.isEU) {
|
|
363
371
|
if (this.config.gaId) {
|
|
364
|
-
|
|
372
|
+
const sendPageView = this.config.sendPageView ?? true;
|
|
373
|
+
await initGoogleAnalytics(this.config.gaId, true, sendPageView);
|
|
365
374
|
}
|
|
366
375
|
(_a = this.showBannerCallback) == null ? void 0 : _a.call(this);
|
|
367
376
|
(_c = (_b = this.config).onBannerShow) == null ? void 0 : _c.call(_b);
|
|
@@ -381,7 +390,8 @@ class ConsentManager {
|
|
|
381
390
|
async applyConsent(categories) {
|
|
382
391
|
var _a, _b;
|
|
383
392
|
if (this.config.gaId) {
|
|
384
|
-
|
|
393
|
+
const sendPageView = this.config.sendPageView ?? true;
|
|
394
|
+
await initGoogleAnalytics(this.config.gaId, !categories.analytics, sendPageView);
|
|
385
395
|
}
|
|
386
396
|
const signals = categoriesToGoogleSignals(categories);
|
|
387
397
|
updateConsent(signals);
|
|
@@ -458,6 +468,23 @@ class ConsentManager {
|
|
|
458
468
|
(_a = this.showBannerCallback) == null ? void 0 : _a.call(this);
|
|
459
469
|
(_c = (_b = this.config).onBannerShow) == null ? void 0 : _c.call(_b);
|
|
460
470
|
}
|
|
471
|
+
/**
|
|
472
|
+
* Track a page view manually (for SPA navigation).
|
|
473
|
+
* Skips sending if analytics consent has not been granted.
|
|
474
|
+
*/
|
|
475
|
+
trackPageView(path, title) {
|
|
476
|
+
const stored = getStoredConsent(this.config);
|
|
477
|
+
if (stored && !stored.categories.analytics) {
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
trackPageView(path, title);
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Check if consent manager has been initialized
|
|
484
|
+
*/
|
|
485
|
+
isInitialized() {
|
|
486
|
+
return this.initialized;
|
|
487
|
+
}
|
|
461
488
|
/**
|
|
462
489
|
* Check if user is detected as EU
|
|
463
490
|
*/
|
|
@@ -491,6 +518,7 @@ export {
|
|
|
491
518
|
loadGtagScript,
|
|
492
519
|
setConsentDefaults,
|
|
493
520
|
storeConsent,
|
|
521
|
+
trackPageView,
|
|
494
522
|
updateConsent
|
|
495
523
|
};
|
|
496
524
|
//# sourceMappingURL=index.js.map
|
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 /** 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 (\n document.querySelector(\n `script[src*=\"googletagmanager.com/gtag/js?id=${gaId}\"]`,\n )\n ) {\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 = () =>\n reject(new Error(`Failed to load gtag.js for ${gaId}`));\n\n document.head.appendChild(script);\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 */\nexport async function initGoogleAnalytics(\n gaId: string,\n defaultDenied = 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 }\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} 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 ??\n 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 await initGoogleAnalytics(this.config.gaId, true);\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(\n categories: Omit<ConsentCategories, \"necessary\">,\n ): Promise<void> {\n // Initialize GA if configured\n if (this.config.gaId) {\n await initGoogleAnalytics(this.config.gaId, !categories.analytics);\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(\n categories: Partial<Omit<ConsentCategories, \"necessary\">>,\n ): 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 * 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(\n config: ConsentConfig = {},\n): ConsentManager {\n return new ConsentManager(config);\n}\n"],"names":["updateGoogleConsent"],"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 * 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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"}
|
|
@@ -2,7 +2,12 @@ import type { Theme } from "vitepress";
|
|
|
2
2
|
import type { ConsentConfig } from "../core/types";
|
|
3
3
|
import { createConsentPlugin, ConsentBanner } from "../vue/index";
|
|
4
4
|
/**
|
|
5
|
-
* VitePress theme enhancement for cookie consent
|
|
5
|
+
* VitePress theme enhancement for cookie consent with SPA page tracking
|
|
6
|
+
*
|
|
7
|
+
* Automatically:
|
|
8
|
+
* - Disables automatic page_view (SPA mode)
|
|
9
|
+
* - Tracks initial page view after mount
|
|
10
|
+
* - Watches Vue Router for SPA navigation and sends page_view events
|
|
6
11
|
*
|
|
7
12
|
* @example
|
|
8
13
|
* ```ts
|
|
@@ -29,7 +34,7 @@ export declare function enhanceWithConsent(theme: Theme, config: ConsentConfig):
|
|
|
29
34
|
* export default {
|
|
30
35
|
* extends: DefaultTheme,
|
|
31
36
|
* enhanceApp({ app }) {
|
|
32
|
-
* app.use(createConsentPlugin({ gaId: 'G-XXX' }));
|
|
37
|
+
* app.use(createConsentPlugin({ gaId: 'G-XXX', sendPageView: false }));
|
|
33
38
|
* },
|
|
34
39
|
* Layout() {
|
|
35
40
|
* return h(DefaultTheme.Layout, null, {
|
package/dist/vitepress/index.js
CHANGED
|
@@ -1,22 +1,47 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { inBrowser } from "vitepress";
|
|
2
|
+
import { nextTick, watch } from "vue";
|
|
3
|
+
import { CONSENT_MANAGER_KEY, ConsentBanner as _sfc_main } from "../vue/index.js";
|
|
4
|
+
import { createConsentPlugin, useConsent } from "../vue/index.js";
|
|
5
|
+
import { createConsentManager } from "../index.js";
|
|
3
6
|
function enhanceWithConsent(theme, config) {
|
|
4
7
|
return {
|
|
5
8
|
...theme,
|
|
6
9
|
enhanceApp(ctx) {
|
|
7
10
|
var _a;
|
|
8
11
|
(_a = theme.enhanceApp) == null ? void 0 : _a.call(theme, ctx);
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
);
|
|
12
|
+
const manager = createConsentManager({
|
|
13
|
+
...config,
|
|
14
|
+
sendPageView: false
|
|
15
|
+
});
|
|
16
|
+
ctx.app.provide("consentManager", manager);
|
|
17
|
+
ctx.app.provide(CONSENT_MANAGER_KEY, manager);
|
|
18
|
+
ctx.app.component("ConsentBanner", _sfc_main);
|
|
19
|
+
if (inBrowser) {
|
|
20
|
+
manager.init().then(() => {
|
|
21
|
+
nextTick(() => {
|
|
22
|
+
manager.trackPageView(window.location.pathname);
|
|
23
|
+
});
|
|
24
|
+
}).catch((err) => {
|
|
25
|
+
console.error("[@structured-world/vue-privacy] Failed to initialize:", err);
|
|
26
|
+
});
|
|
27
|
+
if (ctx.router) {
|
|
28
|
+
watch(
|
|
29
|
+
() => ctx.router.route.path,
|
|
30
|
+
(path) => {
|
|
31
|
+
nextTick(() => {
|
|
32
|
+
manager.trackPageView(path);
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
// Initial page view is tracked after init(); don't fire on watch setup
|
|
36
|
+
{ immediate: false }
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
15
40
|
}
|
|
16
41
|
};
|
|
17
42
|
}
|
|
18
43
|
export {
|
|
19
|
-
ConsentBanner,
|
|
44
|
+
_sfc_main as ConsentBanner,
|
|
20
45
|
createConsentPlugin,
|
|
21
46
|
enhanceWithConsent,
|
|
22
47
|
useConsent
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../../src/vitepress/index.ts"],"sourcesContent":["import type { Theme } from \"vitepress\";\nimport type { ConsentConfig } from \"../core/types\";\nimport { createConsentPlugin, ConsentBanner } from \"../vue/index\";\n\n/**\n * VitePress theme enhancement for cookie consent\n *\n * @example\n * ```ts\n * // docs/.vitepress/theme/index.ts\n * import DefaultTheme from 'vitepress/theme';\n * import { enhanceWithConsent } from '@structured-world/vue-privacy/vitepress';\n *\n * export default enhanceWithConsent(DefaultTheme, {\n * gaId: 'G-XXXXXXXXXX',\n * });\n * ```\n */\nexport function enhanceWithConsent(theme: Theme, config: ConsentConfig): Theme {\n return {\n ...theme,\n enhanceApp(ctx) {\n // Call original enhanceApp if exists\n theme.enhanceApp?.(ctx);\n\n //
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../../src/vitepress/index.ts"],"sourcesContent":["import type { Theme } from \"vitepress\";\nimport { inBrowser } from \"vitepress\";\nimport { watch, nextTick } from \"vue\";\nimport type { ConsentConfig } from \"../core/types\";\nimport { createConsentPlugin, ConsentBanner, CONSENT_MANAGER_KEY } from \"../vue/index\";\nimport { createConsentManager } from \"../core/consent-manager\";\n\n/**\n * VitePress theme enhancement for cookie consent with SPA page tracking\n *\n * Automatically:\n * - Disables automatic page_view (SPA mode)\n * - Tracks initial page view after mount\n * - Watches Vue Router for SPA navigation and sends page_view events\n *\n * @example\n * ```ts\n * // docs/.vitepress/theme/index.ts\n * import DefaultTheme from 'vitepress/theme';\n * import { enhanceWithConsent } from '@structured-world/vue-privacy/vitepress';\n *\n * export default enhanceWithConsent(DefaultTheme, {\n * gaId: 'G-XXXXXXXXXX',\n * });\n * ```\n */\nexport function enhanceWithConsent(theme: Theme, config: ConsentConfig): Theme {\n return {\n ...theme,\n enhanceApp(ctx) {\n // Call original enhanceApp if exists\n theme.enhanceApp?.(ctx);\n\n // VitePress is a SPA — always disable automatic page_view and track\n // navigation manually via router watch. User's sendPageView is ignored.\n const manager = createConsentManager({\n ...config,\n sendPageView: false,\n });\n\n // Provide manager for injection (same keys as createConsentPlugin)\n ctx.app.provide(\"consentManager\", manager);\n ctx.app.provide(CONSENT_MANAGER_KEY, manager);\n\n // Register global component\n ctx.app.component(\"ConsentBanner\", ConsentBanner);\n\n if (inBrowser) {\n // Initialize consent manager\n manager\n .init()\n .then(() => {\n // Track initial page view after init completes.\n // For EU users with denied consent, Google Consent Mode accepts\n // the event but does not store it until consent is granted.\n nextTick(() => {\n manager.trackPageView(window.location.pathname);\n });\n })\n .catch((err) => {\n console.error(\"[@structured-world/vue-privacy] Failed to initialize:\", err);\n });\n\n // Track subsequent SPA navigations via router\n if (ctx.router) {\n watch(\n () => ctx.router.route.path,\n (path: string) => {\n // Wait for Vue to update DOM (including document.title)\n nextTick(() => {\n manager.trackPageView(path);\n });\n },\n // Initial page view is tracked after init(); don't fire on watch setup\n { immediate: false }\n );\n }\n }\n },\n };\n}\n\n/**\n * VitePress composable for adding consent banner to layout\n *\n * @example\n * ```ts\n * // docs/.vitepress/theme/index.ts\n * import DefaultTheme from 'vitepress/theme';\n * import { createConsentPlugin, ConsentBanner } from '@structured-world/vue-privacy/vitepress';\n * import { h } from 'vue';\n *\n * export default {\n * extends: DefaultTheme,\n * enhanceApp({ app }) {\n * app.use(createConsentPlugin({ gaId: 'G-XXX', sendPageView: false }));\n * },\n * Layout() {\n * return h(DefaultTheme.Layout, null, {\n * 'layout-bottom': () => h(ConsentBanner),\n * });\n * },\n * };\n * ```\n */\nexport { createConsentPlugin, ConsentBanner };\n\n// Re-export useConsent for convenience\nexport { useConsent } from \"../vue/index\";\n\n// Re-export core types\nexport type { ConsentConfig } from \"../core/types\";\n"],"names":["ConsentBanner"],"mappings":";;;;;AA0BO,SAAS,mBAAmB,OAAc,QAA8B;AAC7E,SAAO;AAAA,IACL,GAAG;AAAA,IACH,WAAW,KAAK;;AAEd,kBAAM,eAAN,+BAAmB;AAInB,YAAM,UAAU,qBAAqB;AAAA,QACnC,GAAG;AAAA,QACH,cAAc;AAAA,MAAA,CACf;AAGD,UAAI,IAAI,QAAQ,kBAAkB,OAAO;AACzC,UAAI,IAAI,QAAQ,qBAAqB,OAAO;AAG5C,UAAI,IAAI,UAAU,iBAAiBA,SAAa;AAEhD,UAAI,WAAW;AAEb,gBACG,OACA,KAAK,MAAM;AAIV,mBAAS,MAAM;AACb,oBAAQ,cAAc,OAAO,SAAS,QAAQ;AAAA,UAChD,CAAC;AAAA,QACH,CAAC,EACA,MAAM,CAAC,QAAQ;AACd,kBAAQ,MAAM,yDAAyD,GAAG;AAAA,QAC5E,CAAC;AAGH,YAAI,IAAI,QAAQ;AACd;AAAA,YACE,MAAM,IAAI,OAAO,MAAM;AAAA,YACvB,CAAC,SAAiB;AAEhB,uBAAS,MAAM;AACb,wBAAQ,cAAc,IAAI;AAAA,cAC5B,CAAC;AAAA,YACH;AAAA;AAAA,YAEA,EAAE,WAAW,MAAA;AAAA,UAAM;AAAA,QAEvB;AAAA,MACF;AAAA,IACF;AAAA,EAAA;AAEJ;"}
|
package/dist/vue/index.d.ts
CHANGED
|
@@ -54,6 +54,8 @@ export declare function useConsent(): {
|
|
|
54
54
|
hasConsent: () => boolean;
|
|
55
55
|
/** Reset consent and show banner again */
|
|
56
56
|
resetConsent: () => void;
|
|
57
|
+
/** Track a page view manually (for SPA navigation) */
|
|
58
|
+
trackPageView: (path: string, title?: string) => void;
|
|
57
59
|
/** Check if user is detected as EU */
|
|
58
60
|
isEUUser: () => boolean | null;
|
|
59
61
|
/** Get the underlying manager instance */
|
package/dist/vue/index.js
CHANGED
|
@@ -160,6 +160,8 @@ function useConsent() {
|
|
|
160
160
|
hasConsent: () => manager.hasConsent(),
|
|
161
161
|
/** Reset consent and show banner again */
|
|
162
162
|
resetConsent: () => manager.resetConsent(),
|
|
163
|
+
/** Track a page view manually (for SPA navigation) */
|
|
164
|
+
trackPageView: (path, title) => manager.trackPageView(path, title),
|
|
163
165
|
/** Check if user is detected as EU */
|
|
164
166
|
isEUUser: () => manager.isEUUser(),
|
|
165
167
|
/** Get the underlying manager instance */
|
package/dist/vue/index.js.map
CHANGED
|
@@ -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 /** 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":"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"}
|
|
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":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@structured-world/vue-privacy",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Privacy-first consent & analytics for Vue 3, Nuxt 3, VitePress, and Quasar. GDPR/CCPA compliant with Google Consent Mode v2.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Dmitry Prudnikov <mail@polaz.com>",
|