@consenti/ui 0.1.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.
@@ -0,0 +1,452 @@
1
+ type ConsentStatus = 'granted' | 'denied' | 'objected';
2
+ type ConsentValue = Record<string, ConsentStatus>;
3
+ interface Cookie {
4
+ id: string;
5
+ type?: 'consent' | 'legitimate_interest';
6
+ mandatory?: boolean;
7
+ listenGpc?: boolean;
8
+ expiry?: number;
9
+ tcfVendorId?: number;
10
+ tcfPurposes?: number[];
11
+ cpraCategory?: 'sale' | 'sharing' | 'sensitive';
12
+ }
13
+ type ButtonStyle = 'primary' | 'secondary' | 'text' | 'accent';
14
+ type ButtonAction = 'submit' | 'manage' | 'close' | 'custom' | 'link';
15
+ interface Button {
16
+ text: string;
17
+ style: ButtonStyle;
18
+ action: ButtonAction;
19
+ cookies?: string[] | '*' | '!';
20
+ url?: string;
21
+ }
22
+ interface MainBanner {
23
+ position: 'top' | 'bottom' | 'middle' | 'left-bottom' | 'right-bottom';
24
+ overlayOpacity?: number;
25
+ showClose?: boolean;
26
+ showLocaleSwitcher?: boolean;
27
+ heading?: string;
28
+ headingTag?: string;
29
+ htmlText: string;
30
+ buttons: Button[];
31
+ }
32
+ type GpcBanner = MainBanner;
33
+ interface Category {
34
+ id: string;
35
+ heading: string;
36
+ headingTag?: string;
37
+ htmlText: string;
38
+ mandatory?: boolean;
39
+ type?: 'consent' | 'legitimate_interest';
40
+ legitimateInterest?: {
41
+ enabled?: boolean;
42
+ description?: string;
43
+ };
44
+ cookies: string[];
45
+ }
46
+ interface PreferenceModal {
47
+ position?: 'left' | 'right' | 'center';
48
+ overlayOpacity?: number;
49
+ showClose?: boolean;
50
+ showLocaleSwitcher?: boolean;
51
+ heading?: string;
52
+ headingTag?: string;
53
+ subheading?: string;
54
+ htmlText?: string;
55
+ buttons: Button[];
56
+ categories: Category[];
57
+ persistent?: boolean;
58
+ }
59
+ interface DpdpaConfig {
60
+ dataFiduciary: string;
61
+ grievanceEmail: string;
62
+ purposeDescription?: string;
63
+ }
64
+ interface ResolvedProfile {
65
+ id: number;
66
+ version: number;
67
+ defaultLocale: string;
68
+ locales?: string[];
69
+ cookies: Cookie[];
70
+ mainBanner: MainBanner;
71
+ gpcBanner?: GpcBanner;
72
+ preferenceModal: PreferenceModal;
73
+ cookieSigningKey?: string;
74
+ dpdpa?: DpdpaConfig;
75
+ darkMode?: boolean;
76
+ }
77
+ interface GtmConfig {
78
+ containerId?: string;
79
+ events?: string[];
80
+ dataLayer?: string;
81
+ urlPassthrough?: boolean;
82
+ adsDataRedaction?: boolean;
83
+ }
84
+ interface UtilsConfig {
85
+ gtm?: GtmConfig;
86
+ }
87
+ interface ApiConfig {
88
+ enabled?: boolean;
89
+ baseUrl?: string;
90
+ authToken?: string;
91
+ }
92
+ interface ThemeConfig {
93
+ bgColor?: string;
94
+ textColor?: string;
95
+ primaryColor?: string;
96
+ primaryTextColor?: string;
97
+ secondaryColor?: string;
98
+ secondaryTextColor?: string;
99
+ borderColor?: string;
100
+ fontFamily?: string;
101
+ fontSizeBase?: string;
102
+ fontSizeHeading?: string;
103
+ fontSizeMultiplier?: string;
104
+ borderRadius?: string;
105
+ buttonBorderRadius?: string;
106
+ toggleBgOn?: string;
107
+ toggleBgOff?: string;
108
+ accentColor?: string;
109
+ accentTextColor?: string;
110
+ }
111
+ interface CoreConfig {
112
+ profileId?: number;
113
+ autoHonorGPC?: boolean | 'strict';
114
+ locale?: string;
115
+ disableCssTemplate?: boolean;
116
+ signCookies?: boolean;
117
+ cookieSigningKey?: string;
118
+ allowReceipt?: boolean;
119
+ cookieDomains?: string;
120
+ storage?: 'cookie' | 'localStorage';
121
+ theme?: ThemeConfig;
122
+ privacyPolicyUrl?: string;
123
+ regulation?: 'gdpr' | 'ccpa' | 'cpra' | 'dpdpa' | 'uk-gdpr' | 'lgpd' | 'pipeda' | 'popia' | 'pdpa-th' | 'appi' | 'kvkk';
124
+ userId?: string;
125
+ }
126
+ interface ConsentiConfig {
127
+ core: CoreConfig;
128
+ rootEl?: string | HTMLElement;
129
+ darkMode?: boolean;
130
+ autoInit?: boolean;
131
+ api?: ApiConfig;
132
+ utils?: UtilsConfig;
133
+ plugins?: ConsentiPlugin[];
134
+ profileOverride?: Partial<ResolvedProfile>;
135
+ }
136
+ interface ConsentiWidgetAPI {
137
+ hasConsent(): boolean;
138
+ getConsent(): ConsentValue | null;
139
+ getConsentDate(): Date | false;
140
+ getGTMConsent(): Record<string, string> | null;
141
+ bannerVisibility(): 'main' | 'gpc' | false;
142
+ modalVisibility(): 'preference' | false;
143
+ getProfile(): ResolvedProfile | null;
144
+ showBanner(gpc?: boolean): void;
145
+ hideBanner(): void;
146
+ showModal(triggerEl?: HTMLElement): void;
147
+ hideModal(): void;
148
+ submitConsent(consent: Partial<ConsentValue>): Promise<void>;
149
+ deleteConsent(): Promise<void>;
150
+ reConsent(): Promise<void>;
151
+ getRootElement(): HTMLElement | null;
152
+ getBannerElement(): HTMLElement | null;
153
+ getModalElement(): HTMLElement | null;
154
+ switchLocale(locale: string): void;
155
+ init(): Promise<void>;
156
+ onReady(callback: () => void): void;
157
+ destroy(): void;
158
+ }
159
+ declare abstract class ConsentiPlugin {
160
+ abstract initialize(widget: ConsentiWidgetAPI): void | Promise<void>;
161
+ abstract destroy(): void;
162
+ onConsentSubmit?(consent: ConsentValue): void | Promise<void>;
163
+ onBannerShow?(): void | Promise<void>;
164
+ onBannerHide?(): void | Promise<void>;
165
+ onModalShow?(): void | Promise<void>;
166
+ onModalHide?(): void | Promise<void>;
167
+ }
168
+
169
+ /**
170
+ * Main entry point for the Consenti widget.
171
+ *
172
+ * Instantiate one `ConsentiSetup` per page (or per profile ID if you run multiple
173
+ * consent contexts on the same page). The constructor is safe to call during SSR —
174
+ * it silently no-ops when `window` is not defined.
175
+ *
176
+ * @example
177
+ * ```ts
178
+ * const widget = new ConsentiSetup({
179
+ * core: { profileId: 0 },
180
+ * })
181
+ * await widget.ready
182
+ * console.log(widget.hasConsent())
183
+ * ```
184
+ */
185
+
186
+ declare class ConsentiSetup implements ConsentiWidgetAPI {
187
+ private config;
188
+ private initialized;
189
+ private initializing;
190
+ private profile;
191
+ private consentStore;
192
+ private eventBus;
193
+ private banner;
194
+ private modal;
195
+ private renderer;
196
+ private broadcastChannel;
197
+ private readyResolve;
198
+ private readyPromise;
199
+ private plugins;
200
+ /** Signature from the most recent `writeAndSign` call. Included in consent receipts. */
201
+ private lastSignature;
202
+ /** Tracks which banner variant is currently visible, or null when hidden. */
203
+ private _bannerVariant;
204
+ /**
205
+ * Creates and initialises the consent widget.
206
+ *
207
+ * Construction is synchronous; the async `init()` is launched in the background.
208
+ * Use `await widget.ready` or `widget.onReady(cb)` to run code after init completes.
209
+ *
210
+ * @param config - Widget configuration. See `ConsentiConfig` for all options.
211
+ */
212
+ constructor(config: ConsentiConfig);
213
+ private profileId;
214
+ /**
215
+ * Starts widget initialisation. Called automatically unless `autoInit: false` is set.
216
+ * Can be called explicitly after construction when `autoInit: false`, or after `destroy()`
217
+ * to re-initialise the same instance.
218
+ */
219
+ init(): Promise<void>;
220
+ /** Applies the `consenti-root--dark` class based on config or profile flag. */
221
+ private applyDarkMode;
222
+ /**
223
+ * Injects CSS custom property overrides supplied via `core.theme` into the
224
+ * Consenti root element so they cascade down to all widget elements.
225
+ */
226
+ private applyTheme;
227
+ private runPlugins;
228
+ /**
229
+ * Invokes an optional plugin hook on all registered plugins.
230
+ * Async hooks are awaited; errors are caught and logged so a broken plugin
231
+ * never interferes with the consent flow.
232
+ *
233
+ * @param invoke - Function that calls the desired hook method on a single plugin.
234
+ */
235
+ private callPluginHook;
236
+ private buildHandlers;
237
+ /**
238
+ * Classifies a consent submission as `accept_all`, `reject_all`, `custom`, or `update`.
239
+ * Used to enrich events and API payloads with intent metadata.
240
+ */
241
+ private deriveAction;
242
+ private handleBroadcastMessage;
243
+ /**
244
+ * Returns `true` if the visitor has saved consent for the active profile.
245
+ * Always returns `false` during SSR.
246
+ */
247
+ hasConsent(): boolean;
248
+ /**
249
+ * Returns the stored consent map, or `null` if no consent has been saved.
250
+ * Always returns `null` during SSR.
251
+ */
252
+ getConsent(): ConsentValue | null;
253
+ /**
254
+ * Returns the `Date` when consent was last saved, or `false` if no consent exists.
255
+ * Always returns `false` during SSR.
256
+ */
257
+ getConsentDate(): Date | false;
258
+ /**
259
+ * Returns the consent map in Google Consent Mode v2 format, ready to pass to
260
+ * `gtag('consent', 'update', ...)`. Returns `null` if no consent has been saved.
261
+ *
262
+ * Maps Consenti cookie IDs to the seven Google Consent Mode v2 parameters.
263
+ * Cookies whose IDs exactly match a GCM v2 key are forwarded directly.
264
+ * Non-GCM cookies are ignored by this method (they appear in `getConsent()` only).
265
+ */
266
+ getGTMConsent(): Record<string, string> | null;
267
+ /**
268
+ * Records and persists the user's consent choices.
269
+ *
270
+ * Mandatory cookies are always forced to `'granted'` regardless of what is passed.
271
+ * Non-mandatory cookies not present in `consent` default to `'denied'`.
272
+ *
273
+ * @param consent - Partial or full map of cookie ID → status.
274
+ * @param gpcDerived - Internal flag set to `true` when called from the GPC strict path.
275
+ */
276
+ submitConsent(consent: Partial<ConsentValue>, gpcDerived?: boolean): Promise<void>;
277
+ /**
278
+ * Returns the currently visible banner variant, or `false` if no banner is shown.
279
+ * Useful for programmatically checking state before calling `showBanner` / `hideBanner`.
280
+ */
281
+ bannerVisibility(): 'main' | 'gpc' | false;
282
+ /**
283
+ * Returns `'preference'` when the preference modal is open, or `false` when closed.
284
+ */
285
+ modalVisibility(): 'preference' | false;
286
+ /**
287
+ * Mounts and shows the consent banner.
288
+ *
289
+ * No-ops if the requested variant is already visible. If a different variant is
290
+ * currently shown (e.g. switching from main to GPC), the existing banner is replaced.
291
+ *
292
+ * @param useGpcVariant - When `true` and the profile has a `gpcBanner`, that variant
293
+ * is shown instead of `mainBanner`. Used by the GPC flow.
294
+ */
295
+ showBanner(useGpcVariant?: boolean): void;
296
+ /** Hides the banner and notifies other tabs via BroadcastChannel. No-op if already hidden. */
297
+ hideBanner(): void;
298
+ /**
299
+ * Opens the preference modal and hides the banner while the modal is open.
300
+ * No-ops if the modal is already open.
301
+ * If the modal is closed without saving consent, the banner is re-shown automatically
302
+ * (see `hideModal()`). Only one banner or modal is visible at a time.
303
+ *
304
+ * @param triggerEl - The element that triggered the modal open. Focus returns here on close.
305
+ */
306
+ showModal(triggerEl?: HTMLElement): void;
307
+ /**
308
+ * Closes the preference modal and returns focus to the element that opened it.
309
+ * No-ops if the modal is already closed.
310
+ * If the user has not yet given consent, the banner is re-shown automatically.
311
+ */
312
+ hideModal(): void;
313
+ /**
314
+ * Registers a callback to run once the widget has finished initialising.
315
+ * If the widget is already initialised, the callback is invoked synchronously.
316
+ *
317
+ * @param callback - Function to call when the widget is ready.
318
+ */
319
+ onReady(callback: () => void): void;
320
+ /**
321
+ * A `Promise` that resolves once the widget has finished initialising.
322
+ * Equivalent to wrapping `onReady()` in a `Promise`.
323
+ *
324
+ * @example
325
+ * ```ts
326
+ * await widget.ready
327
+ * console.log('Widget ready')
328
+ * ```
329
+ */
330
+ get ready(): Promise<void>;
331
+ /**
332
+ * Deletes the stored consent record and notifies other tabs.
333
+ * The banner will reappear on the next page load (or immediately if the
334
+ * receiving tab calls `showBanner()` in response to `consentDeleted`).
335
+ */
336
+ deleteConsent(): Promise<void>;
337
+ /**
338
+ * Removes existing consent and immediately re-shows the consent banner so the user
339
+ * can review and update their preferences.
340
+ *
341
+ * Equivalent to calling `deleteConsent()` followed by `showBanner()`, but in a single
342
+ * method so callers do not need to coordinate the async deletion with the banner display.
343
+ *
344
+ * Use this to power "Review my consent" links in footers or privacy dashboards.
345
+ */
346
+ reConsent(): Promise<void>;
347
+ /**
348
+ * Returns the fully resolved profile for the current session, or `null` during the
349
+ * async init phase (before the profile has been fetched/resolved).
350
+ *
351
+ * Plugins can use this to read cookie definitions, banner config, modal categories,
352
+ * and the profile version without accessing internal state.
353
+ */
354
+ getProfile(): ResolvedProfile | null;
355
+ /**
356
+ * Returns the `#consenti-root` container element that wraps all widget DOM,
357
+ * or `null` before the widget has mounted or after `destroy()`.
358
+ *
359
+ * Plugins can use this to append custom elements, apply extra CSS variables,
360
+ * or observe mutations within the widget container.
361
+ */
362
+ getRootElement(): HTMLElement | null;
363
+ /**
364
+ * Returns the `#consenti-banner` element when it is present in the DOM, or `null`.
365
+ * The element may be hidden (`display: none`) — check `bannerVisibility()` for visibility state.
366
+ *
367
+ * Useful for adding extra content, custom event listeners, or mutation observers on the banner.
368
+ */
369
+ getBannerElement(): HTMLElement | null;
370
+ /**
371
+ * Returns the `#consenti-modal` element when the modal has been built, or `null`.
372
+ * The element is built on first `showModal()` call and reused on subsequent opens.
373
+ *
374
+ * Useful for injecting extra content into the modal or listening to modal-specific events.
375
+ */
376
+ getModalElement(): HTMLElement | null;
377
+ /**
378
+ * Switches the active locale, re-resolves the profile, and re-renders the widget.
379
+ * Called by the locale switcher UI when the user selects a different language.
380
+ *
381
+ * @param locale - BCP 47 locale code to switch to (e.g. `'fr'`, `'de-AT'`).
382
+ */
383
+ switchLocale(locale: string): void;
384
+ /**
385
+ * Destroys the widget — removes all DOM elements, disconnects the BroadcastChannel,
386
+ * and calls `destroy()` on all registered plugins.
387
+ *
388
+ * After `destroy()`, the instance should not be used again.
389
+ */
390
+ destroy(): void;
391
+ }
392
+
393
+ /**
394
+ * React integration for `@consenti/ui`.
395
+ *
396
+ * Import from the `@consenti/ui/react` subpath to keep this module
397
+ * tree-shaken out of vanilla JS or Vue bundles.
398
+ *
399
+ * Intended for **Next.js** (App Router and Pages Router) and any other React 18+
400
+ * project where you manage the `ConsentiSetup` instance yourself and want the
401
+ * `useConsent()` hook to re-render when consent changes.
402
+ *
403
+ * @example
404
+ * ```tsx
405
+ * // app/providers.tsx (Next.js App Router)
406
+ * 'use client'
407
+ * import { useEffect } from 'react'
408
+ * import { ConsentiSetup } from '@consenti/ui'
409
+ * import { setConsentiWidget } from '@consenti/ui/react'
410
+ *
411
+ * export function ConsentiProvider({ children }: { children: React.ReactNode }) {
412
+ * useEffect(() => {
413
+ * const widget = new ConsentiSetup({ core: { profileId: 0 } })
414
+ * setConsentiWidget(widget)
415
+ * }, [])
416
+ * return <>{children}</>
417
+ * }
418
+ *
419
+ * // Any component
420
+ * import { useConsent } from '@consenti/ui/react'
421
+ *
422
+ * function CookieBanner() {
423
+ * const { hasConsent, submitConsent } = useConsent()
424
+ * if (hasConsent()) return null
425
+ * return <button onClick={() => submitConsent({ analytics: 'granted' })}>Accept</button>
426
+ * }
427
+ * ```
428
+ */
429
+
430
+ /**
431
+ * Registers the active `ConsentiSetup` instance so `useConsent()` can read from it.
432
+ * Call this once after creating your widget, typically inside a `useEffect`.
433
+ *
434
+ * @param widget - The `ConsentiSetup` instance to register.
435
+ */
436
+ declare function setConsentiWidget(widget: ConsentiSetup): void;
437
+ /**
438
+ * React hook that exposes consent state and lets components react to consent changes.
439
+ *
440
+ * Subscribes to `consenti:consentSubmitted` on `window` and triggers a re-render
441
+ * each time consent is updated. Returns SSR-safe fallback values during server render.
442
+ *
443
+ * @returns An object with `hasConsent`, `getConsent`, `submitConsent`, and `getConsentDate`.
444
+ */
445
+ declare function useConsent(): {
446
+ hasConsent: () => boolean;
447
+ getConsent: () => ConsentValue | null;
448
+ submitConsent: (_consent: Partial<ConsentValue>) => Promise<void>;
449
+ getConsentDate: () => Date | false;
450
+ };
451
+
452
+ export { setConsentiWidget, useConsent };
package/dist/react.js ADDED
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/react.ts
21
+ var react_exports = {};
22
+ __export(react_exports, {
23
+ setConsentiWidget: () => setConsentiWidget,
24
+ useConsent: () => useConsent
25
+ });
26
+ module.exports = __toCommonJS(react_exports);
27
+ var import_react = require("react");
28
+ var _widget = null;
29
+ function setConsentiWidget(widget) {
30
+ _widget = widget;
31
+ }
32
+ function useConsent() {
33
+ const isServer = typeof window === "undefined";
34
+ const [, forceUpdate] = (0, import_react.useState)(0);
35
+ (0, import_react.useEffect)(() => {
36
+ if (!_widget) return;
37
+ const handler = () => forceUpdate((n) => n + 1);
38
+ window.addEventListener("consenti:consentSubmitted", handler);
39
+ return () => window.removeEventListener("consenti:consentSubmitted", handler);
40
+ }, []);
41
+ if (isServer) {
42
+ return {
43
+ hasConsent: () => false,
44
+ getConsent: () => null,
45
+ submitConsent: async (_consent) => {
46
+ },
47
+ getConsentDate: () => false
48
+ };
49
+ }
50
+ return {
51
+ /** Returns `true` if the visitor has saved consent. */
52
+ hasConsent: () => _widget?.hasConsent() ?? false,
53
+ /** Returns the stored consent map, or `null`. */
54
+ getConsent: () => _widget?.getConsent() ?? null,
55
+ /** Saves the given partial consent map. Missing keys default to `'denied'`. */
56
+ submitConsent: (consent) => _widget?.submitConsent(consent) ?? Promise.resolve(),
57
+ /** Returns the `Date` of last consent submission, or `false`. */
58
+ getConsentDate: () => _widget?.getConsentDate() ?? false
59
+ };
60
+ }
61
+ //# sourceMappingURL=react.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/react.ts"],"sourcesContent":["/**\n * React integration for `@consenti/ui`.\n *\n * Import from the `@consenti/ui/react` subpath to keep this module\n * tree-shaken out of vanilla JS or Vue bundles.\n *\n * Intended for **Next.js** (App Router and Pages Router) and any other React 18+\n * project where you manage the `ConsentiSetup` instance yourself and want the\n * `useConsent()` hook to re-render when consent changes.\n *\n * @example\n * ```tsx\n * // app/providers.tsx (Next.js App Router)\n * 'use client'\n * import { useEffect } from 'react'\n * import { ConsentiSetup } from '@consenti/ui'\n * import { setConsentiWidget } from '@consenti/ui/react'\n *\n * export function ConsentiProvider({ children }: { children: React.ReactNode }) {\n * useEffect(() => {\n * const widget = new ConsentiSetup({ core: { profileId: 0 } })\n * setConsentiWidget(widget)\n * }, [])\n * return <>{children}</>\n * }\n *\n * // Any component\n * import { useConsent } from '@consenti/ui/react'\n *\n * function CookieBanner() {\n * const { hasConsent, submitConsent } = useConsent()\n * if (hasConsent()) return null\n * return <button onClick={() => submitConsent({ analytics: 'granted' })}>Accept</button>\n * }\n * ```\n */\n\nimport { useState, useEffect } from 'react'\nimport type { ConsentValue } from './types'\nimport type { ConsentiSetup } from './core/consenti-setup'\n\n/**\n * Module-level singleton — the active `ConsentiSetup` instance.\n * Using a module-level variable (rather than React context) keeps the integration\n * zero-configuration: no `<ConsentiProvider>` wrapper is required.\n */\nlet _widget: ConsentiSetup | null = null\n\n/**\n * Registers the active `ConsentiSetup` instance so `useConsent()` can read from it.\n * Call this once after creating your widget, typically inside a `useEffect`.\n *\n * @param widget - The `ConsentiSetup` instance to register.\n */\nexport function setConsentiWidget(widget: ConsentiSetup): void {\n _widget = widget\n}\n\n/**\n * React hook that exposes consent state and lets components react to consent changes.\n *\n * Subscribes to `consenti:consentSubmitted` on `window` and triggers a re-render\n * each time consent is updated. Returns SSR-safe fallback values during server render.\n *\n * @returns An object with `hasConsent`, `getConsent`, `submitConsent`, and `getConsentDate`.\n */\nexport function useConsent() {\n const isServer = typeof window === 'undefined'\n\n const [, forceUpdate] = useState(0)\n\n useEffect(() => {\n if (!_widget) return\n const handler = () => forceUpdate((n) => n + 1)\n window.addEventListener('consenti:consentSubmitted', handler)\n return () => window.removeEventListener('consenti:consentSubmitted', handler)\n }, [])\n\n if (isServer) {\n return {\n hasConsent: () => false as boolean,\n getConsent: () => null as ConsentValue | null,\n submitConsent: async (_consent: Partial<ConsentValue>) => {},\n getConsentDate: () => false as Date | false,\n }\n }\n\n return {\n /** Returns `true` if the visitor has saved consent. */\n hasConsent: () => _widget?.hasConsent() ?? false,\n /** Returns the stored consent map, or `null`. */\n getConsent: () => _widget?.getConsent() ?? null,\n /** Saves the given partial consent map. Missing keys default to `'denied'`. */\n submitConsent: (consent: Partial<ConsentValue>) =>\n _widget?.submitConsent(consent) ?? Promise.resolve(),\n /** Returns the `Date` of last consent submission, or `false`. */\n getConsentDate: () => _widget?.getConsentDate() ?? false,\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAqCA,mBAAoC;AASpC,IAAI,UAAgC;AAQ7B,SAAS,kBAAkB,QAA6B;AAC7D,YAAU;AACZ;AAUO,SAAS,aAAa;AAC3B,QAAM,WAAW,OAAO,WAAW;AAEnC,QAAM,CAAC,EAAE,WAAW,QAAI,uBAAS,CAAC;AAElC,8BAAU,MAAM;AACd,QAAI,CAAC,QAAS;AACd,UAAM,UAAU,MAAM,YAAY,CAAC,MAAM,IAAI,CAAC;AAC9C,WAAO,iBAAiB,6BAA6B,OAAO;AAC5D,WAAO,MAAM,OAAO,oBAAoB,6BAA6B,OAAO;AAAA,EAC9E,GAAG,CAAC,CAAC;AAEL,MAAI,UAAU;AACZ,WAAO;AAAA,MACL,YAAY,MAAM;AAAA,MAClB,YAAY,MAAM;AAAA,MAClB,eAAe,OAAO,aAAoC;AAAA,MAAC;AAAA,MAC3D,gBAAgB,MAAM;AAAA,IACxB;AAAA,EACF;AAEA,SAAO;AAAA;AAAA,IAEL,YAAY,MAAM,SAAS,WAAW,KAAK;AAAA;AAAA,IAE3C,YAAY,MAAM,SAAS,WAAW,KAAK;AAAA;AAAA,IAE3C,eAAe,CAAC,YACd,SAAS,cAAc,OAAO,KAAK,QAAQ,QAAQ;AAAA;AAAA,IAErD,gBAAgB,MAAM,SAAS,eAAe,KAAK;AAAA,EACrD;AACF;","names":[]}
package/dist/react.mjs ADDED
@@ -0,0 +1,40 @@
1
+ // src/react.ts
2
+ import { useState, useEffect } from "react";
3
+ var _widget = null;
4
+ function setConsentiWidget(widget) {
5
+ _widget = widget;
6
+ }
7
+ function useConsent() {
8
+ const isServer = typeof window === "undefined";
9
+ const [, forceUpdate] = useState(0);
10
+ useEffect(() => {
11
+ if (!_widget) return;
12
+ const handler = () => forceUpdate((n) => n + 1);
13
+ window.addEventListener("consenti:consentSubmitted", handler);
14
+ return () => window.removeEventListener("consenti:consentSubmitted", handler);
15
+ }, []);
16
+ if (isServer) {
17
+ return {
18
+ hasConsent: () => false,
19
+ getConsent: () => null,
20
+ submitConsent: async (_consent) => {
21
+ },
22
+ getConsentDate: () => false
23
+ };
24
+ }
25
+ return {
26
+ /** Returns `true` if the visitor has saved consent. */
27
+ hasConsent: () => _widget?.hasConsent() ?? false,
28
+ /** Returns the stored consent map, or `null`. */
29
+ getConsent: () => _widget?.getConsent() ?? null,
30
+ /** Saves the given partial consent map. Missing keys default to `'denied'`. */
31
+ submitConsent: (consent) => _widget?.submitConsent(consent) ?? Promise.resolve(),
32
+ /** Returns the `Date` of last consent submission, or `false`. */
33
+ getConsentDate: () => _widget?.getConsentDate() ?? false
34
+ };
35
+ }
36
+ export {
37
+ setConsentiWidget,
38
+ useConsent
39
+ };
40
+ //# sourceMappingURL=react.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/react.ts"],"sourcesContent":["/**\n * React integration for `@consenti/ui`.\n *\n * Import from the `@consenti/ui/react` subpath to keep this module\n * tree-shaken out of vanilla JS or Vue bundles.\n *\n * Intended for **Next.js** (App Router and Pages Router) and any other React 18+\n * project where you manage the `ConsentiSetup` instance yourself and want the\n * `useConsent()` hook to re-render when consent changes.\n *\n * @example\n * ```tsx\n * // app/providers.tsx (Next.js App Router)\n * 'use client'\n * import { useEffect } from 'react'\n * import { ConsentiSetup } from '@consenti/ui'\n * import { setConsentiWidget } from '@consenti/ui/react'\n *\n * export function ConsentiProvider({ children }: { children: React.ReactNode }) {\n * useEffect(() => {\n * const widget = new ConsentiSetup({ core: { profileId: 0 } })\n * setConsentiWidget(widget)\n * }, [])\n * return <>{children}</>\n * }\n *\n * // Any component\n * import { useConsent } from '@consenti/ui/react'\n *\n * function CookieBanner() {\n * const { hasConsent, submitConsent } = useConsent()\n * if (hasConsent()) return null\n * return <button onClick={() => submitConsent({ analytics: 'granted' })}>Accept</button>\n * }\n * ```\n */\n\nimport { useState, useEffect } from 'react'\nimport type { ConsentValue } from './types'\nimport type { ConsentiSetup } from './core/consenti-setup'\n\n/**\n * Module-level singleton — the active `ConsentiSetup` instance.\n * Using a module-level variable (rather than React context) keeps the integration\n * zero-configuration: no `<ConsentiProvider>` wrapper is required.\n */\nlet _widget: ConsentiSetup | null = null\n\n/**\n * Registers the active `ConsentiSetup` instance so `useConsent()` can read from it.\n * Call this once after creating your widget, typically inside a `useEffect`.\n *\n * @param widget - The `ConsentiSetup` instance to register.\n */\nexport function setConsentiWidget(widget: ConsentiSetup): void {\n _widget = widget\n}\n\n/**\n * React hook that exposes consent state and lets components react to consent changes.\n *\n * Subscribes to `consenti:consentSubmitted` on `window` and triggers a re-render\n * each time consent is updated. Returns SSR-safe fallback values during server render.\n *\n * @returns An object with `hasConsent`, `getConsent`, `submitConsent`, and `getConsentDate`.\n */\nexport function useConsent() {\n const isServer = typeof window === 'undefined'\n\n const [, forceUpdate] = useState(0)\n\n useEffect(() => {\n if (!_widget) return\n const handler = () => forceUpdate((n) => n + 1)\n window.addEventListener('consenti:consentSubmitted', handler)\n return () => window.removeEventListener('consenti:consentSubmitted', handler)\n }, [])\n\n if (isServer) {\n return {\n hasConsent: () => false as boolean,\n getConsent: () => null as ConsentValue | null,\n submitConsent: async (_consent: Partial<ConsentValue>) => {},\n getConsentDate: () => false as Date | false,\n }\n }\n\n return {\n /** Returns `true` if the visitor has saved consent. */\n hasConsent: () => _widget?.hasConsent() ?? false,\n /** Returns the stored consent map, or `null`. */\n getConsent: () => _widget?.getConsent() ?? null,\n /** Saves the given partial consent map. Missing keys default to `'denied'`. */\n submitConsent: (consent: Partial<ConsentValue>) =>\n _widget?.submitConsent(consent) ?? Promise.resolve(),\n /** Returns the `Date` of last consent submission, or `false`. */\n getConsentDate: () => _widget?.getConsentDate() ?? false,\n }\n}\n"],"mappings":";AAqCA,SAAS,UAAU,iBAAiB;AASpC,IAAI,UAAgC;AAQ7B,SAAS,kBAAkB,QAA6B;AAC7D,YAAU;AACZ;AAUO,SAAS,aAAa;AAC3B,QAAM,WAAW,OAAO,WAAW;AAEnC,QAAM,CAAC,EAAE,WAAW,IAAI,SAAS,CAAC;AAElC,YAAU,MAAM;AACd,QAAI,CAAC,QAAS;AACd,UAAM,UAAU,MAAM,YAAY,CAAC,MAAM,IAAI,CAAC;AAC9C,WAAO,iBAAiB,6BAA6B,OAAO;AAC5D,WAAO,MAAM,OAAO,oBAAoB,6BAA6B,OAAO;AAAA,EAC9E,GAAG,CAAC,CAAC;AAEL,MAAI,UAAU;AACZ,WAAO;AAAA,MACL,YAAY,MAAM;AAAA,MAClB,YAAY,MAAM;AAAA,MAClB,eAAe,OAAO,aAAoC;AAAA,MAAC;AAAA,MAC3D,gBAAgB,MAAM;AAAA,IACxB;AAAA,EACF;AAEA,SAAO;AAAA;AAAA,IAEL,YAAY,MAAM,SAAS,WAAW,KAAK;AAAA;AAAA,IAE3C,YAAY,MAAM,SAAS,WAAW,KAAK;AAAA;AAAA,IAE3C,eAAe,CAAC,YACd,SAAS,cAAc,OAAO,KAAK,QAAQ,QAAQ;AAAA;AAAA,IAErD,gBAAgB,MAAM,SAAS,eAAe,KAAK;AAAA,EACrD;AACF;","names":[]}
@@ -0,0 +1,2 @@
1
+ // CSS side-effect import — no exports
2
+ export {}
@@ -0,0 +1,109 @@
1
+ type ConsentStatus = 'granted' | 'denied' | 'objected';
2
+ type ConsentValue = Record<string, ConsentStatus>;
3
+
4
+ /**
5
+ * Test utilities for `@consenti/ui`.
6
+ *
7
+ * Import via the `/testing` subpath export so these helpers are never bundled
8
+ * into production code:
9
+ *
10
+ * ```ts
11
+ * import {
12
+ * createMockConsenti,
13
+ * mockConsent,
14
+ * simulateConsentSubmitted,
15
+ * } from '@consenti/ui/testing'
16
+ * ```
17
+ *
18
+ * The helpers work in any JavaScript environment that provides a DOM
19
+ * (browsers, jsdom, happy-dom). They carry no runtime dependencies and
20
+ * do not import any Consenti internals — safe to use in unit tests without
21
+ * initialising the full widget.
22
+ */
23
+
24
+ /**
25
+ * Builds a `ConsentValue` map from explicit grant/deny lists.
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * const consent = mockConsent(
30
+ * ['analytics_storage'], // granted
31
+ * ['ad_storage', 'ad_user_data'], // denied
32
+ * )
33
+ * // → { analytics_storage: 'granted', ad_storage: 'denied', ad_user_data: 'denied' }
34
+ * ```
35
+ */
36
+ declare function mockConsent(granted?: string[], denied?: string[]): ConsentValue;
37
+ /**
38
+ * Returns a `ConsentValue` with every listed cookie set to `'granted'`.
39
+ * Shorthand for the common "user accepted everything" test scenario.
40
+ */
41
+ declare function mockAllGranted(cookieIds: string[]): ConsentValue;
42
+ /**
43
+ * Returns a `ConsentValue` with every listed cookie set to `'denied'`.
44
+ * Shorthand for the common "user rejected everything" test scenario.
45
+ */
46
+ declare function mockAllDenied(cookieIds: string[]): ConsentValue;
47
+ /**
48
+ * Dispatches `consenti:consentSubmitted` on `window` with the supplied consent
49
+ * map. Use this to trigger code paths that listen to consent changes without
50
+ * going through the full widget initialisation flow.
51
+ */
52
+ declare function simulateConsentSubmitted(consent: ConsentValue): void;
53
+ /**
54
+ * Dispatches `consenti:bannerInitialized` on `window`.
55
+ * Use this to simulate widget initialisation in tests that gate on the event.
56
+ */
57
+ declare function simulateBannerInitialized(hasExistingConsent?: boolean): void;
58
+ /**
59
+ * Dispatches `consenti:consentBeingSubmitted` on `window` before a simulated
60
+ * consent submission. Use in tests that capture both events.
61
+ */
62
+ declare function simulateConsentBeingSubmitted(consent: ConsentValue): void;
63
+ /**
64
+ * Shape returned by {@link createMockConsenti}.
65
+ * Mirrors the public API of `ConsentiSetup` for use in unit tests.
66
+ */
67
+ interface MockConsentiSetup {
68
+ hasConsent: () => boolean;
69
+ getConsent: () => ConsentValue | null;
70
+ getConsentDate: () => Date | false;
71
+ getGTMConsent: () => Record<string, string> | null;
72
+ submitConsent: (consent: Partial<ConsentValue>) => Promise<void>;
73
+ deleteConsent: () => Promise<void>;
74
+ showBanner: (useGpcVariant?: boolean) => void;
75
+ hideBanner: () => void;
76
+ showModal: () => void;
77
+ hideModal: () => void;
78
+ bannerVisibility: () => 'main' | 'gpc' | false;
79
+ modalVisibility: () => 'preference' | false;
80
+ onReady: (cb: () => void) => void;
81
+ readonly ready: Promise<void>;
82
+ destroy: () => void;
83
+ reConsent: () => Promise<void>;
84
+ }
85
+ /**
86
+ * Creates a minimal in-memory mock of `ConsentiSetup` for use in unit tests.
87
+ *
88
+ * All methods are no-ops or return sensible defaults. Pass an initial `consent`
89
+ * map to simulate a returning visitor who has already given consent.
90
+ *
91
+ * The mock's `submitConsent` updates internal state and fires
92
+ * `simulateConsentSubmitted` so event listeners in the component under test
93
+ * are triggered exactly as in production.
94
+ *
95
+ * @example
96
+ * ```ts
97
+ * const widget = createMockConsenti(
98
+ * mockConsent(['analytics_storage'], ['ad_storage'])
99
+ * )
100
+ * expect(widget.hasConsent()).toBe(true)
101
+ * expect(widget.getConsent()?.analytics_storage).toBe('granted')
102
+ *
103
+ * await widget.submitConsent({ ad_storage: 'granted' })
104
+ * expect(widget.getConsent()?.ad_storage).toBe('granted')
105
+ * ```
106
+ */
107
+ declare function createMockConsenti(consent?: ConsentValue | null): MockConsentiSetup;
108
+
109
+ export { type MockConsentiSetup, createMockConsenti, mockAllDenied, mockAllGranted, mockConsent, simulateBannerInitialized, simulateConsentBeingSubmitted, simulateConsentSubmitted };