@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,481 @@
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
+ * Angular integration for `@consenti/ui`.
395
+ *
396
+ * Import from the `@consenti/ui/angular` subpath to keep this module
397
+ * tree-shaken out of vanilla JS, React, and Vue bundles.
398
+ *
399
+ * Designed for **Angular 15+** with standalone components or NgModule-based projects.
400
+ * Initialise `ConsentiSetup` once (typically in `app.component.ts` or a root service)
401
+ * and call `setConsentiWidget()` so that `injectConsent()` can read from it in any
402
+ * component or service.
403
+ *
404
+ * @example
405
+ * ```ts
406
+ * // app.component.ts
407
+ * import { Component, OnInit, OnDestroy } from '@angular/core'
408
+ * import { ConsentiSetup } from '@consenti/ui'
409
+ * import { setConsentiWidget } from '@consenti/ui/angular'
410
+ *
411
+ * @Component({ selector: 'app-root', template: '<router-outlet />' })
412
+ * export class AppComponent implements OnInit, OnDestroy {
413
+ * private widget!: ConsentiSetup
414
+ *
415
+ * ngOnInit() {
416
+ * this.widget = new ConsentiSetup({ core: { profileId: 0 } })
417
+ * setConsentiWidget(this.widget)
418
+ * }
419
+ *
420
+ * ngOnDestroy() {
421
+ * this.widget.destroy()
422
+ * }
423
+ * }
424
+ *
425
+ * // any-component.ts
426
+ * import { Component, ChangeDetectorRef, OnInit, OnDestroy } from '@angular/core'
427
+ * import { injectConsent } from '@consenti/ui/angular'
428
+ *
429
+ * @Component({ selector: 'app-cookie-status', template: `
430
+ * <p *ngIf="consent.hasConsent()">Consent given on {{ consent.getConsentDate() | date }}</p>
431
+ * <button *ngIf="!consent.hasConsent()" (click)="consent.submitConsent({ analytics_storage: 'granted' })">
432
+ * Accept Analytics
433
+ * </button>
434
+ * ` })
435
+ * export class CookieStatusComponent implements OnInit, OnDestroy {
436
+ * consent = injectConsent()
437
+ * private unsub!: () => void
438
+ *
439
+ * constructor(private cdr: ChangeDetectorRef) {}
440
+ *
441
+ * ngOnInit() {
442
+ * this.unsub = this.consent.onConsentChange(() => this.cdr.markForCheck())
443
+ * }
444
+ *
445
+ * ngOnDestroy() {
446
+ * this.unsub()
447
+ * }
448
+ * }
449
+ * ```
450
+ */
451
+
452
+ /**
453
+ * Registers the active `ConsentiSetup` instance so `injectConsent()` can read from it.
454
+ * Call this once after creating your widget, typically in `AppComponent.ngOnInit()` or
455
+ * a root-level Angular service.
456
+ *
457
+ * @param widget - The `ConsentiSetup` instance to register.
458
+ */
459
+ declare function setConsentiWidget(widget: ConsentiSetup): void;
460
+ /**
461
+ * Returns a consent accessor object for use in Angular components and services.
462
+ *
463
+ * Unlike the React and Vue integrations, Angular uses its `ChangeDetectorRef` to
464
+ * trigger UI updates rather than a reactive primitive. Call `onConsentChange()` in
465
+ * `ngOnInit` to subscribe to consent changes and `cdr.markForCheck()` inside the
466
+ * callback to schedule re-renders in `OnPush` components.
467
+ *
468
+ * Returns SSR-safe fallback values when `window` is not defined (Angular Universal).
469
+ *
470
+ * @returns An object with `hasConsent`, `getConsent`, `submitConsent`, `getConsentDate`,
471
+ * and `onConsentChange` (subscription helper).
472
+ */
473
+ declare function injectConsent(): {
474
+ hasConsent: () => boolean;
475
+ getConsent: () => ConsentValue | null;
476
+ submitConsent: (_consent: Partial<ConsentValue>) => Promise<void>;
477
+ getConsentDate: () => Date | false;
478
+ onConsentChange: (_cb: () => void) => () => void;
479
+ };
480
+
481
+ export { injectConsent, setConsentiWidget };