@consenti/ui 0.1.0 → 0.3.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 +548 -151
- package/dist/angular.d.cts +486 -111
- package/dist/angular.d.ts +486 -111
- package/dist/angular.js +1 -73
- package/dist/angular.js.map +1 -1
- package/dist/angular.mjs +1 -52
- package/dist/angular.mjs.map +1 -1
- package/dist/chunk-3EXCD24H.mjs +2 -0
- package/dist/{chunk-ARALKXMB.mjs.map → chunk-3EXCD24H.mjs.map} +1 -1
- package/dist/chunk-J4BKXUD6.mjs +2 -0
- package/dist/chunk-J4BKXUD6.mjs.map +1 -0
- package/dist/hmac-UKMRMKN6.mjs +2 -0
- package/dist/{hmac-N6H7F2K2.mjs.map → hmac-UKMRMKN6.mjs.map} +1 -1
- package/dist/http-AQVVUCX6.mjs +2 -0
- package/dist/index.css +1 -1
- package/dist/index.d.ts +806 -103
- package/dist/index.mjs +1 -2220
- package/dist/index.mjs.map +1 -1
- package/dist/index.umd.js +1 -2304
- package/dist/index.umd.js.map +1 -1
- package/dist/profiles-OEZAKEWL.mjs +2 -0
- package/dist/profiles-OEZAKEWL.mjs.map +1 -0
- package/dist/react.d.cts +470 -89
- package/dist/react.d.ts +470 -89
- package/dist/react.js +1 -60
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +1 -39
- package/dist/react.mjs.map +1 -1
- package/dist/testing.js +1 -94
- package/dist/testing.js.map +1 -1
- package/dist/testing.mjs +1 -73
- package/dist/testing.mjs.map +1 -1
- package/dist/vue.d.cts +473 -82
- package/dist/vue.d.ts +473 -82
- package/dist/vue.js +1 -72
- package/dist/vue.js.map +1 -1
- package/dist/vue.mjs +1 -51
- package/dist/vue.mjs.map +1 -1
- package/package.json +15 -6
- package/dist/chunk-ARALKXMB.mjs +0 -20
- package/dist/hmac-N6H7F2K2.mjs +0 -26
- package/dist/http-VHOGZ6ZJ.mjs +0 -7
- /package/dist/{http-VHOGZ6ZJ.mjs.map → http-AQVVUCX6.mjs.map} +0 -0
package/README.md
CHANGED
|
@@ -8,12 +8,13 @@ Zero-dependency, GDPR-compliant cookie consent widget for any web framework.
|
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
-
## Why
|
|
11
|
+
## Why Consenti?
|
|
12
12
|
|
|
13
13
|
- **Zero runtime dependencies** — browser built-ins only (`fetch`, `crypto.subtle`, `BroadcastChannel`, `CustomEvent`, `document.cookie`, `localStorage`)
|
|
14
|
-
- **Fully standalone or API-connected** — works offline with
|
|
15
|
-
- **Multi-regulation** — GDPR,
|
|
16
|
-
- **
|
|
14
|
+
- **Fully standalone or API-connected** — works offline with pre-built profiles, or connects to `@consenti/api` for dashboard-managed profiles
|
|
15
|
+
- **Multi-regulation** — GDPR, CCPA, CPRA, LGPD, DPDPA, PIPL, and more via 8 built-in compliance groups
|
|
16
|
+
- **Automatic geo-resolution** — detects visitor jurisdiction client-side (timezone + language) or server-side (`/resolve-profile`)
|
|
17
|
+
- **GPC aware** — Global Privacy Control signal detection with `'honor'` / `'strict'` / `'ignore'` modes per profile
|
|
17
18
|
- **GTM / Google Consent Mode v2** — built-in `dataLayer` integration
|
|
18
19
|
- **SSR-safe** — all browser API access is guarded; `new ConsentiSetup()` during SSR is a silent no-op
|
|
19
20
|
- **Fully typed** — ships TypeScript definitions; no `@types` needed
|
|
@@ -48,7 +49,7 @@ import '@consenti/ui/dist/index.css'
|
|
|
48
49
|
<script src="https://cdn.jsdelivr.net/npm/@consenti/ui/dist/index.umd.js"></script>
|
|
49
50
|
<script>
|
|
50
51
|
const { ConsentiSetup } = ConsentiUI
|
|
51
|
-
new ConsentiSetup({
|
|
52
|
+
new ConsentiSetup({ compliance: { type: 'opt-in' } })
|
|
52
53
|
</script>
|
|
53
54
|
```
|
|
54
55
|
|
|
@@ -57,7 +58,7 @@ import '@consenti/ui/dist/index.css'
|
|
|
57
58
|
```html
|
|
58
59
|
<script type="module">
|
|
59
60
|
import { ConsentiSetup } from 'https://esm.sh/@consenti/ui'
|
|
60
|
-
new ConsentiSetup({
|
|
61
|
+
new ConsentiSetup({ compliance: { type: 'opt-in' } })
|
|
61
62
|
</script>
|
|
62
63
|
```
|
|
63
64
|
|
|
@@ -74,8 +75,16 @@ import '@consenti/ui/dist/index.css'
|
|
|
74
75
|
## Quick Start
|
|
75
76
|
|
|
76
77
|
```ts
|
|
77
|
-
// Simplest possible —
|
|
78
|
-
new ConsentiSetup({
|
|
78
|
+
// Simplest possible — auto-detects jurisdiction from browser timezone + language
|
|
79
|
+
new ConsentiSetup({})
|
|
80
|
+
|
|
81
|
+
// Fixed GDPR (EU opt-in) profile, no backend required
|
|
82
|
+
new ConsentiSetup({ compliance: { type: 'opt-in' } })
|
|
83
|
+
|
|
84
|
+
// API-backed — server geo-resolves the right profile per visitor
|
|
85
|
+
new ConsentiSetup({
|
|
86
|
+
api: { enabled: true, baseUrl: 'https://your-backend.com' },
|
|
87
|
+
})
|
|
79
88
|
```
|
|
80
89
|
|
|
81
90
|
A consent banner appears on first visit. Everything below is optional.
|
|
@@ -101,20 +110,27 @@ import { ConsentiSetup } from '@consenti/ui'
|
|
|
101
110
|
import '@consenti/ui/dist/index.css'
|
|
102
111
|
|
|
103
112
|
const widget = new ConsentiSetup({
|
|
104
|
-
// ──
|
|
113
|
+
// ── Core (optional) ──────────────────────────────────────────────────────────
|
|
105
114
|
core: {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
storage: 'cookie',
|
|
111
|
-
cookieDomains: '.example.com',
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
allowReceipt: true,
|
|
116
|
-
disableCssTemplate: false,
|
|
115
|
+
tenantId: 'acme', // identifies your tenant's profiles; default: 'default'
|
|
116
|
+
locale: 'en', // BCP 47; 'auto' = navigator.language; default: 'auto'
|
|
117
|
+
dir: 'auto', // 'ltr' | 'rtl' | 'auto'; 'auto' derives from locale
|
|
118
|
+
// (ar/he/fa/ur/ps/sd/ug/yi → rtl); default: 'auto'
|
|
119
|
+
storage: 'cookie', // 'cookie' | 'localStorage'; default: 'cookie'
|
|
120
|
+
cookieDomains: '.example.com', // comma-separated; first entry used as Domain attribute
|
|
121
|
+
cookieSigningKey: 'min-32-char-secret', // HMAC-SHA256 signing; implicit from presence
|
|
122
|
+
allowReceipt: true, // allow consent receipt download; default: false
|
|
123
|
+
disableCssTemplate: false, // skip all style injection; default: false
|
|
117
124
|
userId: 'server-assigned-uuid', // authenticated users — enables cross-device sync
|
|
125
|
+
|
|
126
|
+
// Pre-built profile fallback
|
|
127
|
+
usePrebuiltProfiles: 'all', // 'all' (default) | ['opt-in', 'opt-out', ...] (non-empty)
|
|
128
|
+
// controls which of the 8 built-in profiles are available
|
|
129
|
+
// as fallbacks when the API is unavailable
|
|
130
|
+
cacheResolvedProfiles: true, // cache /resolve-profile response in sessionStorage; default: true
|
|
131
|
+
console: ['error'], // log levels emitted: 'info' | 'log' | 'warning' | 'error'
|
|
132
|
+
// default: ['error'] only — no noise in production
|
|
133
|
+
|
|
118
134
|
theme: {
|
|
119
135
|
bgColor: '#ffffff',
|
|
120
136
|
textColor: '#1a1a1a',
|
|
@@ -136,6 +152,35 @@ const widget = new ConsentiSetup({
|
|
|
136
152
|
},
|
|
137
153
|
},
|
|
138
154
|
|
|
155
|
+
// ── Compliance routing (optional) ─────────────────────────────────────────
|
|
156
|
+
compliance: {
|
|
157
|
+
// 'auto' → geo-resolve per visitor (default)
|
|
158
|
+
// api.enabled=true → server resolves via /resolve-profile
|
|
159
|
+
// api.enabled=false → resolves client-side from timezone + language
|
|
160
|
+
// ComplianceGroupId → fixed group for all visitors (skip geo)
|
|
161
|
+
// localProfileType → use a locally registered ConsentiProfile
|
|
162
|
+
type: 'auto',
|
|
163
|
+
|
|
164
|
+
// Client-side geo resolver (used only when api.enabled = false)
|
|
165
|
+
// 'default' → browser timezone + navigator.language heuristic (built-in, zero deps)
|
|
166
|
+
// WidgetCountryResolverFn → custom async function: () => Promise<{ country, region, confidence }>
|
|
167
|
+
geoDataProvider: 'default',
|
|
168
|
+
|
|
169
|
+
// Age gate (optional) — see "Age Gate" section below
|
|
170
|
+
ageGate: {
|
|
171
|
+
enabled: false,
|
|
172
|
+
minimumAge: 13,
|
|
173
|
+
requireParentalConsent: false,
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
// TCF (IAB Transparency & Consent Framework) client stub (optional) — see "TCF" section below
|
|
177
|
+
tcf: {
|
|
178
|
+
enabled: false,
|
|
179
|
+
cmpId: 0, // must match the cmpId configured on the backend (TcfConfig)
|
|
180
|
+
cmpVersion: 1,
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
|
|
139
184
|
// ── Mount point (optional) ────────────────────────────────────────────────
|
|
140
185
|
rootEl: '#my-consent-wrapper', // CSS selector or HTMLElement; default: appends to body
|
|
141
186
|
|
|
@@ -147,6 +192,9 @@ const widget = new ConsentiSetup({
|
|
|
147
192
|
enabled: true,
|
|
148
193
|
baseUrl: 'https://your-site.com', // default: window.location.origin
|
|
149
194
|
authToken: '', // sent as Authorization: Bearer <token>
|
|
195
|
+
tenantId: 'acme', // overrides core.tenantId for API calls
|
|
196
|
+
complianceGroup: 'opt-in', // pin a specific group on the API side (Scenario 2B)
|
|
197
|
+
trustDomain: false, // true = bypass allowedOrigins check (use in dev only)
|
|
150
198
|
},
|
|
151
199
|
|
|
152
200
|
// ── GTM / Google Consent Mode v2 (optional) ───────────────────────────────
|
|
@@ -170,47 +218,190 @@ const widget = new ConsentiSetup({
|
|
|
170
218
|
})
|
|
171
219
|
```
|
|
172
220
|
|
|
221
|
+
### `profileOverride` — deep-merge and delete semantics
|
|
222
|
+
|
|
223
|
+
`profileOverride` (and `widget.setProfile()` at runtime) is deep-merged onto the resolved profile
|
|
224
|
+
— server-fetched, pre-built, or locally registered — before anything renders. Merge rules:
|
|
225
|
+
|
|
226
|
+
- Omitting a key (or setting it to `undefined`) leaves the resolved profile's value untouched.
|
|
227
|
+
- An object value merges recursively, key by key — you only need to specify what differs.
|
|
228
|
+
- An array value replaces/merges by index against the base array.
|
|
229
|
+
- Setting a key to `null` **deletes it** from the merged result (JSON Merge Patch / RFC 7396
|
|
230
|
+
semantics). This is the way to remove a single entry from a keyed map — a cookie category, a
|
|
231
|
+
parameter — without having to know or repeat the rest of that map's contents:
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
profileOverride: {
|
|
235
|
+
preferenceModal: {
|
|
236
|
+
categories: { marketing: null }, // removes the 'marketing' category entirely
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
`{ marketing: undefined }` or `{ marketing: {} }` do **not** delete the entry — the former is a
|
|
242
|
+
no-op (same as omitting the key), the latter merges an empty object onto the existing category,
|
|
243
|
+
changing nothing. Only an explicit `null` removes it.
|
|
244
|
+
|
|
173
245
|
---
|
|
174
246
|
|
|
175
|
-
##
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
|
180
|
-
|
|
181
|
-
| `'
|
|
182
|
-
| `'
|
|
183
|
-
| `'
|
|
184
|
-
| `'dpdpa'`
|
|
185
|
-
| `'
|
|
186
|
-
| `'
|
|
187
|
-
| `'
|
|
188
|
-
| `'
|
|
189
|
-
|
|
247
|
+
## Compliance Groups
|
|
248
|
+
|
|
249
|
+
Consenti ships 8 pre-built English profiles, one per compliance group. The active group is resolved automatically per visitor (API or client-side) or fixed globally via `compliance.type`.
|
|
250
|
+
|
|
251
|
+
| Group | Region / Law | Model | GPC Default |
|
|
252
|
+
|----------------------------|-------------------------------|------------|-------------|
|
|
253
|
+
| `'opt-in'` | EU / EEA — GDPR | Opt-in | `'honor'` |
|
|
254
|
+
| `'opt-out'` | California — CCPA | Opt-out | `'honor'` |
|
|
255
|
+
| `'opt-out-strict'` | California — CPRA | Strict opt-out | `'strict'` |
|
|
256
|
+
| `'opt-in-dpdpa'` | India — DPDPA | Opt-in | `'honor'` |
|
|
257
|
+
| `'opt-in-china'` | China — PIPL | Opt-in | `'ignore'` |
|
|
258
|
+
| `'opt-in-brazil'` | Brazil — LGPD | Opt-in | `'honor'` |
|
|
259
|
+
| `'general-privacy-consent'`| Global / general | Opt-in | `'honor'` |
|
|
260
|
+
| `'notice-only'` | Informational | Notice | `'ignore'` |
|
|
261
|
+
|
|
262
|
+
### Fixed group (no geo-routing)
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
// All visitors see the GDPR opt-in banner
|
|
266
|
+
new ConsentiSetup({ compliance: { type: 'opt-in' } })
|
|
267
|
+
|
|
268
|
+
// All visitors see the CCPA opt-out notice
|
|
269
|
+
new ConsentiSetup({ compliance: { type: 'opt-out' } })
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### Auto geo-routing (default)
|
|
273
|
+
|
|
274
|
+
```ts
|
|
275
|
+
// Client-side: timezone + navigator.language → compliance group
|
|
276
|
+
new ConsentiSetup({ compliance: { type: 'auto' } })
|
|
277
|
+
|
|
278
|
+
// Server-side: /resolve-profile → right profile per visitor country
|
|
279
|
+
new ConsentiSetup({
|
|
280
|
+
api: { enabled: true, baseUrl: 'https://consent.example.com' },
|
|
281
|
+
compliance: { type: 'auto' },
|
|
282
|
+
})
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## Profile Resolution Scenarios
|
|
288
|
+
|
|
289
|
+
| Scenario | When | How |
|
|
290
|
+
|---|---|---|
|
|
291
|
+
| **1A** | `api.enabled = false`, `compliance.type = 'auto'` | Timezone + language → group → pre-built profile |
|
|
292
|
+
| **1B** | `api.enabled = false`, `compliance.type = ComplianceGroupId` | Direct pre-built profile load |
|
|
293
|
+
| **2A** | `api.enabled = true`, `compliance.type = 'auto'` | `GET /resolve-profile?tz&lang&locale&tenantId` → `filePath` → static JSON |
|
|
294
|
+
| **2B** | `api.enabled = true`, `compliance.type = ComplianceGroupId` | `GET /profiles/:tenantId/:group/:locale` |
|
|
295
|
+
| **Local** | `compliance.type = localKey` + registered `ConsentiProfile` | Locally registered profile |
|
|
296
|
+
| **Fallback** | Any fetch failure | Pre-built profile → `DEFAULT_PROFILE` |
|
|
297
|
+
|
|
298
|
+
The `/resolve-profile` response URL is cached in `sessionStorage` for 1 hour per tab (`core.cacheResolvedProfiles`). The profile JSON itself is HTTP-cached via `Cache-Control: public, max-age=3600` on the server.
|
|
299
|
+
|
|
300
|
+
### Domain allowlist
|
|
301
|
+
|
|
302
|
+
If a profile's `allowedOrigins` list is configured on the server, the widget checks `window.location.origin` against it before using the profile. A mismatch falls back to the pre-built profile for that group.
|
|
303
|
+
|
|
304
|
+
Set `api.trustDomain: true` to bypass this check (useful for localhost / dev environments where `allowedOrigins` contains only production domains).
|
|
190
305
|
|
|
191
306
|
---
|
|
192
307
|
|
|
193
308
|
## GPC — Global Privacy Control
|
|
194
309
|
|
|
310
|
+
GPC mode is set per-profile on the server (`gpcMode: 'honor' | 'strict' | 'ignore'`). Widget config profileOverrides overrides the profile value when explicitly set.
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
## Age Gate
|
|
315
|
+
|
|
316
|
+
Set `compliance.ageGate.enabled: true` to require age confirmation before the visitor sees any
|
|
317
|
+
other consent UI (banner, GPC, CCPA opt-out — all of it waits behind the age gate on first visit):
|
|
318
|
+
|
|
195
319
|
```ts
|
|
196
320
|
new ConsentiSetup({
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
321
|
+
compliance: {
|
|
322
|
+
ageGate: { enabled: true, minimumAge: 16, requireParentalConsent: true },
|
|
323
|
+
},
|
|
324
|
+
})
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
- **Confirmed** (visitor is old enough) → the normal banner/GPC/CCPA flow proceeds exactly as if
|
|
328
|
+
the age gate didn't exist, and every consent submission from then on carries `ageVerified: true`.
|
|
329
|
+
- **Declined, `requireParentalConsent: false`** → a deny-all consent is submitted immediately
|
|
330
|
+
(mandatory/strictly-necessary cookies still granted), no banner shown, `ageVerified: false`.
|
|
331
|
+
- **Declined, `requireParentalConsent: true`** → same deny-all submission, plus a
|
|
332
|
+
`parentalConsentToken` is generated and a `consenti:parentalConsentRequired` event fires
|
|
333
|
+
carrying it. There's no email-verification pipeline built into this package — the event is the
|
|
334
|
+
hook for wiring your own out-of-band parental-consent process (once verified externally, call
|
|
335
|
+
`PUT /consent/:visitorId` yourself to update the record).
|
|
336
|
+
|
|
337
|
+
The prompt only asks once per visitor per profile — it doesn't re-appear once any consent record
|
|
338
|
+
exists (same "already decided" check the banner itself uses).
|
|
339
|
+
|
|
340
|
+
---
|
|
341
|
+
|
|
342
|
+
## TCF — IAB Transparency & Consent Framework
|
|
343
|
+
|
|
344
|
+
Set `compliance.tcf.enabled: true` to install `window.__tcfapi`, the standard IAB entry point
|
|
345
|
+
third-party ad-tech scripts call to read consent (`ping`, `getTCData`, `addEventListener`,
|
|
346
|
+
`removeEventListener`):
|
|
347
|
+
|
|
348
|
+
```ts
|
|
349
|
+
new ConsentiSetup({
|
|
350
|
+
compliance: {
|
|
351
|
+
tcf: { enabled: true, cmpId: 280, cmpVersion: 1 }, // cmpId must match the backend's TcfConfig
|
|
201
352
|
},
|
|
202
353
|
})
|
|
203
354
|
```
|
|
204
355
|
|
|
205
|
-
|
|
356
|
+
The stub follows the standard queue-command convention, so it's safe regardless of load order
|
|
357
|
+
relative to third-party scripts that call `__tcfapi` before Consenti has initialized. Only one CMP
|
|
358
|
+
may own `window.__tcfapi` per page — if it's already set (a real CMP, or another `ConsentiSetup`
|
|
359
|
+
instance on a multi-profile page), this stub does not overwrite it.
|
|
360
|
+
|
|
361
|
+
This is a simplified implementation: `tcString` is a base64url-encoded JSON payload (the same
|
|
362
|
+
format the backend's `tcfString` uses), not the full IAB binary bitfield encoding, and vendor-list
|
|
363
|
+
metadata (`gvlVersion`, `publisherCC`) is a placeholder since the widget doesn't fetch the GVL
|
|
364
|
+
client-side. For full IAB TCF v2.2 compliance, use the `iabtcf-core` npm package instead.
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
## RTL / Text Direction
|
|
369
|
+
|
|
370
|
+
`core.dir` controls the banner/modal's reading direction:
|
|
371
|
+
|
|
372
|
+
```ts
|
|
373
|
+
new ConsentiSetup({ core: { locale: 'ar', dir: 'auto' } }) // → rtl, derived from locale
|
|
374
|
+
new ConsentiSetup({ core: { dir: 'rtl' } }) // explicit, independent of locale
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
`'auto'` (the default) maps a known RTL-language prefix (`ar`, `he`, `fa`, `ur`, `ps`, `sd`, `ug`,
|
|
378
|
+
`yi`) to `rtl`; anything else is `ltr`. The `dir` attribute is set on the widget's root element, so
|
|
379
|
+
browser-native mirroring (flex layout, `text-align: start/end`) and the widget's own `[dir="rtl"]`
|
|
380
|
+
CSS overrides (toggle switches, locale switcher, close button) apply automatically — no separate
|
|
381
|
+
RTL stylesheet needed. Explicit banner/modal position variants (`left-bottom`, `right-bottom`,
|
|
382
|
+
modal `left`/`right` slide-in) are **not** mirrored — those are screen positions you chose
|
|
383
|
+
deliberately, independent of text direction.
|
|
206
384
|
|
|
207
385
|
---
|
|
208
386
|
|
|
209
387
|
## Profiles
|
|
210
388
|
|
|
211
|
-
###
|
|
389
|
+
### Pre-built profiles (no backend)
|
|
390
|
+
|
|
391
|
+
The widget includes 8 pre-built English profiles as dynamic-import chunks. Only the matched chunk downloads at runtime.
|
|
392
|
+
|
|
393
|
+
```ts
|
|
394
|
+
// GDPR banner — no server needed
|
|
395
|
+
new ConsentiSetup({ compliance: { type: 'opt-in' } })
|
|
212
396
|
|
|
213
|
-
|
|
397
|
+
// CCPA notice — no server needed
|
|
398
|
+
new ConsentiSetup({ compliance: { type: 'opt-out' } })
|
|
399
|
+
|
|
400
|
+
// Restrict which pre-built profiles are available as fallbacks
|
|
401
|
+
new ConsentiSetup({
|
|
402
|
+
core: { usePrebuiltProfiles: ['opt-in', 'opt-out'] },
|
|
403
|
+
})
|
|
404
|
+
```
|
|
214
405
|
|
|
215
406
|
### Local profile (no backend)
|
|
216
407
|
|
|
@@ -221,12 +412,13 @@ import { ConsentiProfile, ConsentiSetup } from '@consenti/ui'
|
|
|
221
412
|
|
|
222
413
|
const profile = new ConsentiProfile({
|
|
223
414
|
defaultLocale: 'en',
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
{
|
|
227
|
-
{
|
|
228
|
-
|
|
229
|
-
|
|
415
|
+
complianceGroup: 'opt-in',
|
|
416
|
+
cookies: {
|
|
417
|
+
necessary: { purpose: 'necessary', listenGpc: false },
|
|
418
|
+
analytics: { purpose: 'analytics', listenGpc: true },
|
|
419
|
+
marketing: { purpose: 'marketing', listenGpc: true, cpraCategory: 'sale' },
|
|
420
|
+
// cpraCategory: 'sale' | 'sharing' | 'sensitive' — used by CPRA group
|
|
421
|
+
},
|
|
230
422
|
translations: {
|
|
231
423
|
en: {
|
|
232
424
|
mainBanner: {
|
|
@@ -237,14 +429,14 @@ const profile = new ConsentiProfile({
|
|
|
237
429
|
heading: 'We value your privacy',
|
|
238
430
|
headingTag: 'h2', // HTML tag for the heading; default 'h2'
|
|
239
431
|
htmlText: 'We use cookies to improve your experience.',
|
|
240
|
-
buttons:
|
|
241
|
-
|
|
432
|
+
buttons: {
|
|
433
|
+
// key = machine id, rendered as the button's DOM id: `consenti-btn-{id}`, for targeting a specific button
|
|
242
434
|
// cookies: '*' = grant all | '!' = deny all | ['id1','id2'] = grant specific
|
|
243
|
-
{ text: '
|
|
244
|
-
{ text: '
|
|
245
|
-
{ text: '
|
|
246
|
-
|
|
247
|
-
|
|
435
|
+
'accept-all': { text: 'Accept All', style: 'primary', action: 'custom', cookies: '*' },
|
|
436
|
+
'reject-optional': { text: 'Reject Optional', style: 'secondary', action: 'custom', cookies: '!' },
|
|
437
|
+
customize: { text: 'Customize', style: 'secondary', action: 'manage' },
|
|
438
|
+
'privacy-policy': { text: 'Privacy Policy', style: 'text', action: 'link', url: '/privacy' },
|
|
439
|
+
},
|
|
248
440
|
},
|
|
249
441
|
gpcBanner: { // shown instead of mainBanner when GPC detected
|
|
250
442
|
position: 'bottom',
|
|
@@ -252,14 +444,14 @@ const profile = new ConsentiProfile({
|
|
|
252
444
|
headingTag: 'h2',
|
|
253
445
|
showLocaleSwitcher: false,
|
|
254
446
|
htmlText: "Your browser's GPC signal was detected. Ad cookies have been pre-denied.",
|
|
255
|
-
buttons:
|
|
256
|
-
{ text: 'Understood',
|
|
257
|
-
{ text: '
|
|
258
|
-
|
|
447
|
+
buttons: {
|
|
448
|
+
understood: { text: 'Understood', style: 'primary', action: 'custom', cookies: '!' },
|
|
449
|
+
customize: { text: 'Customize', style: 'secondary', action: 'manage' },
|
|
450
|
+
},
|
|
259
451
|
},
|
|
260
452
|
preferenceModal: {
|
|
261
453
|
heading: 'Cookie Preferences',
|
|
262
|
-
headingTag: 'h2',
|
|
454
|
+
headingTag: 'h2',
|
|
263
455
|
subheading: 'Choose which cookies you allow.',
|
|
264
456
|
htmlText: 'We use different types of cookies. You can enable or disable each category below.',
|
|
265
457
|
position: 'center', // 'left' | 'right' | 'center'
|
|
@@ -267,35 +459,33 @@ const profile = new ConsentiProfile({
|
|
|
267
459
|
showLocaleSwitcher: false,
|
|
268
460
|
persistent: false, // true = cannot dismiss by clicking outside
|
|
269
461
|
overlayOpacity: 50,
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
{ text: '
|
|
273
|
-
{ text: '
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
462
|
+
mobileFullScreenBreakpoint: 576,
|
|
463
|
+
buttons: {
|
|
464
|
+
'accept-all': { text: 'Accept All', style: 'primary', action: 'custom', cookies: '*' },
|
|
465
|
+
'save-preferences': { text: 'Save Preferences', style: 'primary', action: 'submit' },
|
|
466
|
+
'reject-optional': { text: 'Reject Optional', style: 'text', action: 'custom', cookies: '!' },
|
|
467
|
+
},
|
|
468
|
+
categories: {
|
|
469
|
+
necessary: {
|
|
278
470
|
heading: 'Strictly Necessary',
|
|
279
|
-
headingTag: 'h3',
|
|
471
|
+
headingTag: 'h3',
|
|
280
472
|
htmlText: 'Required for the site to function. <strong>Cannot be disabled.</strong>',
|
|
281
|
-
|
|
473
|
+
legalBasis: 'mandatory',
|
|
282
474
|
cookies: ['necessary'],
|
|
283
475
|
},
|
|
284
|
-
{
|
|
285
|
-
id: 'cat-analytics',
|
|
476
|
+
analytics: {
|
|
286
477
|
heading: 'Analytics',
|
|
287
478
|
htmlText: 'Helps us understand how visitors use the site.',
|
|
288
|
-
|
|
289
|
-
// type: 'legitimate_interest' — refusal stored as 'objected' instead of 'denied'
|
|
479
|
+
legalBasis: 'consent', // 'mandatory' | 'consent' | 'legitimate_interest'
|
|
290
480
|
cookies: ['analytics'],
|
|
291
481
|
},
|
|
292
|
-
{
|
|
293
|
-
id: 'cat-marketing',
|
|
482
|
+
marketing: {
|
|
294
483
|
heading: 'Marketing',
|
|
295
484
|
htmlText: 'Used to personalise ads and measure campaign performance.',
|
|
485
|
+
legalBasis: 'consent',
|
|
296
486
|
cookies: ['marketing'],
|
|
297
487
|
},
|
|
298
|
-
|
|
488
|
+
},
|
|
299
489
|
},
|
|
300
490
|
},
|
|
301
491
|
fr: {
|
|
@@ -307,57 +497,137 @@ const profile = new ConsentiProfile({
|
|
|
307
497
|
})
|
|
308
498
|
|
|
309
499
|
new ConsentiSetup({
|
|
310
|
-
|
|
311
|
-
profileId: profile.getId(), // always use .getId() — never hardcode the ID
|
|
312
|
-
regulation: 'gdpr',
|
|
313
|
-
locale: 'fr',
|
|
314
|
-
},
|
|
500
|
+
compliance: { type: profile.getComplianceGroup() },
|
|
315
501
|
})
|
|
316
502
|
```
|
|
317
503
|
|
|
318
|
-
> **Note:** Local profile IDs are auto-assigned starting from `1000` to avoid collisions with API profile IDs (which start from `1`). Always call `profile.getId()`.
|
|
319
|
-
|
|
320
504
|
### API profile (with backend)
|
|
321
505
|
|
|
322
506
|
```ts
|
|
323
507
|
new ConsentiSetup({
|
|
324
|
-
core: { profileId: 3, regulation: 'gdpr', locale: 'de' },
|
|
325
508
|
api: {
|
|
326
509
|
enabled: true,
|
|
327
510
|
baseUrl: 'https://consent.example.com',
|
|
511
|
+
tenantId: 'acme',
|
|
328
512
|
},
|
|
513
|
+
// compliance.type: 'auto' resolves the right profile per visitor country
|
|
329
514
|
})
|
|
330
515
|
```
|
|
331
516
|
|
|
332
|
-
The widget
|
|
517
|
+
The widget calls `GET /resolve-profile?tz&lang&locale&tenantId`, receives the profile file path, then fetches that static JSON directly. Falls back to pre-built profile if the API is unavailable.
|
|
333
518
|
|
|
334
519
|
---
|
|
335
520
|
|
|
336
521
|
## Cookie Format
|
|
337
522
|
|
|
338
|
-
Consent is persisted as a single cookie named `
|
|
523
|
+
Consent is persisted as a single cookie named `consenti_data` with a compact JSON value:
|
|
339
524
|
|
|
525
|
+
```json
|
|
526
|
+
{"s":1,"v":2,"i":"5huf-…","u":"","t":1751692400,"g":0,"p":0,"c":{"analytics_storage":"g","ad_storage":"d"}}
|
|
340
527
|
```
|
|
341
|
-
|
|
528
|
+
|
|
529
|
+
| Field | Type | Meaning |
|
|
530
|
+
|---|---|---|
|
|
531
|
+
| `s` | number | Profile ID |
|
|
532
|
+
| `v` | number | Profile version |
|
|
533
|
+
| `i` | string | Per-submission UUID (consent receipt ID) |
|
|
534
|
+
| `u` | string | Logged-in user ID (`""` for anonymous) |
|
|
535
|
+
| `t` | number | Unix timestamp of consent |
|
|
536
|
+
| `g` | 0 \| 1 | GPC signal detected |
|
|
537
|
+
| `p` | number | Consent source: 0 = user click, 1 = widget method |
|
|
538
|
+
| `c` | object | Consent map: cookie ID → `"g"` (granted), `"o"` (objected), `"d"` (denied) |
|
|
539
|
+
|
|
540
|
+
When the widget is upgraded from an older version, any existing `consenti_{profileId}` cookie is automatically read, migrated to the new format, and deleted — no consent loss occurs.
|
|
541
|
+
|
|
542
|
+
The consent cookie lifetime equals the shortest `cookie.expiry` value across all profile cookies (in days, converted to `max-age` seconds). When that cookie expires the browser deletes it, the widget sees no consent, and re-shows the banner.
|
|
543
|
+
|
|
544
|
+
---
|
|
545
|
+
|
|
546
|
+
## Display Features
|
|
547
|
+
|
|
548
|
+
### Footer Metadata
|
|
549
|
+
|
|
550
|
+
When `showFooterMetadata: true` is set in the resolved profile, a metadata strip is injected inside the banner and modal after the main content. It shows the visitor's Consent ID (truncated UUID), Consent Date, Profile Version, and a "Privacy Settings" button that opens the preference modal.
|
|
551
|
+
|
|
552
|
+
Configured in the dashboard (ProfileEditor → Step 1) or via `profileOverride`:
|
|
553
|
+
|
|
554
|
+
```ts
|
|
555
|
+
new ConsentiSetup({
|
|
556
|
+
compliance: { type: 'opt-in' },
|
|
557
|
+
profileOverride: { showFooterMetadata: true },
|
|
558
|
+
})
|
|
342
559
|
```
|
|
343
560
|
|
|
344
|
-
|
|
561
|
+
### Enhanced Accessibility
|
|
562
|
+
|
|
563
|
+
When `enhanceAccessibility: true` is set in the resolved profile, the class `.consenti--enhanced-a11y` is added to the root element. This applies:
|
|
564
|
+
|
|
565
|
+
- Minimum 44 × 44 px hit area on all buttons (WCAG 2.1 SC 2.5.5)
|
|
566
|
+
- 3 px solid focus ring (`outline-offset: 2px`) on all focusable elements
|
|
567
|
+
- No impact on default styling — opt-in only
|
|
568
|
+
|
|
569
|
+
```ts
|
|
570
|
+
profileOverride: { enhanceAccessibility: true }
|
|
345
571
|
```
|
|
346
|
-
|
|
572
|
+
|
|
573
|
+
### Responsive Button Stacking (`stackButtonsOnBreakpoint`)
|
|
574
|
+
|
|
575
|
+
Set `mainBanner.stackButtonsOnBreakpoint` (and/or `gpcBanner.stackButtonsOnBreakpoint`) to a pixel width. Below that breakpoint the banner's buttons stack vertically and stretch to full width — useful for narrow mobile screens where 3+ buttons overflow.
|
|
576
|
+
|
|
577
|
+
```ts
|
|
578
|
+
profileOverride: {
|
|
579
|
+
mainBanner: { stackButtonsOnBreakpoint: 576 }, // px; 0 or undefined = disabled
|
|
580
|
+
}
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
A scoped `<style>` element containing a single `@media (max-width: Npx)` rule is injected into the root element — no external stylesheet required, and it's removed on `destroy()`.
|
|
584
|
+
|
|
585
|
+
### Modal Focus Trap (`trapFocus`)
|
|
586
|
+
|
|
587
|
+
When `preferenceModal.trapFocus: true`, keyboard Tab / Shift+Tab navigation is confined within the consent modal while it is open. `Escape` closes the modal and returns focus to the triggering element.
|
|
588
|
+
|
|
589
|
+
```ts
|
|
590
|
+
profileOverride: {
|
|
591
|
+
preferenceModal: { trapFocus: true },
|
|
592
|
+
}
|
|
347
593
|
```
|
|
348
594
|
|
|
349
595
|
---
|
|
350
596
|
|
|
597
|
+
## Debugging
|
|
598
|
+
|
|
599
|
+
```ts
|
|
600
|
+
new ConsentiSetup({
|
|
601
|
+
core: {
|
|
602
|
+
console: ['error', 'warning', 'info'], // add 'log' for verbose output
|
|
603
|
+
},
|
|
604
|
+
})
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
The default is `['error']` only — no noise in production. Add levels when investigating profile resolution, GPC behavior, or domain allowlist failures. All Consenti log lines are prefixed `[Consenti]`.
|
|
608
|
+
|
|
609
|
+
---
|
|
610
|
+
|
|
351
611
|
## Widget API Methods
|
|
352
612
|
|
|
353
613
|
```ts
|
|
354
|
-
const widget = new ConsentiSetup({
|
|
614
|
+
const widget = new ConsentiSetup({ compliance: { type: 'opt-in' } })
|
|
355
615
|
|
|
356
616
|
// Consent state
|
|
357
|
-
widget.hasConsent()
|
|
358
|
-
widget.getConsent()
|
|
359
|
-
widget.
|
|
360
|
-
widget.
|
|
617
|
+
widget.hasConsent() // boolean — true if valid consent record exists
|
|
618
|
+
widget.getConsent() // Record<string, 'granted'|'denied'|'objected'> | null
|
|
619
|
+
widget.getConsent('google-gtm') // Google Consent Mode v2 format
|
|
620
|
+
widget.getConsent('category') // { necessary, functional, preferences, analytics, marketing }
|
|
621
|
+
widget.getConsent('adobe') // { analytics, target, manager, optimizer }
|
|
622
|
+
widget.getConsent('meta') // { pixel, api, plugins, facebookLogin }
|
|
623
|
+
widget.getConsent('microsoft-clarity') // { session, heatmaps, performance }
|
|
624
|
+
widget.getConsent('twilio-segment') // { identify, page, track, group, alias }
|
|
625
|
+
widget.getGTMConsent() // @deprecated — use getConsent('google-gtm')
|
|
626
|
+
widget.getConsentDate() // Date | false — time of last submission
|
|
627
|
+
widget.isCookieGranted('analytics') // boolean — true if that cookie is 'granted'
|
|
628
|
+
widget.isCookieGranted('analytics', true)// 'granted' | 'denied' | 'objected' | false
|
|
629
|
+
widget.isCategoryGranted('cat-analytics') // boolean — true if ALL cookies in category are granted
|
|
630
|
+
widget.isCategoryGranted('cat-analytics', true) // [{ analytics: 'granted' }, { pixel: 'denied' }]
|
|
361
631
|
|
|
362
632
|
// Visibility
|
|
363
633
|
widget.showBanner(gpc?) // show main banner (or GPC variant)
|
|
@@ -367,6 +637,12 @@ widget.hideModal() // close preference modal
|
|
|
367
637
|
widget.bannerVisibility() // 'main' | 'gpc' | false
|
|
368
638
|
widget.modalVisibility() // 'preference' | false
|
|
369
639
|
|
|
640
|
+
// Bulk consent actions
|
|
641
|
+
widget.grantAll() // grant all cookies and dismiss banner
|
|
642
|
+
widget.grantAll(true) // grant only mandatory; deny everything else
|
|
643
|
+
widget.denyAll() // deny non-mandatory; mandatory stay 'granted'
|
|
644
|
+
widget.denyAll(true) // deny all including mandatory (logs a warning)
|
|
645
|
+
|
|
370
646
|
// Actions
|
|
371
647
|
widget.init() // manually start init (when autoInit: false)
|
|
372
648
|
widget.onReady(callback) // called once widget is fully initialised
|
|
@@ -374,15 +650,61 @@ widget.switchLocale(locale) // switch locale and re-render (e.g. 'fr', 'de-
|
|
|
374
650
|
widget.submitConsent(consent) // programmatically submit consent values
|
|
375
651
|
widget.deleteConsent() // delete consent record (cookie + backend if enabled)
|
|
376
652
|
widget.reConsent() // delete consent and re-show banner
|
|
377
|
-
widget.downloadReceipt() // trigger JSON consent receipt download
|
|
378
653
|
widget.destroy() // unmount DOM elements and remove all event listeners
|
|
654
|
+
|
|
655
|
+
// Runtime configuration
|
|
656
|
+
widget.setDarkMode() // toggle dark mode
|
|
657
|
+
widget.setDarkMode(true) // force dark on
|
|
658
|
+
widget.setDarkMode(false) // force light
|
|
659
|
+
widget.setTheme({ primaryColor: '#ff0000' }) // hot-swap CSS tokens (merges into current theme)
|
|
660
|
+
widget.setConfig({ darkMode: true }) // deep-merge partial config (no re-init)
|
|
661
|
+
widget.setProfile({ mainBanner: { heading: 'Updated' } }) // re-render UI with new profile data
|
|
662
|
+
|
|
663
|
+
// Diagnostics
|
|
664
|
+
widget.version() // { package: '1.0.0', profileVersion: 2, consentVersion: 1 }
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
### Consent checks
|
|
668
|
+
|
|
669
|
+
```ts
|
|
670
|
+
// Gate analytics code on a single cookie
|
|
671
|
+
if (widget.isCookieGranted('analytics_storage')) {
|
|
672
|
+
initAnalytics()
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// Read the raw status string when you need it
|
|
676
|
+
const status = widget.isCookieGranted('marketing', true) // 'granted' | 'denied' | 'objected' | false
|
|
677
|
+
|
|
678
|
+
// Gate a feature on an entire category (all cookies in category must be granted)
|
|
679
|
+
if (widget.isCategoryGranted('cat-analytics')) {
|
|
680
|
+
loadHeatmaps()
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// Inspect each cookie's status in a category
|
|
684
|
+
const statuses = widget.isCategoryGranted('cat-marketing', true)
|
|
685
|
+
// [{ ad_storage: 'granted' }, { ad_personalization: 'denied' }]
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
### Typed event subscriptions
|
|
689
|
+
|
|
690
|
+
```ts
|
|
691
|
+
// Both forms are accepted — 'consenti:' prefix is optional
|
|
692
|
+
const handler = (data) => console.log('Consent saved:', data.consent)
|
|
693
|
+
widget.on('consentSubmitted', handler)
|
|
694
|
+
|
|
695
|
+
// Remove later (must pass the same function reference)
|
|
696
|
+
widget.off('consentSubmitted', handler)
|
|
697
|
+
|
|
698
|
+
// Available event names:
|
|
699
|
+
// 'bannerInitialized' | 'bannerVisibility' | 'modalVisibility'
|
|
700
|
+
// 'consentBeingSubmitted' | 'consentSubmitted'
|
|
379
701
|
```
|
|
380
702
|
|
|
381
703
|
### Manual init
|
|
382
704
|
|
|
383
705
|
```ts
|
|
384
706
|
const widget = new ConsentiSetup({
|
|
385
|
-
|
|
707
|
+
compliance: { type: 'opt-in' },
|
|
386
708
|
rootEl: '#consent-mount',
|
|
387
709
|
autoInit: false,
|
|
388
710
|
})
|
|
@@ -400,32 +722,40 @@ await widget.submitConsent({
|
|
|
400
722
|
marketing: 'denied',
|
|
401
723
|
necessary: 'granted', // mandatory cookies are always 'granted' regardless
|
|
402
724
|
})
|
|
725
|
+
|
|
726
|
+
// Or use the convenience methods:
|
|
727
|
+
await widget.grantAll() // accept all
|
|
728
|
+
await widget.denyAll() // Reject Optional non-mandatory
|
|
403
729
|
```
|
|
404
730
|
|
|
405
731
|
---
|
|
406
732
|
|
|
407
733
|
## Events
|
|
408
734
|
|
|
409
|
-
Custom DOM events fire on `
|
|
410
|
-
|
|
411
|
-
| Event | Fired when |
|
|
412
|
-
|
|
413
|
-
| `consenti:bannerInitialized` | Widget initialises and determines banner visibility |
|
|
414
|
-
| `consenti:bannerVisibility` | Banner shows or hides |
|
|
415
|
-
| `consenti:modalVisibility` | Preference modal shows or hides |
|
|
416
|
-
| `consenti:consentBeingSubmitted` | User clicked a consent button (before API call) |
|
|
417
|
-
| `consenti:consentSubmitted` | Consent saved (cookie written + API call if configured) |
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
//
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
735
|
+
Custom DOM events fire on `window` at every lifecycle step. All are prefixed `consenti:`.
|
|
736
|
+
|
|
737
|
+
| Event | Fired when |
|
|
738
|
+
|--------------------------------|----------------------------------------------------------|
|
|
739
|
+
| `consenti:bannerInitialized` | Widget initialises and determines banner visibility |
|
|
740
|
+
| `consenti:bannerVisibility` | Banner shows or hides |
|
|
741
|
+
| `consenti:modalVisibility` | Preference modal shows or hides |
|
|
742
|
+
| `consenti:consentBeingSubmitted` | User clicked a consent button (before API call) |
|
|
743
|
+
| `consenti:consentSubmitted` | Consent saved (cookie written + API call if configured) |
|
|
744
|
+
| `consenti:parentalConsentRequired` | Age gate declined with `requireParentalConsent: true` — carries `parentalConsentToken` |
|
|
745
|
+
|
|
746
|
+
**Recommended:** use `widget.on()` / `widget.off()` for typed subscriptions (the `consenti:` prefix is optional):
|
|
747
|
+
|
|
748
|
+
```ts
|
|
749
|
+
const handler = (data: ConsentEvent) => console.log(data.consent)
|
|
750
|
+
widget.on('consentSubmitted', handler)
|
|
751
|
+
widget.off('consentSubmitted', handler) // same reference to unsubscribe
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
**Raw DOM listeners** (equivalent):
|
|
755
|
+
|
|
756
|
+
```ts
|
|
757
|
+
window.addEventListener('consenti:consentSubmitted', (e: Event) => {
|
|
758
|
+
const detail = (e as CustomEvent<ConsentEvent>).detail
|
|
429
759
|
})
|
|
430
760
|
```
|
|
431
761
|
|
|
@@ -433,39 +763,64 @@ document.addEventListener('consenti:consentSubmitted', (e: Event) => {
|
|
|
433
763
|
|
|
434
764
|
## GTM / Google Consent Mode v2
|
|
435
765
|
|
|
766
|
+
Setting `utils.gtm` to any object (even `{}`) turns on real Consent Mode signalling — no gtag.js
|
|
767
|
+
needs to already be on the page, Consenti defines the standard stub itself.
|
|
768
|
+
|
|
436
769
|
```ts
|
|
437
770
|
new ConsentiSetup({
|
|
438
|
-
|
|
771
|
+
compliance: { type: 'opt-in' },
|
|
439
772
|
utils: {
|
|
440
773
|
gtm: {
|
|
441
|
-
containerId: 'GTM-XXXXXX',
|
|
442
|
-
urlPassthrough: true,
|
|
443
|
-
adsDataRedaction: true,
|
|
774
|
+
containerId: 'GTM-XXXXXX', // omit if you load GTM/gtag.js yourself
|
|
775
|
+
urlPassthrough: true, // cookieless conversion modelling
|
|
776
|
+
adsDataRedaction: true, // redact ad pings when consent denied
|
|
444
777
|
},
|
|
445
778
|
},
|
|
446
779
|
})
|
|
447
780
|
```
|
|
448
781
|
|
|
449
|
-
|
|
782
|
+
**`GtmConfig` options:**
|
|
783
|
+
|
|
784
|
+
| Option | Type | Default | Description |
|
|
785
|
+
|---|---|---|---|
|
|
786
|
+
| `containerId` | `string` | `undefined` | GTM container ID, e.g. `'GTM-XXXXXX'`. When set, Consenti injects the GTM library itself — omit if you already load GTM/gtag.js separately (Consent Mode signalling works either way). |
|
|
787
|
+
| `dataLayer` | `string` | `'dataLayer'` | Name of the dataLayer array on `window`. Override only for a custom variable name. |
|
|
788
|
+
| `verbose` | `boolean` | `false` | When `true`, additionally mirrors every `consenti:*` event onto the dataLayer as a generic `{ event, content }` push — for custom, non-Consent-Mode GTM triggers. |
|
|
789
|
+
| `events` | `string[]` | `[]` | Only relevant when `verbose: true` — narrows which event names get mirrored. Empty means all. Has no effect on the core `gtag('consent', …)` calls, which always fire. |
|
|
790
|
+
| `urlPassthrough` | `boolean` | `false` | Calls `gtag('set', 'url_passthrough', true)` alongside every consent update. |
|
|
791
|
+
| `adsDataRedaction` | `boolean` | `false` | Calls `gtag('set', 'ads_data_redaction', true)` when `ad_storage` is denied. |
|
|
792
|
+
|
|
793
|
+
Consenti calls the real `gtag()` consent API — via the standard stub-queue pattern, so it works
|
|
794
|
+
whether your own gtag.js/GTM snippet loads before or after Consenti:
|
|
450
795
|
|
|
451
796
|
```js
|
|
452
797
|
// On initialisation (default denied state)
|
|
453
|
-
|
|
798
|
+
gtag('consent', 'default', { analytics_storage: 'denied', ... })
|
|
454
799
|
|
|
455
800
|
// On submission
|
|
456
|
-
|
|
801
|
+
gtag('consent', 'update', { analytics_storage: 'granted', ad_storage: 'denied', ... })
|
|
802
|
+
|
|
803
|
+
// Plus, when configured:
|
|
804
|
+
gtag('set', 'url_passthrough', true)
|
|
805
|
+
gtag('set', 'ads_data_redaction', true) // true when ad_storage is denied
|
|
457
806
|
```
|
|
458
807
|
|
|
459
|
-
`
|
|
808
|
+
Set `utils.gtm.verbose: true` to additionally mirror every `consenti:*` event onto the dataLayer
|
|
809
|
+
as a generic `{ event, content }` push — off by default.
|
|
810
|
+
|
|
811
|
+
`getConsent('google-gtm')` returns consent in Google Consent Mode v2 format. `getGTMConsent()` is a deprecated alias for the same call.
|
|
460
812
|
|
|
461
813
|
```ts
|
|
462
|
-
widget.
|
|
814
|
+
widget.getConsent('google-gtm')
|
|
463
815
|
// {
|
|
464
816
|
// analytics_storage: 'granted',
|
|
465
817
|
// ad_storage: 'denied',
|
|
466
818
|
// ad_user_data: 'denied',
|
|
467
819
|
// ad_personalization: 'denied',
|
|
468
820
|
// functionality_storage: 'granted',
|
|
821
|
+
// security_storage: 'granted',
|
|
822
|
+
// ads_data_redaction: 'true',
|
|
823
|
+
// url_passthrough: 'false',
|
|
469
824
|
// }
|
|
470
825
|
```
|
|
471
826
|
|
|
@@ -483,6 +838,8 @@ new ConsentScript({
|
|
|
483
838
|
src: 'https://www.googletagmanager.com/gtag/js?id=G-XXXXX',
|
|
484
839
|
onLoad: () => console.log('Analytics loaded'),
|
|
485
840
|
onRevoke: () => console.log('Analytics removed'),
|
|
841
|
+
// bind: true (default) — auto-removes script on consent revoke, re-injects on re-grant
|
|
842
|
+
// bind: false — check consent once at construction; never auto-remove
|
|
486
843
|
})
|
|
487
844
|
```
|
|
488
845
|
|
|
@@ -555,8 +912,8 @@ Override any token in your own stylesheet:
|
|
|
555
912
|
|
|
556
913
|
```ts
|
|
557
914
|
new ConsentiSetup({
|
|
915
|
+
compliance: { type: 'opt-in' },
|
|
558
916
|
core: {
|
|
559
|
-
regulation: 'gdpr',
|
|
560
917
|
theme: {
|
|
561
918
|
primaryColor: '#7c3aed', // purple brand
|
|
562
919
|
primaryTextColor: '#ffffff',
|
|
@@ -572,7 +929,7 @@ new ConsentiSetup({
|
|
|
572
929
|
|
|
573
930
|
```ts
|
|
574
931
|
new ConsentiSetup({
|
|
575
|
-
|
|
932
|
+
compliance: { type: 'opt-in' },
|
|
576
933
|
darkMode: true, // or: window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
577
934
|
})
|
|
578
935
|
```
|
|
@@ -631,7 +988,8 @@ import '@consenti/ui/dist/index.css'
|
|
|
631
988
|
export function ConsentSetup() {
|
|
632
989
|
useEffect(() => {
|
|
633
990
|
const widget = new ConsentiSetup({
|
|
634
|
-
|
|
991
|
+
compliance: { type: 'opt-in' },
|
|
992
|
+
core: { locale: 'en' },
|
|
635
993
|
})
|
|
636
994
|
return () => widget.destroy()
|
|
637
995
|
}, [])
|
|
@@ -687,8 +1045,8 @@ export function ConsentSetup() {
|
|
|
687
1045
|
let widget: WidgetType
|
|
688
1046
|
import('@consenti/ui').then(({ ConsentiSetup }) => {
|
|
689
1047
|
widget = new ConsentiSetup({
|
|
690
|
-
core: { regulation: 'gdpr', locale: 'en', autoHonorGPC: true },
|
|
691
1048
|
api: { enabled: true, baseUrl: process.env.NEXT_PUBLIC_API_URL },
|
|
1049
|
+
core: { },
|
|
692
1050
|
})
|
|
693
1051
|
widgetRef.current = widget
|
|
694
1052
|
})
|
|
@@ -710,7 +1068,7 @@ let widget: unknown = null
|
|
|
710
1068
|
onMounted(async () => {
|
|
711
1069
|
const { ConsentiSetup } = await import('@consenti/ui')
|
|
712
1070
|
await import('@consenti/ui/dist/index.css')
|
|
713
|
-
widget = new ConsentiSetup({
|
|
1071
|
+
widget = new ConsentiSetup({ compliance: { type: 'opt-in' } })
|
|
714
1072
|
})
|
|
715
1073
|
|
|
716
1074
|
onBeforeUnmount(() => (widget as { destroy?: () => void })?.destroy?.())
|
|
@@ -739,7 +1097,7 @@ export class ConsentService implements OnDestroy {
|
|
|
739
1097
|
async init() {
|
|
740
1098
|
if (!isPlatformBrowser(this.platformId)) return
|
|
741
1099
|
const { ConsentiSetup } = await import('@consenti/ui')
|
|
742
|
-
this.widget = new ConsentiSetup({
|
|
1100
|
+
this.widget = new ConsentiSetup({ compliance: { type: 'opt-in' } })
|
|
743
1101
|
}
|
|
744
1102
|
|
|
745
1103
|
ngOnDestroy() {
|
|
@@ -766,7 +1124,7 @@ export class MyComponent {
|
|
|
766
1124
|
import { ConsentiSetup } from '@consenti/ui'
|
|
767
1125
|
import '@consenti/ui/dist/index.css'
|
|
768
1126
|
|
|
769
|
-
const widget = new ConsentiSetup({
|
|
1127
|
+
const widget = new ConsentiSetup({ compliance: { type: 'opt-in' }, core: { locale: 'en' } })
|
|
770
1128
|
|
|
771
1129
|
document.querySelector('#cookie-settings')?.addEventListener('click', () => {
|
|
772
1130
|
widget.showModal()
|
|
@@ -798,7 +1156,7 @@ class MyPlugin extends ConsentiPlugin {
|
|
|
798
1156
|
}
|
|
799
1157
|
|
|
800
1158
|
new ConsentiSetup({
|
|
801
|
-
|
|
1159
|
+
compliance: { type: 'opt-in' },
|
|
802
1160
|
plugins: [new MyPlugin()],
|
|
803
1161
|
})
|
|
804
1162
|
```
|
|
@@ -809,7 +1167,7 @@ new ConsentiSetup({
|
|
|
809
1167
|
|
|
810
1168
|
```ts
|
|
811
1169
|
// Safe to call during SSR — silently no-ops, never touches the DOM:
|
|
812
|
-
const widget = new ConsentiSetup({
|
|
1170
|
+
const widget = new ConsentiSetup({ compliance: { type: 'opt-in' } })
|
|
813
1171
|
// All browser API access is guarded by isClient() internally.
|
|
814
1172
|
```
|
|
815
1173
|
|
|
@@ -830,8 +1188,8 @@ import {
|
|
|
830
1188
|
simulateConsentSubmitted,
|
|
831
1189
|
} from '@consenti/ui/testing'
|
|
832
1190
|
|
|
833
|
-
mockAllGranted() // fake-grant
|
|
834
|
-
mockAllDenied() // fake-deny
|
|
1191
|
+
mockAllGranted(['analytics', 'marketing']) // fake-grant these cookies for unit tests
|
|
1192
|
+
mockAllDenied(['analytics', 'marketing']) // fake-deny these cookies
|
|
835
1193
|
simulateConsentSubmitted({ analytics: 'granted' })
|
|
836
1194
|
```
|
|
837
1195
|
|
|
@@ -840,20 +1198,44 @@ simulateConsentSubmitted({ analytics: 'granted' })
|
|
|
840
1198
|
## Minimal Recipes
|
|
841
1199
|
|
|
842
1200
|
```ts
|
|
843
|
-
//
|
|
844
|
-
new ConsentiSetup({
|
|
1201
|
+
// Auto geo-route — no backend, client-side timezone + language heuristic
|
|
1202
|
+
new ConsentiSetup({})
|
|
1203
|
+
|
|
1204
|
+
// CCPA opt-out for all visitors
|
|
1205
|
+
new ConsentiSetup({ compliance: { type: 'opt-out' } })
|
|
845
1206
|
|
|
846
1207
|
// Cross-subdomain consent
|
|
847
|
-
new ConsentiSetup({ core: {
|
|
1208
|
+
new ConsentiSetup({ core: { storage: 'cookie', cookieDomains: '.example.com' } })
|
|
848
1209
|
|
|
849
1210
|
// Authenticated user — cross-device sync via API
|
|
850
|
-
new ConsentiSetup({
|
|
1211
|
+
new ConsentiSetup({
|
|
1212
|
+
core: { userId: '{{ server_user_id }}' },
|
|
1213
|
+
api: { enabled: true },
|
|
1214
|
+
})
|
|
851
1215
|
|
|
852
|
-
//
|
|
1216
|
+
// GTM
|
|
853
1217
|
new ConsentiSetup({
|
|
854
|
-
core: {
|
|
1218
|
+
core: {},
|
|
855
1219
|
utils: { gtm: { containerId: 'GTM-XXXXXX', adsDataRedaction: true } },
|
|
856
1220
|
})
|
|
1221
|
+
|
|
1222
|
+
// Verbose debug logging during development
|
|
1223
|
+
new ConsentiSetup({
|
|
1224
|
+
core: { console: ['error', 'warning', 'info', 'log'] },
|
|
1225
|
+
})
|
|
1226
|
+
|
|
1227
|
+
// Arabic site — RTL derived automatically from locale
|
|
1228
|
+
new ConsentiSetup({ core: { locale: 'ar' } })
|
|
1229
|
+
|
|
1230
|
+
// COPPA — block under-13s, require parental consent
|
|
1231
|
+
new ConsentiSetup({
|
|
1232
|
+
compliance: { ageGate: { enabled: true, minimumAge: 13, requireParentalConsent: true } },
|
|
1233
|
+
})
|
|
1234
|
+
|
|
1235
|
+
// TCF — install the __tcfapi stub for ad-tech scripts (cmpId must match the backend's tcf config)
|
|
1236
|
+
new ConsentiSetup({
|
|
1237
|
+
compliance: { tcf: { enabled: true, cmpId: 280, cmpVersion: 1 } },
|
|
1238
|
+
})
|
|
857
1239
|
```
|
|
858
1240
|
|
|
859
1241
|
---
|
|
@@ -864,17 +1246,32 @@ All types are exported from the main entry:
|
|
|
864
1246
|
|
|
865
1247
|
```ts
|
|
866
1248
|
import type {
|
|
867
|
-
ConsentiConfig,
|
|
868
|
-
CoreConfig,
|
|
869
|
-
ApiConfig,
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
1249
|
+
ConsentiConfig, // top-level config object
|
|
1250
|
+
CoreConfig, // core section
|
|
1251
|
+
ApiConfig, // api section
|
|
1252
|
+
ComplianceWidgetConfig, // compliance section
|
|
1253
|
+
AgeGateWidgetConfig, // compliance.ageGate section
|
|
1254
|
+
WidgetCountryResolverFn, // custom geo resolver function type
|
|
1255
|
+
UtilsConfig, // utils section
|
|
1256
|
+
GtmConfig, // utils.gtm section
|
|
1257
|
+
ThemeConfig, // core.theme section
|
|
1258
|
+
ConsentValue, // 'granted' | 'denied' | 'objected'
|
|
1259
|
+
ConsentiProfile, // local profile class
|
|
1260
|
+
NonEmptyArray, // [T, ...T[]] — used by usePrebuiltProfiles
|
|
875
1261
|
} from '@consenti/ui'
|
|
876
1262
|
```
|
|
877
1263
|
|
|
1264
|
+
### Breaking changes from v0.0.x
|
|
1265
|
+
|
|
1266
|
+
The following `CoreConfig` fields were **removed** in v0.1.x:
|
|
1267
|
+
|
|
1268
|
+
| Removed field | Replacement |
|
|
1269
|
+
|----------------------|-------------|
|
|
1270
|
+
| `core.profileId` | Use `compliance.type` with a compliance group or local profile key |
|
|
1271
|
+
| `core.regulation` | Use `compliance.type` — compliance group is derived server-side or from pre-built profiles |
|
|
1272
|
+
| `core.signCookies` | Implicit: set `core.cookieSigningKey` to enable signing; no separate flag needed |
|
|
1273
|
+
| `core.privacyPolicyUrl` | Add a link button directly in the profile banner/modal `buttons` map with `action: 'link'` |
|
|
1274
|
+
|
|
878
1275
|
---
|
|
879
1276
|
|
|
880
1277
|
## Contributing
|