Add programmatic consent API to @probo/cookie-banner
Expose a ConsentManager singleton via `@probo/cookie-banner/consent` (ESM) and `window.Probo.consent` (IIFE) so customers can check and react to consent state in their bundled JavaScript code, solving the problem of third-party SDKs initialized programmatically that cannot be blocked via data-cookie-consent attributes. Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
@@ -38,6 +38,12 @@ await Promise.all([
|
||||
outfile: "dist/cookie-banner-headless.mjs",
|
||||
format: "esm",
|
||||
}),
|
||||
esbuild.build({
|
||||
...shared,
|
||||
entryPoints: ["src/consent.ts"],
|
||||
outfile: "dist/cookie-banner-consent.mjs",
|
||||
format: "esm",
|
||||
}),
|
||||
esbuild.build({
|
||||
...shared,
|
||||
entryPoints: ["src/themed-banner/iife.ts"],
|
||||
|
||||
@@ -16,6 +16,11 @@
|
||||
"types": "./dist/headless/index.d.ts",
|
||||
"import": "./dist/cookie-banner-headless.mjs",
|
||||
"default": "./dist/cookie-banner-headless.mjs"
|
||||
},
|
||||
"./consent": {
|
||||
"types": "./dist/consent.d.ts",
|
||||
"import": "./dist/cookie-banner-consent.mjs",
|
||||
"default": "./dist/cookie-banner-consent.mjs"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
deactivateElements,
|
||||
observeAndActivate,
|
||||
} from "./activation";
|
||||
import { getConsent } from "./consent";
|
||||
import { COOKIE_NAME, getConsentCookie, setConsentCookie } from "./cookie";
|
||||
import type { Detector } from "./detectors";
|
||||
import { CookieDetector, ReportQueue, ResourceDetector, StorageDetector } from "./detectors";
|
||||
@@ -109,6 +110,7 @@ export class CookieBannerClient {
|
||||
};
|
||||
this._gpcApplied = cookie.action === "GPC";
|
||||
this.activate(cookie.data);
|
||||
getConsent()._setReady(cookie.data, true);
|
||||
void flush(this.bannerId);
|
||||
return;
|
||||
}
|
||||
@@ -140,16 +142,24 @@ export class CookieBannerClient {
|
||||
config.consent_expiry_days,
|
||||
);
|
||||
this.activate(apiConsent.consent_data);
|
||||
getConsent()._setReady(apiConsent.consent_data, true);
|
||||
} else {
|
||||
this.consent = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.consent && this.gpcDetected) {
|
||||
const gpcData: Record<string, boolean> = {};
|
||||
for (const cat of config.categories) {
|
||||
gpcData[cat.slug] = cat.kind === "NECESSARY";
|
||||
}
|
||||
getConsent()._setReady(gpcData, false);
|
||||
this.gpc();
|
||||
this._gpcApplied = true;
|
||||
} else if (!this.consent) {
|
||||
this.activate(this.buildDefaultConsentData());
|
||||
const defaults = this.buildDefaultConsentData();
|
||||
this.activate(defaults);
|
||||
getConsent()._setReady(defaults, false);
|
||||
}
|
||||
|
||||
void flush(this.bannerId);
|
||||
@@ -263,6 +273,7 @@ export class CookieBannerClient {
|
||||
);
|
||||
|
||||
this.activate(consentData);
|
||||
getConsent()._notify(consentData);
|
||||
|
||||
const url = new URL(`${this.bannerId}/consents`, this.baseUrl);
|
||||
const body = {
|
||||
|
||||
91
packages/cookie-banner/src/consent.ts
Normal file
91
packages/cookie-banner/src/consent.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
export type ConsentData = Record<string, boolean>;
|
||||
type Callback = (consent: ConsentData) => void;
|
||||
|
||||
export class ConsentManager {
|
||||
private _ready = false;
|
||||
private _hasResponse = false;
|
||||
private _consent: ConsentData = {};
|
||||
private readonly _readyListeners: Callback[] = [];
|
||||
private readonly _changeListeners: Callback[] = [];
|
||||
|
||||
get ready(): boolean {
|
||||
return this._ready;
|
||||
}
|
||||
|
||||
get hasResponse(): boolean {
|
||||
return this._hasResponse;
|
||||
}
|
||||
|
||||
has(category: string): boolean {
|
||||
return !!this._consent[category];
|
||||
}
|
||||
|
||||
getAll(): ConsentData {
|
||||
return { ...this._consent };
|
||||
}
|
||||
|
||||
onReady(cb: Callback): () => void {
|
||||
if (this._ready) {
|
||||
cb({ ...this._consent });
|
||||
return () => {};
|
||||
}
|
||||
this._readyListeners.push(cb);
|
||||
return () => {
|
||||
const idx = this._readyListeners.indexOf(cb);
|
||||
if (idx !== -1) this._readyListeners.splice(idx, 1);
|
||||
};
|
||||
}
|
||||
|
||||
onChange(cb: Callback): () => void {
|
||||
this._changeListeners.push(cb);
|
||||
return () => {
|
||||
const idx = this._changeListeners.indexOf(cb);
|
||||
if (idx !== -1) this._changeListeners.splice(idx, 1);
|
||||
};
|
||||
}
|
||||
|
||||
/** @internal Called by CookieBannerClient when consent state is first resolved. */
|
||||
_setReady(consent: ConsentData, hasResponse: boolean): void {
|
||||
this._consent = consent;
|
||||
this._hasResponse = hasResponse;
|
||||
this._ready = true;
|
||||
for (const cb of this._readyListeners.splice(0)) {
|
||||
cb({ ...this._consent });
|
||||
}
|
||||
for (const cb of this._changeListeners) {
|
||||
cb({ ...this._consent });
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal Called by CookieBannerClient when consent changes after user action. */
|
||||
_notify(consent: ConsentData): void {
|
||||
this._consent = consent;
|
||||
this._hasResponse = true;
|
||||
for (const cb of this._changeListeners) {
|
||||
cb({ ...this._consent });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let instance: ConsentManager | null = null;
|
||||
|
||||
export function getConsent(): ConsentManager {
|
||||
if (!instance) {
|
||||
instance = new ConsentManager();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
@@ -12,10 +12,17 @@
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
import { getConsent } from "../consent";
|
||||
import { registerCookieBanner } from "./index";
|
||||
|
||||
registerCookieBanner();
|
||||
|
||||
const w = window as unknown as Record<string, unknown>;
|
||||
if (!w.Probo) {
|
||||
w.Probo = {};
|
||||
}
|
||||
(w.Probo as Record<string, unknown>).consent = getConsent();
|
||||
|
||||
const script = document.currentScript as HTMLScriptElement | null;
|
||||
|
||||
if (script) {
|
||||
|
||||
Reference in New Issue
Block a user