cookie-app 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +417 -0
- package/dist/index.d.mts +357 -0
- package/dist/index.d.ts +357 -0
- package/dist/index.js +722 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +687 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +65 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/CookieConsent.tsx","../src/defaults.ts","../src/use-consent.ts","../src/consent-mode.ts"],"sourcesContent":["'use client';\r\n\r\nimport React, { useState, useEffect, useCallback, useRef } from 'react';\r\nimport type { CookieConsentProps, ConsentLevel } from './types';\r\nimport {\r\n DEFAULT_TEXTS,\r\n STORAGE_KEY,\r\n STORAGE_DATE_KEY,\r\n CONSENT_CHANGE_EVENT,\r\n DEFAULT_BRAND_COLOR,\r\n DEFAULT_EXPIRY_DAYS,\r\n DEFAULT_WAIT_FOR_UPDATE,\r\n} from './defaults';\r\n\r\n// ─── Helpers ───\r\n\r\nfunction detectLanguage(): 'fr' | 'en' {\r\n if (typeof navigator === 'undefined') return 'fr';\r\n const lang = navigator.language?.substring(0, 2);\r\n return lang === 'en' ? 'en' : 'fr';\r\n}\r\n\r\nfunction isExpired(expiryDays: number): boolean {\r\n try {\r\n const d = localStorage.getItem(STORAGE_DATE_KEY);\r\n if (!d) return true;\r\n const age = (Date.now() - parseInt(d, 10)) / (1000 * 60 * 60 * 24);\r\n return age > expiryDays;\r\n } catch {\r\n return true;\r\n }\r\n}\r\n\r\nfunction getStoredConsent(expiryDays: number): ConsentLevel | null {\r\n try {\r\n const stored = localStorage.getItem(STORAGE_KEY) as ConsentLevel | null;\r\n if (stored && !isExpired(expiryDays)) return stored;\r\n // Expired — clean up\r\n if (stored) {\r\n localStorage.removeItem(STORAGE_KEY);\r\n localStorage.removeItem(STORAGE_DATE_KEY);\r\n }\r\n return null;\r\n } catch {\r\n return null;\r\n }\r\n}\r\n\r\n// ─── Injected CSS (hover, focus, responsive, glassmorphism) ───\r\n\r\nfunction buildInjectCss(brandColor: string, glassmorphism: boolean, customCss: string): string {\r\n return `\r\n#loi25-banner *{box-sizing:border-box;margin:0;padding:0;}\r\n#loi25-banner button{cursor:pointer;transition:transform .15s,opacity .15s;}\r\n#loi25-banner button:hover{transform:translateY(-1px);opacity:.9;}\r\n#loi25-banner button:focus-visible,#loi25-banner a:focus-visible{outline:2px solid ${brandColor};outline-offset:2px;}\r\n${glassmorphism ? '#loi25-banner.loi25-glass{backdrop-filter:blur(16px) saturate(1.8);-webkit-backdrop-filter:blur(16px) saturate(1.8);}' : ''}\r\n#loi25-reconsent{transition:transform .2s,opacity .3s;}\r\n#loi25-reconsent:hover{transform:scale(1.1)!important;}\r\n@media(max-width:600px){\r\n #loi25-banner .loi25-inner{padding:16px!important;}\r\n #loi25-banner .loi25-btns{flex-direction:column!important;}\r\n #loi25-banner .loi25-btns button{width:100%!important;}\r\n}\r\n${customCss}`.trim();\r\n}\r\n\r\n// ─── Theme colors (matches WordPress plugin exactly) ───\r\n\r\nfunction getThemeColors(theme: 'light' | 'dark', glassmorphism: boolean) {\r\n const dk = theme === 'dark';\r\n return {\r\n bg: dk\r\n ? `rgba(24,24,27,${glassmorphism ? '.75' : '1'})`\r\n : `rgba(255,255,255,${glassmorphism ? '.8' : '1'})`,\r\n text: dk ? '#e4e4e7' : '#1e293b',\r\n muted: dk ? '#a1a1aa' : '#64748b',\r\n border: dk ? '#3f3f46' : '#e2e8f0',\r\n btnBg: dk ? '#27272a' : '#f1f5f9',\r\n btnText: dk ? '#e4e4e7' : '#334155',\r\n };\r\n}\r\n\r\n// ─── Main Component ───\r\n\r\n/**\r\n * Quebec Law 25 cookie consent banner.\r\n *\r\n * Drop-in React component with script blocking, Google Consent Mode v2,\r\n * 3 banner styles (bar / popup / corner), bilingual support, and smooth\r\n * animations. Zero external dependencies.\r\n *\r\n * **Google Consent Mode v2 compliance** requires a synchronous inline\r\n * `<script>` in `<head>` that sets consent defaults **before** Google tags\r\n * load. Use `getConsentModeScript()` for this. The `<CookieConsent>`\r\n * component then handles the `consent('update', ...)` calls when the user\r\n * interacts with the banner.\r\n *\r\n * @example\r\n * ```tsx\r\n * // app/layout.tsx (Next.js 15 App Router)\r\n * import { CookieConsent, getConsentModeScript } from 'cookie-app';\r\n *\r\n * export default function RootLayout({ children }) {\r\n * return (\r\n * <html lang=\"fr\">\r\n * <head>\r\n * // 1. Consent defaults — MUST come before the Google tag\r\n * <script dangerouslySetInnerHTML={{ __html: getConsentModeScript() }} />\r\n * // 2. Google tag (gtag.js)\r\n * <script async src=\"https://www.googletagmanager.com/gtag/js?id=G-XXXXX\" />\r\n * </head>\r\n * <body>\r\n * {children}\r\n * // 3. Consent banner — handles consent('update') on user choice\r\n * <CookieConsent\r\n * lang=\"auto\"\r\n * style=\"popup\"\r\n * theme=\"dark\"\r\n * glassmorphism\r\n * consentMode\r\n * adsDataRedaction\r\n * urlPassthrough\r\n * privacyUrl=\"/privacy\"\r\n * onConsent={(level) => console.log('Consent:', level)}\r\n * />\r\n * </body>\r\n * </html>\r\n * );\r\n * }\r\n * ```\r\n */\r\nexport function CookieConsent({\r\n lang = 'fr',\r\n position = 'bottom',\r\n theme = 'light',\r\n style = 'bar',\r\n glassmorphism = false,\r\n privacyUrl = '/politique-de-confidentialite',\r\n poweredBy = false,\r\n brandColor = DEFAULT_BRAND_COLOR,\r\n expiryDays = DEFAULT_EXPIRY_DAYS,\r\n showReconsent = true,\r\n animation = 'slide',\r\n showIcon = true,\r\n customCss = '',\r\n textsFr,\r\n textsEn,\r\n onConsent,\r\n consentMode = false,\r\n adsDataRedaction = false,\r\n urlPassthrough = false,\r\n consentModeRegion,\r\n waitForUpdate = DEFAULT_WAIT_FOR_UPDATE,\r\n scripts = '',\r\n reloadOnConsent = false,\r\n}: CookieConsentProps) {\r\n const [mounted, setMounted] = useState(false);\r\n const [showBanner, setShowBanner] = useState(false);\r\n const [isVisible, setIsVisible] = useState(false);\r\n const [consent, setConsentState] = useState<ConsentLevel | null>(null);\r\n const scriptsInjectedRef = useRef(false);\r\n const consentModeInitRef = useRef(false);\r\n\r\n // ─── Resolve language ───\r\n const resolvedLang = lang === 'auto' ? (mounted ? detectLanguage() : 'fr') : lang;\r\n\r\n // ─── Resolve texts ───\r\n const defaults = DEFAULT_TEXTS[resolvedLang] || DEFAULT_TEXTS.fr;\r\n const customTexts = resolvedLang === 'fr' ? textsFr : textsEn;\r\n const texts = {\r\n title: customTexts?.title || defaults.title,\r\n message: customTexts?.message || defaults.message,\r\n accept: customTexts?.accept || defaults.accept,\r\n reject: customTexts?.reject || defaults.reject,\r\n privacy: customTexts?.privacy || defaults.privacy,\r\n powered: customTexts?.powered || defaults.powered,\r\n };\r\n\r\n // ─── Theme colors ───\r\n const colors = getThemeColors(theme, glassmorphism);\r\n\r\n // ─── Initialize on mount ───\r\n useEffect(() => {\r\n setMounted(true);\r\n const stored = getStoredConsent(expiryDays);\r\n setConsentState(stored);\r\n if (!stored) {\r\n setShowBanner(true);\r\n }\r\n }, [expiryDays]);\r\n\r\n // ─── Sync with external consent changes (e.g. useConsent().resetConsent) ───\r\n useEffect(() => {\r\n if (!mounted) return;\r\n const handler = () => {\r\n const stored = getStoredConsent(expiryDays);\r\n setConsentState(stored);\r\n if (!stored && !showBanner) {\r\n setIsVisible(false);\r\n setShowBanner(true);\r\n }\r\n };\r\n window.addEventListener(CONSENT_CHANGE_EVENT, handler);\r\n window.addEventListener('storage', handler);\r\n return () => {\r\n window.removeEventListener(CONSENT_CHANGE_EVENT, handler);\r\n window.removeEventListener('storage', handler);\r\n };\r\n }, [mounted, expiryDays, showBanner]);\r\n\r\n // ─── Animate banner in ───\r\n useEffect(() => {\r\n if (!showBanner || !mounted) return;\r\n\r\n // Double rAF — matches the WordPress plugin's animation trigger\r\n const raf1 = requestAnimationFrame(() => {\r\n requestAnimationFrame(() => {\r\n setIsVisible(true);\r\n });\r\n });\r\n\r\n // Focus accept button for accessibility\r\n const timer = setTimeout(() => {\r\n const btn = document.getElementById('loi25-yes');\r\n if (btn) btn.focus();\r\n }, 500);\r\n\r\n return () => {\r\n cancelAnimationFrame(raf1);\r\n clearTimeout(timer);\r\n };\r\n }, [showBanner, mounted]);\r\n\r\n // ─── Google Consent Mode v2 ───\r\n useEffect(() => {\r\n if (!consentMode || !mounted) return;\r\n\r\n // Set up dataLayer and gtag\r\n const w = window as unknown as Record<string, unknown>;\r\n w.dataLayer = (w.dataLayer as unknown[]) || [];\r\n if (!w.gtag) {\r\n w.gtag = function gtag() {\r\n // eslint-disable-next-line prefer-rest-params\r\n (w.dataLayer as unknown[]).push(arguments);\r\n };\r\n }\r\n\r\n const gtag = w.gtag as (...args: unknown[]) => void;\r\n\r\n // Only set defaults once\r\n if (!consentModeInitRef.current) {\r\n consentModeInitRef.current = true;\r\n\r\n // Google requires defaults to ALWAYS start as 'denied' for tracking\r\n // types. Functional/security types default to 'granted'.\r\n const defaultConsent: Record<string, unknown> = {\r\n ad_storage: 'denied',\r\n ad_user_data: 'denied',\r\n ad_personalization: 'denied',\r\n analytics_storage: 'denied',\r\n functionality_storage: 'granted',\r\n personalization_storage: 'granted',\r\n security_storage: 'granted',\r\n wait_for_update: waitForUpdate,\r\n };\r\n\r\n // Scope defaults to specific regions if provided\r\n if (consentModeRegion && consentModeRegion.length > 0) {\r\n defaultConsent.region = consentModeRegion;\r\n }\r\n\r\n gtag('consent', 'default', defaultConsent);\r\n\r\n // Redact ad click identifiers when ad_storage is denied\r\n if (adsDataRedaction) {\r\n gtag('set', 'ads_data_redaction', true);\r\n }\r\n\r\n // Pass GCLID/DCLID through URL params when cookies are denied\r\n if (urlPassthrough) {\r\n gtag('set', 'url_passthrough', true);\r\n }\r\n\r\n // If the user previously granted consent, immediately update so\r\n // tags fire with full measurement data without waiting for the banner.\r\n if (consent === 'all') {\r\n gtag('consent', 'update', {\r\n ad_storage: 'granted',\r\n ad_user_data: 'granted',\r\n ad_personalization: 'granted',\r\n analytics_storage: 'granted',\r\n });\r\n }\r\n }\r\n }, [consentMode, consent, mounted, waitForUpdate, consentModeRegion, adsDataRedaction, urlPassthrough]);\r\n\r\n // ─── Script Vault: inject scripts when consent is 'all' ───\r\n useEffect(() => {\r\n if (!scripts || consent !== 'all' || !mounted || scriptsInjectedRef.current) return;\r\n scriptsInjectedRef.current = true;\r\n\r\n const tmp = document.createElement('div');\r\n tmp.innerHTML = scripts;\r\n const els = tmp.querySelectorAll('script');\r\n els.forEach((el) => {\r\n const ns = document.createElement('script');\r\n if (el.src) {\r\n ns.src = el.src;\r\n } else {\r\n ns.textContent = el.text || el.textContent || '';\r\n }\r\n Array.from(el.attributes).forEach((attr) => {\r\n if (attr.name !== 'src') ns.setAttribute(attr.name, attr.value);\r\n });\r\n document.head.appendChild(ns);\r\n });\r\n }, [scripts, consent, mounted]);\r\n\r\n // ─── Keyboard: Escape = Necessary Only ───\r\n useEffect(() => {\r\n if (!showBanner) return;\r\n const handler = (e: KeyboardEvent) => {\r\n if (e.key === 'Escape') {\r\n handleConsent('necessary');\r\n }\r\n };\r\n document.addEventListener('keydown', handler);\r\n return () => document.removeEventListener('keydown', handler);\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, [showBanner]);\r\n\r\n // ─── Handle consent ───\r\n const handleConsent = useCallback(\r\n (level: ConsentLevel) => {\r\n // Store in localStorage\r\n try {\r\n localStorage.setItem(STORAGE_KEY, level);\r\n localStorage.setItem(STORAGE_DATE_KEY, Date.now().toString());\r\n } catch {\r\n // Silently fail\r\n }\r\n\r\n setConsentState(level);\r\n setIsVisible(false); // Trigger exit animation\r\n\r\n // Update Google Consent Mode — always send the update for both\r\n // 'all' (granted) and 'necessary' (denied). This is critical for\r\n // the reconsent flow where a user revokes previously granted consent.\r\n if (consentMode) {\r\n const w = window as unknown as Record<string, unknown>;\r\n const gtag = w.gtag as ((...args: unknown[]) => void) | undefined;\r\n if (gtag) {\r\n const granted = level === 'all';\r\n gtag('consent', 'update', {\r\n ad_storage: granted ? 'granted' : 'denied',\r\n ad_user_data: granted ? 'granted' : 'denied',\r\n ad_personalization: granted ? 'granted' : 'denied',\r\n analytics_storage: granted ? 'granted' : 'denied',\r\n });\r\n }\r\n }\r\n\r\n // Fire callback\r\n onConsent?.(level);\r\n\r\n // Dispatch custom event for useConsent hook\r\n window.dispatchEvent(new Event(CONSENT_CHANGE_EVENT));\r\n\r\n // Remove banner after animation completes\r\n setTimeout(() => {\r\n setShowBanner(false);\r\n\r\n // Reload if scripts need to run from page start\r\n if (reloadOnConsent && level === 'all' && scripts) {\r\n window.location.reload();\r\n }\r\n }, 400);\r\n },\r\n [consentMode, onConsent, reloadOnConsent, scripts],\r\n );\r\n\r\n // ─── Handle reconsent ───\r\n const handleReconsent = useCallback(() => {\r\n try {\r\n localStorage.removeItem(STORAGE_KEY);\r\n localStorage.removeItem(STORAGE_DATE_KEY);\r\n } catch {\r\n // Silently fail\r\n }\r\n setConsentState(null);\r\n setIsVisible(false);\r\n setShowBanner(true);\r\n scriptsInjectedRef.current = false;\r\n window.dispatchEvent(new Event(CONSENT_CHANGE_EVENT));\r\n }, []);\r\n\r\n // ─── SSR guard ───\r\n if (!mounted) return null;\r\n\r\n // ─── Banner styles ───\r\n const getBannerStyle = (): React.CSSProperties => {\r\n const base: React.CSSProperties = {\r\n fontFamily:\r\n \"-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,sans-serif\",\r\n lineHeight: 1.5,\r\n boxSizing: 'border-box' as const,\r\n zIndex: 999999,\r\n };\r\n\r\n if (style === 'bar') {\r\n const borderSide = position === 'top' ? 'borderBottom' : 'borderTop';\r\n return {\r\n ...base,\r\n position: 'fixed' as const,\r\n left: 0,\r\n right: 0,\r\n ...(position === 'top' ? { top: 0 } : { bottom: 0 }),\r\n background: colors.bg,\r\n [borderSide]: `1px solid ${colors.border}`,\r\n padding: 0,\r\n color: colors.text,\r\n boxShadow: `0 ${position === 'top' ? '2' : '-2'}px 20px rgba(0,0,0,.1)`,\r\n transition:\r\n animation === 'slide'\r\n ? 'transform .4s cubic-bezier(.4,0,.2,1), opacity .4s ease'\r\n : 'opacity .5s ease',\r\n transform: isVisible\r\n ? 'translateY(0)'\r\n : animation === 'slide'\r\n ? `translateY(${position === 'bottom' ? '100%' : '-100%'})`\r\n : 'none',\r\n opacity: isVisible ? 1 : 0,\r\n };\r\n }\r\n\r\n if (style === 'popup') {\r\n return {\r\n ...base,\r\n position: 'fixed' as const,\r\n top: '50%',\r\n left: '50%',\r\n maxWidth: 480,\r\n width: 'calc(100% - 40px)',\r\n borderRadius: 16,\r\n padding: 0,\r\n background: colors.bg,\r\n color: colors.text,\r\n boxShadow: '0 25px 60px rgba(0,0,0,.2)',\r\n transition: 'transform .35s cubic-bezier(.4,0,.2,1), opacity .35s ease',\r\n transform: isVisible\r\n ? 'translate(-50%, -50%) scale(1)'\r\n : 'translate(-50%, -50%) scale(.9)',\r\n opacity: isVisible ? 1 : 0,\r\n };\r\n }\r\n\r\n // corner\r\n return {\r\n ...base,\r\n position: 'fixed' as const,\r\n ...(position === 'top' ? { top: 20 } : { bottom: 20 }),\r\n right: 20,\r\n maxWidth: 380,\r\n width: 'calc(100% - 40px)',\r\n borderRadius: 16,\r\n padding: 0,\r\n background: colors.bg,\r\n color: colors.text,\r\n boxShadow: '0 8px 30px rgba(0,0,0,.12)',\r\n border: `1px solid ${colors.border}`,\r\n transition:\r\n animation === 'slide'\r\n ? 'transform .4s cubic-bezier(.4,0,.2,1), opacity .4s ease'\r\n : 'opacity .5s ease',\r\n transform: isVisible\r\n ? 'translateX(0)'\r\n : animation === 'slide'\r\n ? 'translateX(120%)'\r\n : 'none',\r\n opacity: isVisible ? 1 : 0,\r\n };\r\n };\r\n\r\n return (\r\n <>\r\n {/* ─── Injected styles for hover, focus, responsive, glassmorphism ─── */}\r\n <style\r\n dangerouslySetInnerHTML={{\r\n __html: buildInjectCss(brandColor, glassmorphism, customCss),\r\n }}\r\n />\r\n\r\n {/* ─── Popup overlay ─── */}\r\n {showBanner && style === 'popup' && (\r\n <div\r\n id=\"loi25-overlay\"\r\n style={{\r\n position: 'fixed',\r\n top: 0,\r\n left: 0,\r\n right: 0,\r\n bottom: 0,\r\n zIndex: 999998,\r\n background: 'rgba(0,0,0,.4)',\r\n opacity: isVisible ? 1 : 0,\r\n transition: 'opacity .35s ease',\r\n }}\r\n />\r\n )}\r\n\r\n {/* ─── Consent Banner ─── */}\r\n {showBanner && (\r\n <div\r\n id=\"loi25-banner\"\r\n role=\"dialog\"\r\n aria-label={\r\n resolvedLang === 'fr' ? 'Consentement aux cookies' : 'Cookie consent'\r\n }\r\n aria-modal={style === 'popup' ? 'true' : undefined}\r\n className={glassmorphism ? 'loi25-glass' : undefined}\r\n style={getBannerStyle()}\r\n >\r\n <div className=\"loi25-inner\" style={{ padding: '24px 28px' }}>\r\n {/* Title */}\r\n <div\r\n style={{\r\n fontWeight: 700,\r\n fontSize: 17,\r\n marginBottom: 10,\r\n display: 'flex',\r\n alignItems: 'center',\r\n gap: 8,\r\n }}\r\n >\r\n {showIcon && <span style={{ fontSize: 22 }}>🍪</span>}\r\n {texts.title}\r\n </div>\r\n\r\n {/* Message */}\r\n <p\r\n style={{\r\n margin: '0 0 18px',\r\n color: colors.muted,\r\n fontSize: 14,\r\n lineHeight: 1.6,\r\n }}\r\n >\r\n {texts.message}\r\n </p>\r\n\r\n {/* Buttons */}\r\n <div\r\n className=\"loi25-btns\"\r\n style={{\r\n display: 'flex',\r\n flexWrap: 'wrap',\r\n gap: 10,\r\n alignItems: 'center',\r\n }}\r\n >\r\n <button\r\n id=\"loi25-yes\"\r\n type=\"button\"\r\n onClick={() => handleConsent('all')}\r\n style={{\r\n background: brandColor,\r\n color: '#fff',\r\n border: 'none',\r\n padding: '11px 24px',\r\n borderRadius: 8,\r\n fontWeight: 600,\r\n fontSize: 14,\r\n }}\r\n >\r\n {texts.accept}\r\n </button>\r\n <button\r\n id=\"loi25-no\"\r\n type=\"button\"\r\n onClick={() => handleConsent('necessary')}\r\n style={{\r\n background: colors.btnBg,\r\n color: colors.btnText,\r\n border: `1px solid ${colors.border}`,\r\n padding: '11px 24px',\r\n borderRadius: 8,\r\n fontWeight: 600,\r\n fontSize: 14,\r\n }}\r\n >\r\n {texts.reject}\r\n </button>\r\n </div>\r\n\r\n {/* Footer links */}\r\n <div\r\n style={{\r\n marginTop: 14,\r\n display: 'flex',\r\n flexWrap: 'wrap',\r\n gap: 12,\r\n alignItems: 'center',\r\n }}\r\n >\r\n <a\r\n href={privacyUrl}\r\n style={{\r\n color: colors.muted,\r\n fontSize: 12,\r\n textDecoration: 'underline',\r\n }}\r\n target=\"_blank\"\r\n rel=\"noopener noreferrer\"\r\n >\r\n {texts.privacy}\r\n </a>\r\n {poweredBy && (\r\n <a\r\n href=\"https://rayelsconsulting.com\"\r\n target=\"_blank\"\r\n rel=\"noopener noreferrer\"\r\n style={{\r\n color: colors.muted,\r\n fontSize: 11,\r\n marginLeft: 'auto',\r\n textDecoration: 'none',\r\n opacity: 0.6,\r\n }}\r\n >\r\n {texts.powered} Rayels\r\n </a>\r\n )}\r\n </div>\r\n </div>\r\n </div>\r\n )}\r\n\r\n {/* ─── Reconsent floating button ─── */}\r\n {!showBanner && consent !== null && showReconsent && (\r\n <button\r\n id=\"loi25-reconsent\"\r\n type=\"button\"\r\n onClick={handleReconsent}\r\n aria-label={\r\n resolvedLang === 'fr' ? 'Gérer les cookies' : 'Manage cookies'\r\n }\r\n style={{\r\n position: 'fixed',\r\n bottom: 20,\r\n left: 20,\r\n zIndex: 999998,\r\n width: 44,\r\n height: 44,\r\n borderRadius: '50%',\r\n border: 'none',\r\n background: brandColor,\r\n color: '#fff',\r\n fontSize: 20,\r\n cursor: 'pointer',\r\n boxShadow: '0 4px 12px rgba(0,0,0,.15)',\r\n display: 'flex',\r\n alignItems: 'center',\r\n justifyContent: 'center',\r\n }}\r\n >\r\n {showIcon ? '🍪' : '⚙️'}\r\n </button>\r\n )}\r\n </>\r\n );\r\n}\r\n","/** localStorage key for consent level ('all' | 'necessary'). */\r\nexport const STORAGE_KEY = 'loi25-consent';\r\n\r\n/** localStorage key for consent timestamp. */\r\nexport const STORAGE_DATE_KEY = 'loi25-consent-date';\r\n\r\n/** Custom event name dispatched when consent changes programmatically. */\r\nexport const CONSENT_CHANGE_EVENT = 'loi25-consent-change';\r\n\r\n/** Default brand color (blue). */\r\nexport const DEFAULT_BRAND_COLOR = '#1d4ed8';\r\n\r\n/** Default consent expiry in days. */\r\nexport const DEFAULT_EXPIRY_DAYS = 365;\r\n\r\n/** Default wait_for_update value in milliseconds for Google Consent Mode v2. */\r\nexport const DEFAULT_WAIT_FOR_UPDATE = 500;\r\n\r\n/** Default banner texts for both languages. */\r\nexport const DEFAULT_TEXTS = {\r\n fr: {\r\n title: 'Respect de votre vie privée',\r\n message:\r\n 'Ce site utilise des témoins (cookies) pour améliorer votre expérience. Conformément à la Loi 25 du Québec, nous demandons votre consentement.',\r\n accept: 'Tout accepter',\r\n reject: 'Nécessaires seulement',\r\n privacy: 'Politique de confidentialité',\r\n powered: 'Propulsé par',\r\n },\r\n en: {\r\n title: 'Your Privacy Matters',\r\n message:\r\n \"This website uses cookies to improve your experience. In compliance with Quebec's Law 25, we ask for your consent.\",\r\n accept: 'Accept All',\r\n reject: 'Necessary Only',\r\n privacy: 'Privacy Policy',\r\n powered: 'Powered by',\r\n },\r\n} as const;\r\n","'use client';\r\n\r\nimport { useSyncExternalStore, useCallback } from 'react';\r\nimport type { ConsentLevel, ConsentState } from './types';\r\nimport {\r\n STORAGE_KEY,\r\n STORAGE_DATE_KEY,\r\n CONSENT_CHANGE_EVENT,\r\n DEFAULT_EXPIRY_DAYS,\r\n} from './defaults';\r\n\r\n// ─── External store helpers ───\r\n\r\nfunction subscribe(callback: () => void): () => void {\r\n window.addEventListener('storage', callback);\r\n window.addEventListener(CONSENT_CHANGE_EVENT, callback);\r\n return () => {\r\n window.removeEventListener('storage', callback);\r\n window.removeEventListener(CONSENT_CHANGE_EVENT, callback);\r\n };\r\n}\r\n\r\nfunction getSnapshot(): string | null {\r\n try {\r\n return localStorage.getItem(STORAGE_KEY);\r\n } catch {\r\n return null;\r\n }\r\n}\r\n\r\nfunction getServerSnapshot(): string | null {\r\n return null;\r\n}\r\n\r\n// ─── Helpers ───\r\n\r\nfunction isExpired(expiryDays: number): boolean {\r\n try {\r\n const d = localStorage.getItem(STORAGE_DATE_KEY);\r\n if (!d) return true;\r\n const age = (Date.now() - parseInt(d, 10)) / (1000 * 60 * 60 * 24);\r\n return age > expiryDays;\r\n } catch {\r\n return true;\r\n }\r\n}\r\n\r\n// ─── Hook ───\r\n\r\n/**\r\n * React hook to read and manage cookie consent state.\r\n *\r\n * SSR-safe — returns `null` consent on the server.\r\n * Automatically syncs across tabs and with the `<CookieConsent>` component.\r\n *\r\n * @param expiryDays - Number of days before consent expires. Default: 365.\r\n *\r\n * @example\r\n * ```tsx\r\n * const { consent, hasConsent, resetConsent } = useConsent();\r\n *\r\n * if (hasConsent && consent === 'all') {\r\n * // User accepted all cookies\r\n * }\r\n * ```\r\n */\r\nexport function useConsent(expiryDays: number = DEFAULT_EXPIRY_DAYS): ConsentState {\r\n const raw = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);\r\n\r\n // Validate that consent hasn't expired\r\n const isValid = (() => {\r\n if (!raw) return false;\r\n if (typeof window === 'undefined') return false;\r\n return !isExpired(expiryDays);\r\n })();\r\n\r\n const consent: ConsentLevel | null = isValid ? (raw as ConsentLevel) : null;\r\n\r\n const resetConsent = useCallback(() => {\r\n try {\r\n localStorage.removeItem(STORAGE_KEY);\r\n localStorage.removeItem(STORAGE_DATE_KEY);\r\n } catch {\r\n // Silently fail if localStorage is unavailable\r\n }\r\n window.dispatchEvent(new Event(CONSENT_CHANGE_EVENT));\r\n }, []);\r\n\r\n const setConsent = useCallback((level: ConsentLevel) => {\r\n try {\r\n localStorage.setItem(STORAGE_KEY, level);\r\n localStorage.setItem(STORAGE_DATE_KEY, Date.now().toString());\r\n } catch {\r\n // Silently fail if localStorage is unavailable\r\n }\r\n window.dispatchEvent(new Event(CONSENT_CHANGE_EVENT));\r\n }, []);\r\n\r\n return {\r\n consent,\r\n hasConsent: consent !== null,\r\n resetConsent,\r\n setConsent,\r\n };\r\n}\r\n","import { STORAGE_KEY, STORAGE_DATE_KEY, DEFAULT_EXPIRY_DAYS } from './defaults';\r\n\r\n// ─── Types ───\r\n\r\n/** Options for the synchronous consent mode default script. */\r\nexport interface ConsentModeDefaults {\r\n /** Default state for advertising cookie storage. @default 'denied' */\r\n analytics_storage?: 'granted' | 'denied';\r\n /** Default state for analytics cookie storage. @default 'denied' */\r\n ad_storage?: 'granted' | 'denied';\r\n /** Default state for sending user data to Google for advertising. @default 'denied' */\r\n ad_user_data?: 'granted' | 'denied';\r\n /** Default state for personalized advertising. @default 'denied' */\r\n ad_personalization?: 'granted' | 'denied';\r\n /** Default state for functionality storage (e.g. language settings). @default 'granted' */\r\n functionality_storage?: 'granted' | 'denied';\r\n /** Default state for personalization storage (e.g. video recommendations). @default 'granted' */\r\n personalization_storage?: 'granted' | 'denied';\r\n /** Default state for security storage (e.g. authentication, fraud prevention). @default 'granted' */\r\n security_storage?: 'granted' | 'denied';\r\n /**\r\n * Milliseconds Google tags wait for a consent update before firing.\r\n * Required for async consent banners (React components always load async).\r\n * @default 500\r\n */\r\n wait_for_update?: number;\r\n /**\r\n * ISO 3166-2 region codes to scope the consent defaults.\r\n * Example: `['CA-QC']` for Quebec only.\r\n */\r\n region?: string[];\r\n /**\r\n * When `true` and `ad_storage` is denied, ad click identifiers in pings\r\n * are redacted and requests go through a cookieless domain.\r\n * @default false\r\n */\r\n ads_data_redaction?: boolean;\r\n /**\r\n * When `true`, passes ad click information (GCLID/DCLID) through URL\r\n * parameters across pages when `ad_storage` is denied.\r\n * @default false\r\n */\r\n url_passthrough?: boolean;\r\n /**\r\n * Number of days before stored consent expires.\r\n * Must match the `expiryDays` prop on `<CookieConsent>`.\r\n * @default 365\r\n */\r\n expiry_days?: number;\r\n}\r\n\r\n// ─── Script Generator ───\r\n\r\n/**\r\n * Returns a self-contained JavaScript string that sets Google Consent Mode v2\r\n * defaults **synchronously**. Place this in `<head>` **before** the Google tag\r\n * (gtag.js or GTM) so that consent defaults are visible when tags initialize.\r\n *\r\n * On returning visits where the user previously accepted all cookies, the\r\n * script also calls `consent('update', ...)` immediately so tags fire with\r\n * full measurement data without waiting for the React banner to hydrate.\r\n *\r\n * @example\r\n * ```tsx\r\n * // app/layout.tsx (Next.js App Router)\r\n * import { getConsentModeScript } from 'cookie-app';\r\n *\r\n * export default function RootLayout({ children }) {\r\n * return (\r\n * <html lang=\"fr\">\r\n * <head>\r\n * // Consent defaults — MUST come before the Google tag\r\n * <script dangerouslySetInnerHTML={{ __html: getConsentModeScript() }} />\r\n * // Google tag (gtag.js) goes AFTER the consent default\r\n * </head>\r\n * <body>\r\n * {children}\r\n * <CookieConsent consentMode />\r\n * </body>\r\n * </html>\r\n * );\r\n * }\r\n * ```\r\n */\r\nexport function getConsentModeScript(options?: ConsentModeDefaults): string {\r\n const opts = options ?? {};\r\n\r\n const adStorage = opts.ad_storage ?? 'denied';\r\n const adUserData = opts.ad_user_data ?? 'denied';\r\n const adPersonalization = opts.ad_personalization ?? 'denied';\r\n const analyticsStorage = opts.analytics_storage ?? 'denied';\r\n const functionalityStorage = opts.functionality_storage ?? 'granted';\r\n const personalizationStorage = opts.personalization_storage ?? 'granted';\r\n const securityStorage = opts.security_storage ?? 'granted';\r\n const waitForUpdate = opts.wait_for_update ?? 500;\r\n const region = opts.region;\r\n const adsDataRedaction = opts.ads_data_redaction ?? false;\r\n const urlPassthrough = opts.url_passthrough ?? false;\r\n const expiryDays = opts.expiry_days ?? DEFAULT_EXPIRY_DAYS;\r\n\r\n // Build the consent default object as a JSON-safe string\r\n const defaultObj: Record<string, unknown> = {\r\n ad_storage: adStorage,\r\n ad_user_data: adUserData,\r\n ad_personalization: adPersonalization,\r\n analytics_storage: analyticsStorage,\r\n functionality_storage: functionalityStorage,\r\n personalization_storage: personalizationStorage,\r\n security_storage: securityStorage,\r\n wait_for_update: waitForUpdate,\r\n };\r\n\r\n if (region && region.length > 0) {\r\n defaultObj.region = region;\r\n }\r\n\r\n const defaultJson = JSON.stringify(defaultObj);\r\n\r\n // Build optional gtag('set', ...) calls\r\n const setCalls: string[] = [];\r\n if (adsDataRedaction) {\r\n setCalls.push(\"gtag('set','ads_data_redaction',true);\");\r\n }\r\n if (urlPassthrough) {\r\n setCalls.push(\"gtag('set','url_passthrough',true);\");\r\n }\r\n\r\n // The inline script:\r\n // 1. Defines dataLayer + gtag\r\n // 2. Sets consent defaults (always denied for tracking types)\r\n // 3. Optionally sets ads_data_redaction / url_passthrough\r\n // 4. Checks localStorage for returning users and calls consent('update')\r\n return [\r\n // Define dataLayer and gtag\r\n `window.dataLayer=window.dataLayer||[];`,\r\n `function gtag(){dataLayer.push(arguments);}`,\r\n\r\n // Set consent defaults\r\n `gtag('consent','default',${defaultJson});`,\r\n\r\n // Optional set calls\r\n ...setCalls,\r\n\r\n // Check for returning user with stored consent\r\n `(function(){`,\r\n ` try{`,\r\n ` var c=localStorage.getItem(${JSON.stringify(STORAGE_KEY)});`,\r\n ` var d=localStorage.getItem(${JSON.stringify(STORAGE_DATE_KEY)});`,\r\n ` if(c&&d){`,\r\n ` var age=(Date.now()-parseInt(d,10))/(1000*60*60*24);`,\r\n ` if(age<=${expiryDays}&&c==='all'){`,\r\n ` gtag('consent','update',{`,\r\n ` 'ad_storage':'granted',`,\r\n ` 'ad_user_data':'granted',`,\r\n ` 'ad_personalization':'granted',`,\r\n ` 'analytics_storage':'granted'`,\r\n ` });`,\r\n ` }`,\r\n ` }`,\r\n ` }catch(e){}`,\r\n `})();`,\r\n ].join('\\n');\r\n}\r\n"],"mappings":"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","names":["gtag","useCallback","isExpired","useCallback"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cookie-app",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Quebec Law 25 (Bill 64) cookie consent for React & Next.js. Script blocking, Google Consent Mode v2, 3 banner styles, bilingual, zero dependencies.",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": {
|
|
11
|
+
"types": "./dist/index.d.mts",
|
|
12
|
+
"default": "./dist/index.mjs"
|
|
13
|
+
},
|
|
14
|
+
"require": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"default": "./dist/index.js"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist"
|
|
22
|
+
],
|
|
23
|
+
"sideEffects": false,
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsup",
|
|
26
|
+
"dev": "tsup --watch",
|
|
27
|
+
"example": "npm run dev --prefix example",
|
|
28
|
+
"prepublishOnly": "npm run build",
|
|
29
|
+
"typecheck": "tsc --noEmit"
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"react": ">=18.0.0",
|
|
33
|
+
"react-dom": ">=18.0.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/react": "^19.0.0",
|
|
37
|
+
"@types/react-dom": "^19.0.0",
|
|
38
|
+
"react": "^19.0.0",
|
|
39
|
+
"react-dom": "^19.0.0",
|
|
40
|
+
"tsup": "^8.0.0",
|
|
41
|
+
"typescript": "^5.7.0"
|
|
42
|
+
},
|
|
43
|
+
"keywords": [
|
|
44
|
+
"loi25",
|
|
45
|
+
"loi-25",
|
|
46
|
+
"quebec",
|
|
47
|
+
"cookie-consent",
|
|
48
|
+
"privacy",
|
|
49
|
+
"gdpr",
|
|
50
|
+
"law25",
|
|
51
|
+
"bill64",
|
|
52
|
+
"nextjs",
|
|
53
|
+
"react",
|
|
54
|
+
"consent-mode",
|
|
55
|
+
"google-consent-mode",
|
|
56
|
+
"script-blocking"
|
|
57
|
+
],
|
|
58
|
+
"author": "Rayels Consulting <info@rayelsconsulting.com>",
|
|
59
|
+
"license": "MIT",
|
|
60
|
+
"homepage": "https://rayelsconsulting.com/tools/loi25-wordpress-plugin",
|
|
61
|
+
"repository": {
|
|
62
|
+
"type": "git",
|
|
63
|
+
"url": "https://github.com/rayelsconsulting/cookie-app"
|
|
64
|
+
}
|
|
65
|
+
}
|