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 ADDED
@@ -0,0 +1,417 @@
1
+ # cookie-app
2
+
3
+ Quebec Law 25 (Bill 64) cookie consent for **React & Next.js 15**.
4
+
5
+ Script blocking, Google Consent Mode v2, 3 banner styles, bilingual (FR/EN), zero dependencies.
6
+
7
+ Converted from the [Loi 25 Quebec WordPress plugin](https://rayelsconsulting.com/tools/loi25-wordpress-plugin) by [Rayels Consulting](https://rayelsconsulting.com).
8
+
9
+ ---
10
+
11
+ ## Features
12
+
13
+ - **Zero config** — works out of the box with sensible defaults
14
+ - **Script Vault** — blocks tracking scripts until consent is granted
15
+ - **Google Consent Mode v2** — full compliance with all 7 consent types, `wait_for_update`, `ads_data_redaction`, `url_passthrough`, and region-scoped defaults
16
+ - **Synchronous head script** — `getConsentModeScript()` helper for correct tag ordering
17
+ - **3 banner styles** — full-width bar, centered popup, corner widget
18
+ - **Glassmorphism** — modern frosted glass effect
19
+ - **Bilingual** — French (default) and English with auto-detection
20
+ - **Custom text** — override every string in both languages
21
+ - **Brand color** — match your website's design
22
+ - **Consent expiry** — auto re-ask after configurable days
23
+ - **Re-consent button** — floating cookie button to change consent
24
+ - **Smooth animations** — slide or fade transitions
25
+ - **Custom CSS** — full styling control
26
+ - **Accessible** — keyboard navigation (Escape = reject), ARIA labels, focus management
27
+ - **SSR-safe** — works with Next.js 15 App Router and Server Components
28
+ - **TypeScript** — full type definitions included
29
+ - **Tiny** — zero external dependencies, under 10KB
30
+
31
+ ---
32
+
33
+ ## Installation
34
+
35
+ ```bash
36
+ npm install cookie-app
37
+ ```
38
+
39
+ ```bash
40
+ yarn add cookie-app
41
+ ```
42
+
43
+ ```bash
44
+ pnpm add cookie-app
45
+ ```
46
+
47
+ ---
48
+
49
+ ## Quick Start
50
+
51
+ ### Next.js 15 (App Router)
52
+
53
+ ```tsx
54
+ // app/layout.tsx
55
+ import { CookieConsent } from 'cookie-app';
56
+
57
+ export default function RootLayout({
58
+ children,
59
+ }: {
60
+ children: React.ReactNode;
61
+ }) {
62
+ return (
63
+ <html lang="fr">
64
+ <body>
65
+ {children}
66
+ <CookieConsent />
67
+ </body>
68
+ </html>
69
+ );
70
+ }
71
+ ```
72
+
73
+ That's it! The banner appears automatically for new visitors with French defaults.
74
+
75
+ ---
76
+
77
+ ## Google Consent Mode v2 (Compliant Setup)
78
+
79
+ For full compliance with the [official Google documentation](https://developers.google.com/tag-platform/security/guides/consent), you need **two parts**:
80
+
81
+ 1. A **synchronous inline script** in `<head>` that sets consent defaults **before** Google tags load
82
+ 2. The **`<CookieConsent>`** component that sends `consent('update', ...)` when the user interacts
83
+
84
+ ### Recommended Setup
85
+
86
+ ```tsx
87
+ // app/layout.tsx (Next.js 15 App Router)
88
+ import { CookieConsent, getConsentModeScript } from 'cookie-app';
89
+
90
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
91
+ return (
92
+ <html lang="fr">
93
+ <head>
94
+ {/* 1. Consent defaults — MUST come before the Google tag */}
95
+ <script dangerouslySetInnerHTML={{ __html: getConsentModeScript() }} />
96
+
97
+ {/* 2. Google tag (gtag.js) — loads AFTER consent defaults are set */}
98
+ <script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXX" />
99
+ <script dangerouslySetInnerHTML={{ __html: `
100
+ window.dataLayer = window.dataLayer || [];
101
+ function gtag(){dataLayer.push(arguments);}
102
+ gtag('js', new Date());
103
+ gtag('config', 'G-XXXXX');
104
+ ` }} />
105
+ </head>
106
+ <body>
107
+ {children}
108
+
109
+ {/* 3. Consent banner — handles consent('update') on user choice */}
110
+ <CookieConsent
111
+ consentMode
112
+ adsDataRedaction
113
+ urlPassthrough
114
+ lang="auto"
115
+ style="popup"
116
+ theme="dark"
117
+ privacyUrl="/privacy"
118
+ />
119
+ </body>
120
+ </html>
121
+ );
122
+ }
123
+ ```
124
+
125
+ ### What this does
126
+
127
+ | Step | Timing | What happens |
128
+ |------|--------|--------------|
129
+ | `getConsentModeScript()` | Synchronous in `<head>` | Sets all consent types to `denied` with `wait_for_update: 500ms`. For returning users who previously accepted, immediately calls `consent('update', granted)`. |
130
+ | Google tag loads | After consent defaults | Tags see the default consent state and behave accordingly. |
131
+ | `<CookieConsent>` mounts | After hydration | Also sets defaults via `useEffect` as a fallback, and applies `ads_data_redaction` / `url_passthrough`. |
132
+ | User clicks Accept/Reject | On interaction | Calls `consent('update', ...)` with `granted` or `denied` for all 4 tracking types. |
133
+
134
+ ### `getConsentModeScript()` Options
135
+
136
+ ```tsx
137
+ getConsentModeScript({
138
+ // Override defaults (all default to 'denied' except functional types)
139
+ ad_storage: 'denied',
140
+ ad_user_data: 'denied',
141
+ ad_personalization: 'denied',
142
+ analytics_storage: 'denied',
143
+ functionality_storage: 'granted', // default: 'granted'
144
+ personalization_storage: 'granted', // default: 'granted'
145
+ security_storage: 'granted', // default: 'granted'
146
+
147
+ // How long Google tags wait for consent update (ms)
148
+ wait_for_update: 500, // default: 500
149
+
150
+ // Scope defaults to specific regions (ISO 3166-2)
151
+ region: ['CA-QC'],
152
+
153
+ // Redact ad click identifiers when ad_storage is denied
154
+ ads_data_redaction: true,
155
+
156
+ // Pass GCLID/DCLID through URL params when cookies denied
157
+ url_passthrough: true,
158
+
159
+ // Must match expiryDays on <CookieConsent>
160
+ expiry_days: 365,
161
+ });
162
+ ```
163
+
164
+ ### Consent Types Managed
165
+
166
+ | Consent Type | Default | Description |
167
+ |---|---|---|
168
+ | `ad_storage` | `denied` | Advertising cookie storage |
169
+ | `ad_user_data` | `denied` | User data for advertising |
170
+ | `ad_personalization` | `denied` | Personalized advertising |
171
+ | `analytics_storage` | `denied` | Analytics cookie storage |
172
+ | `functionality_storage` | `granted` | Functionality (e.g. language settings) |
173
+ | `personalization_storage` | `granted` | Personalization (e.g. video recommendations) |
174
+ | `security_storage` | `granted` | Security (e.g. authentication, fraud prevention) |
175
+
176
+ ---
177
+
178
+ ## Full Example
179
+
180
+ ```tsx
181
+ import { CookieConsent } from 'cookie-app';
182
+
183
+ <CookieConsent
184
+ lang="auto"
185
+ position="bottom"
186
+ theme="dark"
187
+ style="popup"
188
+ glassmorphism
189
+ brandColor="#7c3aed"
190
+ privacyUrl="/politique-de-confidentialite"
191
+ expiryDays={365}
192
+ showReconsent
193
+ showIcon
194
+ animation="slide"
195
+ consentMode
196
+ adsDataRedaction
197
+ urlPassthrough
198
+ consentModeRegion={['CA-QC']}
199
+ scripts={`
200
+ <!-- Google Analytics -->
201
+ <script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXX"></script>
202
+ <script>
203
+ window.dataLayer = window.dataLayer || [];
204
+ function gtag(){dataLayer.push(arguments);}
205
+ gtag('js', new Date());
206
+ gtag('config', 'G-XXXXXX');
207
+ </script>
208
+ `}
209
+ textsFr={{
210
+ title: 'Respect de votre vie privée',
211
+ message: 'Nous utilisons des cookies pour améliorer votre expérience.',
212
+ accept: 'Tout accepter',
213
+ reject: 'Refuser',
214
+ }}
215
+ textsEn={{
216
+ title: 'Your Privacy Matters',
217
+ message: 'We use cookies to improve your experience.',
218
+ accept: 'Accept All',
219
+ reject: 'Reject',
220
+ }}
221
+ onConsent={(level) => {
222
+ console.log('User chose:', level); // 'all' or 'necessary'
223
+ }}
224
+ />
225
+ ```
226
+
227
+ ---
228
+
229
+ ## Props
230
+
231
+ | Prop | Type | Default | Description |
232
+ |------|------|---------|-------------|
233
+ | `lang` | `'fr' \| 'en' \| 'auto'` | `'fr'` | Banner language. `'auto'` detects from `navigator.language`. |
234
+ | `position` | `'top' \| 'bottom'` | `'bottom'` | Banner position (bar & corner styles). |
235
+ | `theme` | `'light' \| 'dark'` | `'light'` | Color theme. |
236
+ | `style` | `'bar' \| 'popup' \| 'corner'` | `'bar'` | Banner display style. |
237
+ | `glassmorphism` | `boolean` | `false` | Frosted glass effect. |
238
+ | `privacyUrl` | `string` | `'/politique-de-confidentialite'` | Privacy policy link URL. |
239
+ | `poweredBy` | `boolean` | `false` | Show "Powered by Rayels" link. |
240
+ | `brandColor` | `string` | `'#1d4ed8'` | Accept button & reconsent button color. |
241
+ | `expiryDays` | `number` | `365` | Days before consent expires. |
242
+ | `showReconsent` | `boolean` | `true` | Show floating reconsent button. |
243
+ | `animation` | `'slide' \| 'fade'` | `'slide'` | Animation type. |
244
+ | `showIcon` | `boolean` | `true` | Show cookie emoji in banner & button. |
245
+ | `customCss` | `string` | `''` | Custom CSS targeting `#loi25-banner`. |
246
+ | `textsFr` | `ConsentTexts` | — | French text overrides. |
247
+ | `textsEn` | `ConsentTexts` | — | English text overrides. |
248
+ | `onConsent` | `(level: ConsentLevel) => void` | — | Callback when user consents. |
249
+ | `consentMode` | `boolean` | `false` | Enable Google Consent Mode v2. |
250
+ | `adsDataRedaction` | `boolean` | `false` | Redact ad click identifiers when `ad_storage` is denied. |
251
+ | `urlPassthrough` | `boolean` | `false` | Pass GCLID/DCLID through URL params when cookies denied. |
252
+ | `consentModeRegion` | `string[]` | — | ISO 3166-2 region codes to scope consent defaults (e.g. `['CA-QC']`). |
253
+ | `waitForUpdate` | `number` | `500` | Milliseconds Google tags wait for consent update before firing. |
254
+ | `scripts` | `string` | `''` | HTML of tracking scripts to block until consent. |
255
+ | `reloadOnConsent` | `boolean` | `false` | Reload page after accepting (for scripts that need page-start execution). |
256
+
257
+ ---
258
+
259
+ ## `useConsent` Hook
260
+
261
+ Read and manage consent state from any component. SSR-safe.
262
+
263
+ ```tsx
264
+ 'use client';
265
+
266
+ import { useConsent } from 'cookie-app';
267
+
268
+ export function AnalyticsLoader() {
269
+ const { consent, hasConsent, resetConsent, setConsent } = useConsent();
270
+
271
+ if (hasConsent && consent === 'all') {
272
+ return <p>Analytics are enabled.</p>;
273
+ }
274
+
275
+ return (
276
+ <div>
277
+ <p>No analytics consent.</p>
278
+ <button onClick={resetConsent}>Change cookie preferences</button>
279
+ </div>
280
+ );
281
+ }
282
+ ```
283
+
284
+ ### Return Values
285
+
286
+ | Property | Type | Description |
287
+ |----------|------|-------------|
288
+ | `consent` | `'all' \| 'necessary' \| null` | Current consent level. |
289
+ | `hasConsent` | `boolean` | Whether valid (non-expired) consent exists. |
290
+ | `resetConsent` | `() => void` | Clear consent and trigger banner. |
291
+ | `setConsent` | `(level: ConsentLevel) => void` | Set consent programmatically. |
292
+
293
+ ---
294
+
295
+ ## Script Vault
296
+
297
+ The killer feature. Paste your tracking scripts into the `scripts` prop and they are **automatically blocked** until the user clicks "Accept All".
298
+
299
+ ```tsx
300
+ <CookieConsent
301
+ scripts={`
302
+ <!-- Meta Pixel -->
303
+ <script>
304
+ !function(f,b,e,v,n,t,s){...}(window,document,'script',
305
+ 'https://connect.facebook.net/en_US/fbevents.js');
306
+ fbq('init', '123456789');
307
+ fbq('track', 'PageView');
308
+ </script>
309
+
310
+ <!-- Hotjar -->
311
+ <script>
312
+ (function(h,o,t,j,a,r){...})(window,document,'https://static.hotjar.com/c/hotjar-','.js?sv=');
313
+ </script>
314
+ `}
315
+ />
316
+ ```
317
+
318
+ Scripts are dynamically injected into `<head>` after consent. For scripts that must run at page load (e.g., GTM), set `reloadOnConsent` to trigger a page reload.
319
+
320
+ ---
321
+
322
+ ## Exported Constants
323
+
324
+ ```tsx
325
+ import {
326
+ STORAGE_KEY, // 'loi25-consent'
327
+ STORAGE_DATE_KEY, // 'loi25-consent-date'
328
+ CONSENT_CHANGE_EVENT, // 'loi25-consent-change'
329
+ DEFAULT_BRAND_COLOR, // '#1d4ed8'
330
+ DEFAULT_EXPIRY_DAYS, // 365
331
+ DEFAULT_WAIT_FOR_UPDATE,// 500
332
+ DEFAULT_TEXTS, // { fr: {...}, en: {...} }
333
+ } from 'cookie-app';
334
+ ```
335
+
336
+ ### Vanilla JS API
337
+
338
+ You can also check consent outside of React:
339
+
340
+ ```js
341
+ localStorage.getItem('loi25-consent'); // 'all' | 'necessary' | null
342
+ ```
343
+
344
+ ---
345
+
346
+ ## TypeScript
347
+
348
+ All types are exported:
349
+
350
+ ```tsx
351
+ import type {
352
+ ConsentLevel,
353
+ Language,
354
+ BannerStyle,
355
+ BannerPosition,
356
+ BannerTheme,
357
+ Animation,
358
+ ConsentTexts,
359
+ CookieConsentProps,
360
+ ConsentState,
361
+ ConsentModeDefaults,
362
+ } from 'cookie-app';
363
+ ```
364
+
365
+ ---
366
+
367
+ ## Banner Styles
368
+
369
+ ### Bar (default)
370
+ Full-width bar fixed to top or bottom of the viewport.
371
+
372
+ ### Popup
373
+ Centered modal with a semi-transparent overlay backdrop.
374
+
375
+ ### Corner
376
+ Compact widget anchored to the bottom-right (or top-right) corner.
377
+
378
+ ---
379
+
380
+ ## Migration from WordPress Plugin
381
+
382
+ If you're migrating from the WordPress plugin:
383
+
384
+ | WordPress Setting | React Prop |
385
+ |-------------------|------------|
386
+ | `rayels_loi25_lang` | `lang` |
387
+ | `rayels_loi25_position` | `position` |
388
+ | `rayels_loi25_theme` | `theme` |
389
+ | `rayels_loi25_style` | `style` |
390
+ | `rayels_loi25_glass` | `glassmorphism` |
391
+ | `rayels_loi25_privacy_url` | `privacyUrl` |
392
+ | `rayels_loi25_powered_by` | `poweredBy` |
393
+ | `rayels_loi25_brand_color` | `brandColor` |
394
+ | `rayels_loi25_consent_mode` | `consentMode` |
395
+ | `rayels_loi25_expiry` | `expiryDays` |
396
+ | `rayels_loi25_reconsent` | `showReconsent` |
397
+ | `rayels_loi25_animation` | `animation` |
398
+ | `rayels_loi25_show_icon` | `showIcon` |
399
+ | `rayels_loi25_custom_css` | `customCss` |
400
+ | `rayels_loi25_scripts_analytics` | `scripts` |
401
+ | `rayels_loi25_title_fr` / `_en` | `textsFr.title` / `textsEn.title` |
402
+ | `rayels_loi25_message_fr` / `_en` | `textsFr.message` / `textsEn.message` |
403
+ | `rayels_loi25_btn_accept_fr` / `_en` | `textsFr.accept` / `textsEn.accept` |
404
+ | `rayels_loi25_btn_reject_fr` / `_en` | `textsFr.reject` / `textsEn.reject` |
405
+
406
+ **What's different:**
407
+ - No admin settings page (configuration is via props)
408
+ - No dashboard stats widget (use `onConsent` callback to log to your own backend)
409
+ - No database table (use `onConsent` for server-side logging)
410
+ - No cache flushing (not needed in Next.js)
411
+ - localStorage keys are identical -- consent carries over from the WordPress version
412
+
413
+ ---
414
+
415
+ ## License
416
+
417
+ MIT -- [Rayels Consulting](https://rayelsconsulting.com), Montreal, Quebec.