Add Global Privacy Control (GPC) support to cookie banner SDK
When navigator.globalPrivacyControl is true and no prior consent exists, auto-reject all non-necessary cookies with action "GPC", skip showing the banner, and display an "Opt-Out Preference Signal Honored" badge on the settings button (CPRA compliance). GPC labels are hardcoded in the SDK for en/fr/de/es. Users can still override via the preference panel. Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
@@ -87,6 +87,7 @@ export class CookieBannerClient {
|
||||
private consent: VisitorConsent | null = null;
|
||||
private observer: MutationObserver | null = null;
|
||||
private detector: CookieDetector | null = null;
|
||||
private _gpcApplied = false;
|
||||
|
||||
constructor(config: CookieBannerClientOptions) {
|
||||
let base = config.baseUrl;
|
||||
@@ -152,6 +153,11 @@ export class CookieBannerClient {
|
||||
this.consent = null;
|
||||
}
|
||||
|
||||
if (!this.consent && this.gpcDetected) {
|
||||
this.gpc();
|
||||
this._gpcApplied = true;
|
||||
}
|
||||
|
||||
void flush(this.bannerId);
|
||||
}
|
||||
|
||||
@@ -170,6 +176,26 @@ export class CookieBannerClient {
|
||||
return this.consent !== null;
|
||||
}
|
||||
|
||||
get gpcDetected(): boolean {
|
||||
return typeof navigator !== "undefined" &&
|
||||
(navigator as Navigator & { globalPrivacyControl?: boolean }).globalPrivacyControl === true;
|
||||
}
|
||||
|
||||
get gpcApplied(): boolean {
|
||||
return this._gpcApplied;
|
||||
}
|
||||
|
||||
gpc(): void {
|
||||
const cfg = this.config;
|
||||
|
||||
const consentData: Record<string, boolean> = {};
|
||||
for (const cat of cfg.categories) {
|
||||
consentData[cat.slug] = cat.kind === "NECESSARY";
|
||||
}
|
||||
|
||||
this.recordConsent("GPC", consentData);
|
||||
}
|
||||
|
||||
acceptAll(): void {
|
||||
const cfg = this.config;
|
||||
|
||||
|
||||
@@ -66,6 +66,7 @@ export interface ProboRootElement extends ProboElement {
|
||||
readonly state: ProboState;
|
||||
readonly reopenWidget: string;
|
||||
readonly consentDraft: ConsentDraft;
|
||||
readonly gpcApplied: boolean;
|
||||
setState(state: ProboState): void;
|
||||
updateDraft(category: string, value: boolean): void;
|
||||
}
|
||||
|
||||
@@ -53,6 +53,10 @@ export class ProboCookieBannerRoot extends ProboElement implements ProboRootElem
|
||||
return this._draft;
|
||||
}
|
||||
|
||||
get gpcApplied(): boolean {
|
||||
return this._client?.gpcApplied ?? false;
|
||||
}
|
||||
|
||||
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void {
|
||||
if (name === "reopen-widget" && oldValue !== newValue) {
|
||||
this.dispatchEvent(
|
||||
@@ -150,7 +154,7 @@ export class ProboCookieBannerRoot extends ProboElement implements ProboRootElem
|
||||
new CustomEvent("probo-ready", {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
detail: { config: this._config },
|
||||
detail: { config: this._config, gpcApplied: this.gpcApplied },
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ export class ProboSettingsButton extends HTMLElement {
|
||||
}
|
||||
|
||||
static get observedAttributes(): string[] {
|
||||
return ["position", "aria-settings-label"];
|
||||
return ["position", "aria-settings-label", "gpc-label"];
|
||||
}
|
||||
|
||||
attributeChangedCallback(name: string, _oldValue: string | null, newValue: string | null): void {
|
||||
@@ -36,6 +36,9 @@ export class ProboSettingsButton extends HTMLElement {
|
||||
btn.setAttribute("aria-label", newValue);
|
||||
}
|
||||
}
|
||||
if (name === "gpc-label") {
|
||||
this.updateGpcBadge(newValue);
|
||||
}
|
||||
}
|
||||
|
||||
private get position(): string {
|
||||
@@ -73,10 +76,19 @@ export class ProboSettingsButton extends HTMLElement {
|
||||
}
|
||||
button:hover { opacity: 0.85; }
|
||||
.icon { display: flex; flex-shrink: 0; }
|
||||
.gpc-badge {
|
||||
display: none;
|
||||
font-size: 11px;
|
||||
line-height: 1;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
:host([gpc-label]) .gpc-badge { display: inline; }
|
||||
::slotted(*) { display: contents; }
|
||||
</style>
|
||||
<button part="button" aria-label="Cookie settings">
|
||||
<span class="icon" part="icon" aria-hidden="true">${COOKIE_ICON}</span>
|
||||
<span class="gpc-badge" part="gpc-badge"></span>
|
||||
<slot></slot>
|
||||
</button>
|
||||
`;
|
||||
@@ -141,4 +153,11 @@ export class ProboSettingsButton extends HTMLElement {
|
||||
if (!this.root) return;
|
||||
this.root.setState("panel");
|
||||
};
|
||||
|
||||
private updateGpcBadge(label: string | null): void {
|
||||
const badge = this.shadow.querySelector(".gpc-badge");
|
||||
if (badge) {
|
||||
badge.textContent = label ?? "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,3 +52,14 @@ const COOKIE_DETAIL_LABELS: Record<string, Record<string, string>> = {
|
||||
export function getCookieDetailLabels(lang: string): Record<string, string> {
|
||||
return COOKIE_DETAIL_LABELS[lang] ?? COOKIE_DETAIL_LABELS.en;
|
||||
}
|
||||
|
||||
const GPC_LABELS: Record<string, string> = {
|
||||
en: "Opt-Out Preference Signal Honored",
|
||||
fr: "Signal de préférence de désinscription respecté",
|
||||
de: "Opt-Out-Präferenzsignal beachtet",
|
||||
es: "Señal de exclusión respetada",
|
||||
};
|
||||
|
||||
export function getGpcLabel(lang: string): string {
|
||||
return GPC_LABELS[lang] ?? GPC_LABELS.en;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
import { registerComponents } from "../components";
|
||||
import type { ProboCookieBannerRoot } from "../components/cookie-banner-root";
|
||||
import type { BannerConfig } from "../client";
|
||||
import { interpolate } from "../i18n";
|
||||
import { getGpcLabel, interpolate } from "../i18n";
|
||||
import { BRANDING, CHEVRON_DOWN, CLOSE_ICON } from "../html";
|
||||
import { THEMED_STYLES } from "./styles";
|
||||
|
||||
@@ -127,13 +127,18 @@ export class ProboThemedBanner extends HTMLElement {
|
||||
const root = this.shadow.querySelector("probo-cookie-banner-root") as ProboCookieBannerRoot;
|
||||
|
||||
root.addEventListener("probo-ready", (e: Event) => {
|
||||
const config = (e as CustomEvent).detail.config as BannerConfig;
|
||||
const detail = (e as CustomEvent).detail;
|
||||
const config = detail.config as BannerConfig;
|
||||
this.applyTexts(config);
|
||||
if (!config.show_branding) {
|
||||
this.shadow.querySelectorAll("[data-branding]").forEach(el => {
|
||||
(el as HTMLElement).setAttribute("hidden", "");
|
||||
});
|
||||
}
|
||||
if (detail.gpcApplied) {
|
||||
const settingsBtn = this.shadow.querySelector("probo-settings-button");
|
||||
settingsBtn?.setAttribute("gpc-label", getGpcLabel(config.language));
|
||||
}
|
||||
});
|
||||
|
||||
this.shadow.querySelector("[data-action=back]")?.addEventListener("click", () => {
|
||||
|
||||
Reference in New Issue
Block a user