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
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.
|