@consenti/ui 0.3.0 → 0.4.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 +283 -98
- package/dist/angular.d.cts +326 -43
- package/dist/angular.d.ts +326 -43
- package/dist/angular.js +1 -1
- package/dist/angular.js.map +1 -1
- package/dist/angular.mjs +1 -1
- package/dist/angular.mjs.map +1 -1
- package/dist/chunk-RDE7CM6T.mjs +2 -0
- package/dist/chunk-RDE7CM6T.mjs.map +1 -0
- package/dist/index.css +1 -1
- package/dist/index.d.ts +377 -42
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/index.umd.js.map +1 -1
- package/dist/profiles-5ZLWWN2L.mjs +2 -0
- package/dist/react.d.cts +326 -43
- package/dist/react.d.ts +326 -43
- package/dist/react.js +1 -1
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +1 -1
- package/dist/react.mjs.map +1 -1
- package/dist/testing.d.cts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +1 -1
- package/dist/testing.js.map +1 -1
- package/dist/testing.mjs +1 -1
- package/dist/testing.mjs.map +1 -1
- package/dist/vue.d.cts +326 -43
- package/dist/vue.d.ts +326 -43
- package/dist/vue.js +1 -1
- package/dist/vue.js.map +1 -1
- package/dist/vue.mjs +1 -1
- package/dist/vue.mjs.map +1 -1
- package/package.json +9 -5
- package/dist/chunk-J4BKXUD6.mjs +0 -2
- package/dist/chunk-J4BKXUD6.mjs.map +0 -1
- package/dist/profiles-OEZAKEWL.mjs +0 -2
- /package/dist/{profiles-OEZAKEWL.mjs.map → profiles-5ZLWWN2L.mjs.map} +0 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @consenti/ui
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
GDPR-style cookie consent widget for any web framework — zero required runtime dependencies.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@consenti/ui)
|
|
6
6
|
[](../../LICENSE)
|
|
@@ -12,12 +12,15 @@ Zero-dependency, GDPR-compliant cookie consent widget for any web framework.
|
|
|
12
12
|
|
|
13
13
|
- **Zero runtime dependencies** — browser built-ins only (`fetch`, `crypto.subtle`, `BroadcastChannel`, `CustomEvent`, `document.cookie`, `localStorage`)
|
|
14
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
|
|
15
|
+
- **Multi-regulation** — Maintained: GDPR, CCPA, CPRA, LGPD. Partial: TCF v2.3 (real binary encoding requires the `@consenti/api` backend) — only relevant if you monetize through programmatic/RTB ad exchanges. In development: DPDPA. More via 8 built-in compliance groups — see [ECOSYSTEM.md](../../ECOSYSTEM.md) for per-regulation status
|
|
16
|
+
|
|
17
|
+
> **Registration is only needed for programmatic ad monetization.** For first-party analytics/marketing consent, Consenti is fully compliant with zero external registration. Consenti implements the IAB TCF/GPP technical specs but is **not** itself a registered CMP; if you do need TCF/GPP, you must register your own `cmpId` with IAB Europe/MSPA independently before going live — see the [TCF & GPP Registration Guide](https://consenti.dev/docs/compliance/tcf-and-gpp-registration).
|
|
16
18
|
- **Automatic geo-resolution** — detects visitor jurisdiction client-side (timezone + language) or server-side (`/resolve-profile`)
|
|
17
19
|
- **GPC aware** — Global Privacy Control signal detection with `'honor'` / `'strict'` / `'ignore'` modes per profile
|
|
18
20
|
- **GTM / Google Consent Mode v2** — built-in `dataLayer` integration
|
|
19
21
|
- **SSR-safe** — all browser API access is guarded; `new ConsentiSetup()` during SSR is a silent no-op
|
|
20
22
|
- **Fully typed** — ships TypeScript definitions; no `@types` needed
|
|
23
|
+
- **Companion CLI**: [`@consenti/scanner`](../scanner) crawls a site under none/reject-all/accept-all consent states and reports undeclared third-party trackers
|
|
21
24
|
|
|
22
25
|
---
|
|
23
26
|
|
|
@@ -35,17 +38,15 @@ ES2020+ · Chrome 80+ · Firefox 74+ · Safari 13.1+
|
|
|
35
38
|
npm install @consenti/ui
|
|
36
39
|
```
|
|
37
40
|
|
|
38
|
-
Then import
|
|
41
|
+
Then import the class:
|
|
39
42
|
|
|
40
43
|
```ts
|
|
41
44
|
import { ConsentiSetup } from '@consenti/ui'
|
|
42
|
-
import '@consenti/ui/dist/index.css'
|
|
43
45
|
```
|
|
44
46
|
|
|
45
47
|
### CDN / UMD (no build step)
|
|
46
48
|
|
|
47
49
|
```html
|
|
48
|
-
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@consenti/ui/dist/index.css" />
|
|
49
50
|
<script src="https://cdn.jsdelivr.net/npm/@consenti/ui/dist/index.umd.js"></script>
|
|
50
51
|
<script>
|
|
51
52
|
const { ConsentiSetup } = ConsentiUI
|
|
@@ -63,12 +64,15 @@ import '@consenti/ui/dist/index.css'
|
|
|
63
64
|
```
|
|
64
65
|
|
|
65
66
|
### CSS options
|
|
67
|
+
Choose any approach to load stylesheet, by default it will be auto injected
|
|
66
68
|
|
|
67
69
|
| Approach | How |
|
|
68
70
|
|-----------------------------------|-----------------------------------------------------------------------|
|
|
69
|
-
|
|
|
71
|
+
| Auto Injection (recommended) | by default consenti will inject css |
|
|
72
|
+
| Manual injection (avoids FOUC) | `import '@consenti/ui/dist/index.css'` in your bundler entry or code and set `disableCssTemplate: true;` |
|
|
70
73
|
| Skip all CSS (bring your own) | Set `core.disableCssTemplate: true` — no styles injected |
|
|
71
74
|
| CSS custom properties only | Import CSS, then override `--consenti-*` variables in your stylesheet |
|
|
75
|
+
| core.theme config in ConsentiSetup| Provide css variables for your stylesheet in the setup as js object |
|
|
72
76
|
|
|
73
77
|
---
|
|
74
78
|
|
|
@@ -107,21 +111,30 @@ A consent banner appears on first visit. Everything below is optional.
|
|
|
107
111
|
|
|
108
112
|
```ts
|
|
109
113
|
import { ConsentiSetup } from '@consenti/ui'
|
|
110
|
-
import '@consenti/ui/dist/index.css'
|
|
111
114
|
|
|
112
115
|
const widget = new ConsentiSetup({
|
|
113
116
|
// ── Core (optional) ──────────────────────────────────────────────────────────
|
|
114
117
|
core: {
|
|
115
118
|
tenantId: 'acme', // identifies your tenant's profiles; default: 'default'
|
|
116
119
|
locale: 'en', // BCP 47; 'auto' = navigator.language; default: 'auto'
|
|
117
|
-
dir: 'auto', // 'ltr' | 'rtl' | 'auto'; 'auto' derives from locale
|
|
118
|
-
// (
|
|
120
|
+
dir: 'auto', // 'ltr' | 'rtl' | 'auto'; 'auto' derives from locale via
|
|
121
|
+
// Intl.Locale(...).getTextInfo().direction (falls back to a
|
|
122
|
+
// hand-maintained ar/he/fa/ur/ps/sd/ug/yi → rtl list on
|
|
123
|
+
// engines without getTextInfo() support); default: 'auto'
|
|
119
124
|
storage: 'cookie', // 'cookie' | 'localStorage'; default: 'cookie'
|
|
125
|
+
cookieName: 'consenti_data', // cookie/localStorage key name; default: 'consenti_data'
|
|
126
|
+
// not switched automatically by detected region — set
|
|
127
|
+
// explicitly if you want a different name (e.g. 'euconsent-v2',
|
|
128
|
+
// only meaningful if you're using the spec-correct binary TCF
|
|
129
|
+
// encoder — @consenti/api + @iabtechlabtcf/core — since that
|
|
130
|
+
// name implies IAB's binary format, not Consenti's own encoding)
|
|
120
131
|
cookieDomains: '.example.com', // comma-separated; first entry used as Domain attribute
|
|
121
132
|
cookieSigningKey: 'min-32-char-secret', // HMAC-SHA256 signing; implicit from presence
|
|
122
133
|
allowReceipt: true, // allow consent receipt download; default: false
|
|
123
134
|
disableCssTemplate: false, // skip all style injection; default: false
|
|
124
135
|
userId: 'server-assigned-uuid', // authenticated users — enables cross-device sync
|
|
136
|
+
// prefer widget.getUserId()/setUserId() (or the
|
|
137
|
+
// consenti:listener:identify event) to change it after init
|
|
125
138
|
|
|
126
139
|
// Pre-built profile fallback
|
|
127
140
|
usePrebuiltProfiles: 'all', // 'all' (default) | ['opt-in', 'opt-out', ...] (non-empty)
|
|
@@ -131,24 +144,31 @@ const widget = new ConsentiSetup({
|
|
|
131
144
|
console: ['error'], // log levels emitted: 'info' | 'log' | 'warning' | 'error'
|
|
132
145
|
// default: ['error'] only — no noise in production
|
|
133
146
|
|
|
147
|
+
// Field names mirror their --consenti-* CSS variable literally (--consenti-color-primary
|
|
148
|
+
// → colorPrimary). Every field is optional and maps 1:1 to one CSS custom property — see
|
|
149
|
+
// apps/ui/src/styles/_variables.scss for the full set of ~33 variables. Common ones:
|
|
134
150
|
theme: {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
151
|
+
colorBg: '#ffffff',
|
|
152
|
+
colorText: '#1a1a1a',
|
|
153
|
+
colorTextMuted: '#949dab',
|
|
154
|
+
colorPrimary: '#1565c0',
|
|
155
|
+
colorPrimaryText: '#ffffff',
|
|
156
|
+
colorSecondary: '#f0f4f8',
|
|
157
|
+
colorSecondaryText: '#1a3460',
|
|
158
|
+
colorBorder: '#e2e8f0',
|
|
159
|
+
colorAccent: '#d32f2f',
|
|
160
|
+
colorAccentText: '#ffffff',
|
|
144
161
|
fontFamily: 'system-ui, sans-serif',
|
|
145
162
|
fontSizeBase: '14px',
|
|
146
163
|
fontSizeHeading: '18px',
|
|
147
164
|
fontSizeMultiplier: '1',
|
|
148
165
|
borderRadius: '8px',
|
|
149
|
-
|
|
166
|
+
borderRadiusBtn: '4px',
|
|
150
167
|
toggleBgOn: '#1565c0',
|
|
151
168
|
toggleBgOff: '#cccccc',
|
|
169
|
+
// ...plus colorSecondaryBorder, colorOverlay, fontFamilyMono, fontWeightHeading,
|
|
170
|
+
// lineHeight, spacingXs/Sm/Md/Lg, shadow, toggleBgPartial, toggleKnob,
|
|
171
|
+
// toggleWidth/Height, zBanner/Overlay/Modal
|
|
152
172
|
},
|
|
153
173
|
},
|
|
154
174
|
|
|
@@ -166,12 +186,12 @@ const widget = new ConsentiSetup({
|
|
|
166
186
|
// WidgetCountryResolverFn → custom async function: () => Promise<{ country, region, confidence }>
|
|
167
187
|
geoDataProvider: 'default',
|
|
168
188
|
|
|
169
|
-
//
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
189
|
+
// Only meaningful when api.enabled = false (ignored, with a warning, otherwise — the server
|
|
190
|
+
// resolves the group in that mode). Only overrides country→group mapping; country/region
|
|
191
|
+
// detection above always uses the embedded geo data.
|
|
192
|
+
// 'default' → embedded map (default) | a URL string (fetched; the browser's own HTTP cache
|
|
193
|
+
// honors whatever Cache-Control/ETag the response sends) | an inline ComplianceMapData object
|
|
194
|
+
complianceMap: 'default',
|
|
175
195
|
|
|
176
196
|
// TCF (IAB Transparency & Consent Framework) client stub (optional) — see "TCF" section below
|
|
177
197
|
tcf: {
|
|
@@ -179,6 +199,16 @@ const widget = new ConsentiSetup({
|
|
|
179
199
|
cmpId: 0, // must match the cmpId configured on the backend (TcfConfig)
|
|
180
200
|
cmpVersion: 1,
|
|
181
201
|
},
|
|
202
|
+
|
|
203
|
+
// GPP (IAB Global Privacy Platform, US National section) client stub (optional) — see "GPP" section below
|
|
204
|
+
gpp: {
|
|
205
|
+
enabled: false,
|
|
206
|
+
cmpId: 0, // must match the cmpId configured on the backend (GppConfig)
|
|
207
|
+
cmpVersion: 1,
|
|
208
|
+
mspaCoveredTransaction: false, // no honest default — required
|
|
209
|
+
mspaOptOutOptionMode: 0, // 0 = not applicable | 1 = yes | 2 = no
|
|
210
|
+
mspaServiceProviderMode: 0,
|
|
211
|
+
},
|
|
182
212
|
},
|
|
183
213
|
|
|
184
214
|
// ── Mount point (optional) ────────────────────────────────────────────────
|
|
@@ -309,30 +339,67 @@ Set `api.trustDomain: true` to bypass this check (useful for localhost / dev env
|
|
|
309
339
|
|
|
310
340
|
GPC mode is set per-profile on the server (`gpcMode: 'honor' | 'strict' | 'ignore'`). Widget config profileOverrides overrides the profile value when explicitly set.
|
|
311
341
|
|
|
342
|
+
`gpcMode` only applies once the widget itself has loaded and initialized. If a tag-loading script
|
|
343
|
+
sits earlier in `<head>` (GTM/gtag.js, or another script), it can still fire before that. Use
|
|
344
|
+
`buildSyncGpcSnippet()` to close that gap — it returns a tiny, dependency-free `<script>` string to
|
|
345
|
+
place first in `<head>`, ahead of everything else, that checks `navigator.globalPrivacyControl`
|
|
346
|
+
synchronously and pre-freezes Google Consent Mode v2 to denied:
|
|
347
|
+
|
|
348
|
+
```ts
|
|
349
|
+
import { buildSyncGpcSnippet } from '@consenti/ui'
|
|
350
|
+
|
|
351
|
+
buildSyncGpcSnippet() // default dataLayer name
|
|
352
|
+
buildSyncGpcSnippet({ dataLayerName: 'myDataLayer' }) // custom dataLayer name
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
Safe to run before Consenti's own `gtm` config pushes its defaults on init — same values, same
|
|
356
|
+
stub-queue, so the second push is a no-op rather than a conflict. Full walkthrough with the
|
|
357
|
+
`<head>` ordering: [Advanced Configuration → GPC](https://consenti.dev/docs/ui/advanced-configuration).
|
|
358
|
+
|
|
312
359
|
---
|
|
313
360
|
|
|
314
361
|
## Age Gate
|
|
315
362
|
|
|
316
|
-
|
|
317
|
-
|
|
363
|
+
Age gate is a **per-profile, per-locale** setting — not a widget-config option. GDPR Article 8's
|
|
364
|
+
consent age varies 13–16 by EU member state and DPDPA has its own child-data age rules, so one
|
|
365
|
+
global age doesn't fit a multi-region deployment. Enable it on a profile in the dashboard's Profile
|
|
366
|
+
Editor Step 1 (minimum age, optional parental-consent requirement), and author the modal's
|
|
367
|
+
heading/body/button text per locale in the Main Banner content step — the widget reads it off the
|
|
368
|
+
resolved profile (`profile.ageGate`/`profile.ageGateModal`) automatically, no widget config needed.
|
|
369
|
+
For a standalone/local profile (`registerProfile()`), set `ageGate`/`ageGateModal` directly on the
|
|
370
|
+
`EmbeddedProfile`/`EmbeddedTranslations` you register.
|
|
318
371
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
compliance: {
|
|
322
|
-
ageGate: { enabled: true, minimumAge: 16, requireParentalConsent: true },
|
|
323
|
-
},
|
|
324
|
-
})
|
|
325
|
-
```
|
|
372
|
+
When enabled, it blocks every other consent UI (banner, GPC, CCPA opt-out — all of it waits behind
|
|
373
|
+
the age gate on first visit):
|
|
326
374
|
|
|
327
375
|
- **Confirmed** (visitor is old enough) → the normal banner/GPC/CCPA flow proceeds exactly as if
|
|
328
376
|
the age gate didn't exist, and every consent submission from then on carries `ageVerified: true`.
|
|
329
377
|
- **Declined, `requireParentalConsent: false`** → a deny-all consent is submitted immediately
|
|
330
378
|
(mandatory/strictly-necessary cookies still granted), no banner shown, `ageVerified: false`.
|
|
331
379
|
- **Declined, `requireParentalConsent: true`** → same deny-all submission, plus a
|
|
332
|
-
`parentalConsentToken` is
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
`
|
|
380
|
+
`parentalConsentToken` is requested from the backend (`POST /consent/:visitorId/parental-consent-request`,
|
|
381
|
+
signed with the backend's `compliance.dataSigningHash` when configured — falls back to a
|
|
382
|
+
client-generated, unsigned token if `api.enabled` is false) and a
|
|
383
|
+
`consenti:parentalConsentRequired` event fires carrying it. There's no email-sending pipeline
|
|
384
|
+
built into this package — the event (or your backend's `eventBus.on('consent.parentalConsentRequired', ...)`
|
|
385
|
+
listener, see the `@consenti/api` README's "Parental consent" section) is the hook for wiring
|
|
386
|
+
your own out-of-band verification process.
|
|
387
|
+
|
|
388
|
+
When the parent later completes that process (often on a fresh page with no live widget instance —
|
|
389
|
+
their own device, not the child's), resolve the token with the standalone `resolveParentalConsent`
|
|
390
|
+
export instead of a `ConsentiSetup` method:
|
|
391
|
+
|
|
392
|
+
```ts
|
|
393
|
+
import { resolveParentalConsent } from '@consenti/ui'
|
|
394
|
+
|
|
395
|
+
const { visitorId, profileId } = await resolveParentalConsent(token, { baseUrl: 'https://consent.example.com' })
|
|
396
|
+
// dispatches consenti:parentalConsentResolved on window — a page with a live widget instance
|
|
397
|
+
// can listen for it and react (e.g. re-show the banner for the real consent choice); what
|
|
398
|
+
// "resolved" should actually do to consent state is host-defined, same as the request side.
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
This is plumbing, not verifiable parental consent by itself — see the `@consenti/api` README for
|
|
402
|
+
the full caveats (token replay isn't prevented, staying stateless is a deliberate tradeoff).
|
|
336
403
|
|
|
337
404
|
The prompt only asks once per visitor per profile — it doesn't re-appear once any consent record
|
|
338
405
|
exists (same "already decided" check the banner itself uses).
|
|
@@ -359,9 +426,50 @@ may own `window.__tcfapi` per page — if it's already set (a real CMP, or anoth
|
|
|
359
426
|
instance on a multi-profile page), this stub does not overwrite it.
|
|
360
427
|
|
|
361
428
|
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
|
|
363
|
-
metadata (`gvlVersion`, `publisherCC`) is
|
|
364
|
-
client-side
|
|
429
|
+
default format the backend's `tcfString` uses), not the full IAB binary bitfield encoding, and
|
|
430
|
+
vendor-list metadata (`gvlVersion`, `publisherCC`) is operator-supplied via `TcfWidgetConfig`
|
|
431
|
+
rather than fetched client-side (the GVL is multi-megabyte; IAB policy expects CMPs to cache their
|
|
432
|
+
own copy server-side rather than have every visitor's browser re-fetch it). `gdprApplies` is
|
|
433
|
+
derived automatically from the resolved profile's compliance group.
|
|
434
|
+
|
|
435
|
+
For spec-correct binary TC-string encoding, run with the `@consenti/api` backend, set
|
|
436
|
+
`compliance.tcf.publisherCC`, and install the optional `@iabtechlabtcf/core` peer dependency
|
|
437
|
+
(the actively-maintained IAB Tech Lab package — not `iabtcf-core`, which doesn't exist on npm).
|
|
438
|
+
See the [TCF & GPP Registration Guide](https://consenti.dev/docs/compliance/tcf-and-gpp-registration) for details.
|
|
439
|
+
|
|
440
|
+
---
|
|
441
|
+
|
|
442
|
+
## GPP — IAB Global Privacy Platform (US National)
|
|
443
|
+
|
|
444
|
+
Set `compliance.gpp.enabled: true` to install `window.__gpp`, the standard IAB entry point
|
|
445
|
+
scripts call for US state-privacy-law signals (`ping`, `addEventListener`, `removeEventListener`,
|
|
446
|
+
`getSection`, `hasSection`) — `usnat` (US National) section only:
|
|
447
|
+
|
|
448
|
+
```ts
|
|
449
|
+
new ConsentiSetup({
|
|
450
|
+
compliance: {
|
|
451
|
+
gpp: {
|
|
452
|
+
enabled: true,
|
|
453
|
+
cmpId: 280, // must match the backend's GppConfig
|
|
454
|
+
cmpVersion: 1,
|
|
455
|
+
mspaCoveredTransaction: true, // whether this deployment falls under MSPA signatory obligations
|
|
456
|
+
mspaOptOutOptionMode: 1, // 0 = not applicable | 1 = yes | 2 = no
|
|
457
|
+
mspaServiceProviderMode: 0,
|
|
458
|
+
},
|
|
459
|
+
},
|
|
460
|
+
})
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
Sale/sharing opt-out flags are derived automatically from the resolved profile's cookies tagged
|
|
464
|
+
`cpraCategory: 'sale'` / `'sharing'` and the visitor's actual consent — no separate config needed.
|
|
465
|
+
Like TCF, only one CMP may own `window.__gpp` per page; this stub does not overwrite an existing one,
|
|
466
|
+
and follows the standard stub-queue convention so load order relative to third-party scripts doesn't
|
|
467
|
+
matter.
|
|
468
|
+
|
|
469
|
+
Unlike TCF, GPP's US National section needs no Global Vendor List, so both this stub's `gppString`
|
|
470
|
+
and the backend's `gppString` use the same real, spec-correct encoder when the optional
|
|
471
|
+
`@iabgpp/cmpapi` peer dependency is installed on `@consenti/api` — there's no "simplified fallback"
|
|
472
|
+
format the way TCF has. Without it, `gppString` falls back to a base64url-encoded JSON payload.
|
|
365
473
|
|
|
366
474
|
---
|
|
367
475
|
|
|
@@ -433,7 +541,7 @@ const profile = new ConsentiProfile({
|
|
|
433
541
|
// key = machine id, rendered as the button's DOM id: `consenti-btn-{id}`, for targeting a specific button
|
|
434
542
|
// cookies: '*' = grant all | '!' = deny all | ['id1','id2'] = grant specific
|
|
435
543
|
'accept-all': { text: 'Accept All', style: 'primary', action: 'custom', cookies: '*' },
|
|
436
|
-
'reject-optional': { text: 'Reject Optional', style: '
|
|
544
|
+
'reject-optional': { text: 'Reject Optional', style: 'primary', action: 'custom', cookies: '!' }, // same weight as accept-all — CNIL requires reject not be visually subordinate
|
|
437
545
|
customize: { text: 'Customize', style: 'secondary', action: 'manage' },
|
|
438
546
|
'privacy-policy': { text: 'Privacy Policy', style: 'text', action: 'link', url: '/privacy' },
|
|
439
547
|
},
|
|
@@ -652,11 +760,19 @@ widget.deleteConsent() // delete consent record (cookie + backend if e
|
|
|
652
760
|
widget.reConsent() // delete consent and re-show banner
|
|
653
761
|
widget.destroy() // unmount DOM elements and remove all event listeners
|
|
654
762
|
|
|
763
|
+
// Logged-in user identity
|
|
764
|
+
widget.getUserId() // string | null — current application user ID
|
|
765
|
+
widget.setUserId('user-42') // reconsents (by default) if it differs from the stored cookie's user
|
|
766
|
+
widget.setUserId(null) // clear identity (e.g. logout)
|
|
767
|
+
widget.setUserId('user-42', false) // update identity without reconsenting
|
|
768
|
+
// Or dispatch from anywhere (e.g. right after your own login flow completes):
|
|
769
|
+
// window.dispatchEvent(new CustomEvent('consenti:listener:identify', { detail: { userId: 'user-42' } }))
|
|
770
|
+
|
|
655
771
|
// Runtime configuration
|
|
656
772
|
widget.setDarkMode() // toggle dark mode
|
|
657
773
|
widget.setDarkMode(true) // force dark on
|
|
658
774
|
widget.setDarkMode(false) // force light
|
|
659
|
-
widget.setTheme({
|
|
775
|
+
widget.setTheme({ colorPrimary: '#ff0000' }) // hot-swap CSS tokens (merges into current theme)
|
|
660
776
|
widget.setConfig({ darkMode: true }) // deep-merge partial config (no re-init)
|
|
661
777
|
widget.setProfile({ mainBanner: { heading: 'Updated' } }) // re-render UI with new profile data
|
|
662
778
|
|
|
@@ -743,6 +859,14 @@ Custom DOM events fire on `window` at every lifecycle step. All are prefixed `co
|
|
|
743
859
|
| `consenti:consentSubmitted` | Consent saved (cookie written + API call if configured) |
|
|
744
860
|
| `consenti:parentalConsentRequired` | Age gate declined with `requireParentalConsent: true` — carries `parentalConsentToken` |
|
|
745
861
|
|
|
862
|
+
The events above are **outbound** (widget → host). `consenti:listener:*` is the opposite
|
|
863
|
+
direction — **inbound** (host → widget), dispatched by your own code to tell the widget
|
|
864
|
+
something:
|
|
865
|
+
|
|
866
|
+
| Event | Dispatch when |
|
|
867
|
+
|--------------------------------|----------------------------------------------------------|
|
|
868
|
+
| `consenti:listener:identify` | Your own login/logout flow completes — `detail: { userId: string \| null, reConsent?: boolean }`, equivalent to calling `widget.setUserId(userId, reConsent)` |
|
|
869
|
+
|
|
746
870
|
**Recommended:** use `widget.on()` / `widget.off()` for typed subscriptions (the `consenti:` prefix is optional):
|
|
747
871
|
|
|
748
872
|
```ts
|
|
@@ -843,20 +967,64 @@ new ConsentScript({
|
|
|
843
967
|
})
|
|
844
968
|
```
|
|
845
969
|
|
|
846
|
-
###
|
|
970
|
+
### ConsentAction — run a callback on a parameter's grant status
|
|
971
|
+
|
|
972
|
+
For integrations that expose their own `sdk.optIn()`/`sdk.optOut()` API (Segment, Mixpanel,
|
|
973
|
+
Amplitude, Sentry, etc.) instead of a `<script>` tag — use `ConsentScript` for that case instead.
|
|
974
|
+
|
|
975
|
+
```ts
|
|
976
|
+
import { ConsentAction } from '@consenti/ui'
|
|
977
|
+
|
|
978
|
+
new ConsentAction({
|
|
979
|
+
id: 'analytics_storage',
|
|
980
|
+
widget,
|
|
981
|
+
onGrant: () => analyticsSdk.optIn(),
|
|
982
|
+
onDeny: () => analyticsSdk.optOut(),
|
|
983
|
+
// bind: true (default) — re-fires on every future consent change
|
|
984
|
+
})
|
|
985
|
+
```
|
|
986
|
+
|
|
987
|
+
### CategoryScript / CategoryAction — gate on a whole category
|
|
988
|
+
|
|
989
|
+
Same as `ConsentScript`/`ConsentAction`, but keyed on a preference-modal category instead of a
|
|
990
|
+
single parameter — "granted" only when *every* parameter in the category is `'granted'`.
|
|
991
|
+
|
|
992
|
+
```ts
|
|
993
|
+
import { CategoryScript, CategoryAction } from '@consenti/ui'
|
|
994
|
+
|
|
995
|
+
new CategoryScript({ categoryId: 'marketing', widget, src: 'https://example.com/ad-pixel.js' })
|
|
996
|
+
|
|
997
|
+
new CategoryAction({
|
|
998
|
+
id: 'marketing',
|
|
999
|
+
widget,
|
|
1000
|
+
onGrant: () => adSdk.enableAll(),
|
|
1001
|
+
onDeny: () => adSdk.disableAll(),
|
|
1002
|
+
})
|
|
1003
|
+
```
|
|
1004
|
+
|
|
1005
|
+
### scanConsentScripts — declarative `data-*` script gating
|
|
1006
|
+
|
|
1007
|
+
Zero-JS integration path: mark `<script>` tags with `data-consenti-consent-script` /
|
|
1008
|
+
`data-consenti-category-script` and Consenti scans and wires them up automatically at the end of
|
|
1009
|
+
every `init()` cycle (call `scanConsentScripts(widget)` again manually after adding tags at runtime).
|
|
1010
|
+
|
|
1011
|
+
```html
|
|
1012
|
+
<script type="text/plain" data-consenti-consent-script="analytics_storage" src="https://www.googletagmanager.com/gtag/js?id=G-XXXXX"></script>
|
|
1013
|
+
<script type="text/plain" data-consenti-category-script="marketing" src="https://example.com/pixel.js"></script>
|
|
1014
|
+
<script type="text/plain" data-consenti-consent-script="ad_storage" data-consenti-bind="false">/* inline snippet, evaluated once */</script>
|
|
1015
|
+
```
|
|
1016
|
+
|
|
1017
|
+
### BannerTrigger — open banner or modal from any element
|
|
847
1018
|
|
|
848
1019
|
```ts
|
|
849
|
-
import {
|
|
1020
|
+
import { BannerTrigger } from '@consenti/ui'
|
|
850
1021
|
|
|
851
1022
|
// Attach to existing element
|
|
852
|
-
new
|
|
1023
|
+
new BannerTrigger({ widget, el: '#cookie-settings', action: 'modal' })
|
|
853
1024
|
|
|
854
1025
|
// Auto-create a button
|
|
855
|
-
new
|
|
856
|
-
|
|
857
|
-
label: 'Manage Cookies',
|
|
858
|
-
appendTo: document.querySelector('#footer'),
|
|
859
|
-
})
|
|
1026
|
+
const trigger = new BannerTrigger({ widget, action: 'modal', label: 'Manage Cookies' })
|
|
1027
|
+
document.querySelector('#footer')?.appendChild(trigger.getElement())
|
|
860
1028
|
```
|
|
861
1029
|
|
|
862
1030
|
---
|
|
@@ -870,41 +1038,50 @@ Override any token in your own stylesheet:
|
|
|
870
1038
|
```css
|
|
871
1039
|
:root {
|
|
872
1040
|
/* Colors */
|
|
873
|
-
--consenti-
|
|
874
|
-
--consenti-
|
|
875
|
-
--consenti-
|
|
876
|
-
--consenti-
|
|
877
|
-
--consenti-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
--consenti-
|
|
881
|
-
--consenti-
|
|
882
|
-
--consenti-
|
|
883
|
-
--consenti-
|
|
884
|
-
--consenti-
|
|
885
|
-
--consenti-btn-padding: 0.5rem 1.25rem;
|
|
886
|
-
--consenti-btn-font-weight: 600;
|
|
887
|
-
|
|
888
|
-
/* Banner */
|
|
889
|
-
--consenti-banner-shadow: 0 -4px 24px rgba(0, 0, 0, 0.12);
|
|
890
|
-
--consenti-banner-radius: 0;
|
|
891
|
-
--consenti-banner-padding: 1.5rem;
|
|
892
|
-
--consenti-banner-max-width: 960px;
|
|
893
|
-
|
|
894
|
-
/* Modal */
|
|
895
|
-
--consenti-modal-radius: 12px;
|
|
896
|
-
--consenti-modal-shadow: 0 8px 32px rgba(0, 0, 0, 0.24);
|
|
897
|
-
--consenti-modal-width: 560px;
|
|
898
|
-
--consenti-modal-max-height: 80vh;
|
|
899
|
-
|
|
900
|
-
/* Toggle */
|
|
901
|
-
--consenti-toggle-on: #1565c0;
|
|
902
|
-
--consenti-toggle-off: #ccc;
|
|
1041
|
+
--consenti-color-bg: #ffffff;
|
|
1042
|
+
--consenti-color-text: #1a2e4a;
|
|
1043
|
+
--consenti-color-text-muted: #949dab;
|
|
1044
|
+
--consenti-color-primary: #04111f;
|
|
1045
|
+
--consenti-color-primary-text: #ffffff;
|
|
1046
|
+
--consenti-color-secondary: #f0f4f8;
|
|
1047
|
+
--consenti-color-secondary-text: #1a2e4a;
|
|
1048
|
+
--consenti-color-border: #dbe4ee;
|
|
1049
|
+
--consenti-color-secondary-border: #1a2e4a;
|
|
1050
|
+
--consenti-color-overlay: #04111f;
|
|
1051
|
+
--consenti-color-accent: #d32f2f;
|
|
1052
|
+
--consenti-color-accent-text: #ffffff;
|
|
903
1053
|
|
|
904
1054
|
/* Typography */
|
|
905
|
-
--consenti-font-size-base: 14px;
|
|
906
|
-
--consenti-font-size-mult: 1;
|
|
907
1055
|
--consenti-font-family: system-ui, -apple-system, sans-serif;
|
|
1056
|
+
--consenti-font-family-mono: ui-monospace, monospace;
|
|
1057
|
+
--consenti-font-size-base: 14px;
|
|
1058
|
+
--consenti-font-size-heading: 16px;
|
|
1059
|
+
--consenti-font-weight-heading: 600;
|
|
1060
|
+
--consenti-line-height: 1.5;
|
|
1061
|
+
|
|
1062
|
+
/* Spacing */
|
|
1063
|
+
--consenti-spacing-xs: 5px;
|
|
1064
|
+
--consenti-spacing-sm: 8px;
|
|
1065
|
+
--consenti-spacing-md: 16px;
|
|
1066
|
+
--consenti-spacing-lg: 24px;
|
|
1067
|
+
|
|
1068
|
+
/* Shape */
|
|
1069
|
+
--consenti-border-radius: 8px;
|
|
1070
|
+
--consenti-border-radius-btn: 0;
|
|
1071
|
+
--consenti-shadow: 0 4px 24px rgba(21, 101, 192, 0.14);
|
|
1072
|
+
|
|
1073
|
+
/* Toggle (preference modal) */
|
|
1074
|
+
--consenti-toggle-bg-on: #43a047;
|
|
1075
|
+
--consenti-toggle-bg-partial: #97c098;
|
|
1076
|
+
--consenti-toggle-bg-off: #9ca3af;
|
|
1077
|
+
--consenti-toggle-knob: #ffffff;
|
|
1078
|
+
--consenti-toggle-width: 52px;
|
|
1079
|
+
--consenti-toggle-height: 28px;
|
|
1080
|
+
|
|
1081
|
+
/* Stacking */
|
|
1082
|
+
--consenti-z-banner: 9999;
|
|
1083
|
+
--consenti-z-overlay: 9998;
|
|
1084
|
+
--consenti-z-modal: 10000;
|
|
908
1085
|
}
|
|
909
1086
|
```
|
|
910
1087
|
|
|
@@ -915,10 +1092,10 @@ new ConsentiSetup({
|
|
|
915
1092
|
compliance: { type: 'opt-in' },
|
|
916
1093
|
core: {
|
|
917
1094
|
theme: {
|
|
918
|
-
|
|
919
|
-
|
|
1095
|
+
colorPrimary: '#7c3aed', // purple brand
|
|
1096
|
+
colorPrimaryText: '#ffffff',
|
|
920
1097
|
borderRadius: '12px',
|
|
921
|
-
|
|
1098
|
+
borderRadiusBtn: '999px', // pill buttons
|
|
922
1099
|
fontFamily: 'Inter, sans-serif',
|
|
923
1100
|
},
|
|
924
1101
|
},
|
|
@@ -983,7 +1160,6 @@ new ConsentiSetup({
|
|
|
983
1160
|
|
|
984
1161
|
import { useEffect } from 'react'
|
|
985
1162
|
import { ConsentiSetup } from '@consenti/ui'
|
|
986
|
-
import '@consenti/ui/dist/index.css'
|
|
987
1163
|
|
|
988
1164
|
export function ConsentSetup() {
|
|
989
1165
|
useEffect(() => {
|
|
@@ -1036,7 +1212,6 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|
|
1036
1212
|
|
|
1037
1213
|
import { useEffect, useRef } from 'react'
|
|
1038
1214
|
import type { ConsentiSetup as WidgetType } from '@consenti/ui'
|
|
1039
|
-
import '@consenti/ui/dist/index.css'
|
|
1040
1215
|
|
|
1041
1216
|
export function ConsentSetup() {
|
|
1042
1217
|
const widgetRef = useRef<WidgetType | null>(null)
|
|
@@ -1067,7 +1242,6 @@ let widget: unknown = null
|
|
|
1067
1242
|
|
|
1068
1243
|
onMounted(async () => {
|
|
1069
1244
|
const { ConsentiSetup } = await import('@consenti/ui')
|
|
1070
|
-
await import('@consenti/ui/dist/index.css')
|
|
1071
1245
|
widget = new ConsentiSetup({ compliance: { type: 'opt-in' } })
|
|
1072
1246
|
})
|
|
1073
1247
|
|
|
@@ -1106,15 +1280,18 @@ export class ConsentService implements OnDestroy {
|
|
|
1106
1280
|
}
|
|
1107
1281
|
```
|
|
1108
1282
|
|
|
1109
|
-
Or use the built-in Angular
|
|
1283
|
+
Or use the built-in Angular integration (`setConsentiWidget()` once at app root, then
|
|
1284
|
+
`injectConsent()` in any component/service):
|
|
1110
1285
|
|
|
1111
1286
|
```ts
|
|
1112
|
-
import {
|
|
1287
|
+
import { injectConsent } from '@consenti/ui/angular'
|
|
1113
1288
|
|
|
1114
1289
|
@Component({ /* ... */ })
|
|
1115
1290
|
export class MyComponent {
|
|
1116
|
-
consent =
|
|
1291
|
+
consent = injectConsent()
|
|
1117
1292
|
// consent.hasConsent() | consent.showModal() | consent.getConsent()
|
|
1293
|
+
// consent.onConsentChange(cb) / onBannerChange(cb) / onModalChange(cb) — call in
|
|
1294
|
+
// ngOnInit, unsubscribe with the returned function in ngOnDestroy
|
|
1118
1295
|
}
|
|
1119
1296
|
```
|
|
1120
1297
|
|
|
@@ -1122,7 +1299,6 @@ export class MyComponent {
|
|
|
1122
1299
|
|
|
1123
1300
|
```js
|
|
1124
1301
|
import { ConsentiSetup } from '@consenti/ui'
|
|
1125
|
-
import '@consenti/ui/dist/index.css'
|
|
1126
1302
|
|
|
1127
1303
|
const widget = new ConsentiSetup({ compliance: { type: 'opt-in' }, core: { locale: 'en' } })
|
|
1128
1304
|
|
|
@@ -1227,10 +1403,8 @@ new ConsentiSetup({
|
|
|
1227
1403
|
// Arabic site — RTL derived automatically from locale
|
|
1228
1404
|
new ConsentiSetup({ core: { locale: 'ar' } })
|
|
1229
1405
|
|
|
1230
|
-
// COPPA — block under-13s, require parental consent
|
|
1231
|
-
|
|
1232
|
-
compliance: { ageGate: { enabled: true, minimumAge: 13, requireParentalConsent: true } },
|
|
1233
|
-
})
|
|
1406
|
+
// COPPA — block under-13s, require parental consent: set on the profile in the dashboard
|
|
1407
|
+
// (Profile Editor Step 1 → Enable age gate → minimum age 13, require parental consent), not here.
|
|
1234
1408
|
|
|
1235
1409
|
// TCF — install the __tcfapi stub for ad-tech scripts (cmpId must match the backend's tcf config)
|
|
1236
1410
|
new ConsentiSetup({
|
|
@@ -1250,11 +1424,11 @@ import type {
|
|
|
1250
1424
|
CoreConfig, // core section
|
|
1251
1425
|
ApiConfig, // api section
|
|
1252
1426
|
ComplianceWidgetConfig, // compliance section
|
|
1253
|
-
AgeGateWidgetConfig, // compliance.ageGate section
|
|
1254
1427
|
WidgetCountryResolverFn, // custom geo resolver function type
|
|
1255
1428
|
UtilsConfig, // utils section
|
|
1256
1429
|
GtmConfig, // utils.gtm section
|
|
1257
1430
|
ThemeConfig, // core.theme section
|
|
1431
|
+
IdentifyEventDetail, // consenti:listener:identify event detail
|
|
1258
1432
|
ConsentValue, // 'granted' | 'denied' | 'objected'
|
|
1259
1433
|
ConsentiProfile, // local profile class
|
|
1260
1434
|
NonEmptyArray, // [T, ...T[]] — used by usePrebuiltProfiles
|
|
@@ -1274,6 +1448,17 @@ The following `CoreConfig` fields were **removed** in v0.1.x:
|
|
|
1274
1448
|
|
|
1275
1449
|
---
|
|
1276
1450
|
|
|
1451
|
+
## Security notes
|
|
1452
|
+
|
|
1453
|
+
**`core.cookieSigningKey` is spoofable in standalone mode.** Without a `@consenti/api` backend,
|
|
1454
|
+
this key has nowhere to live but the shipped browser bundle — so anyone can read it out of your JS
|
|
1455
|
+
and re-sign a forged cookie with it. It still catches accidental tampering (e.g. a stale value from
|
|
1456
|
+
an older config), but it is not tamper-*proof* against a motivated attacker. If you need consent
|
|
1457
|
+
records that hold up as evidence, sign them server-side instead with `compliance.dataSigningHash`
|
|
1458
|
+
(`@consenti/api`), where the key never reaches the browser.
|
|
1459
|
+
|
|
1460
|
+
---
|
|
1461
|
+
|
|
1277
1462
|
## Contributing
|
|
1278
1463
|
|
|
1279
1464
|
See [CONTRIBUTING.md](../../CONTRIBUTING.md) in the monorepo root.
|