From b4bb8715ee22a416cec8990a004b42603b8cd490 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20R=C3=A9?= Date: Thu, 7 May 2026 09:50:54 +0400 Subject: [PATCH] Adapt cookie banner UI and texts per regulation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The server now resolves regulation-specific translations (opt-out notice for CCPA, simple notice when no regulation applies) and remaps text keys before returning the config. The client hides buttons whose text is empty, so the banner layout adapts without client-side consent-mode logic. Signed-off-by: Émile Ré --- packages/cookie-banner/src/components/base.ts | 4 +- .../src/components/cookie-banner-root.ts | 12 +++++- .../src/themed-banner/themed-banner.ts | 15 +++++++ pkg/cookiebanner/defaults.go | 36 +++++++++++++++++ pkg/cookiebanner/service.go | 39 +++++++++++++++++++ pkg/server/api/cookiebanner/v1/handler.go | 18 ++------- 6 files changed, 107 insertions(+), 17 deletions(-) diff --git a/packages/cookie-banner/src/components/base.ts b/packages/cookie-banner/src/components/base.ts index ef0ae5983..5b22282a3 100644 --- a/packages/cookie-banner/src/components/base.ts +++ b/packages/cookie-banner/src/components/base.ts @@ -13,7 +13,7 @@ // PERFORMANCE OF THIS SOFTWARE. import type { CookieBannerClient } from "../client"; -import type { BannerConfig } from "../types"; +import type { BannerConfig, Regulation } from "../types"; export type ProboState = "loading" | "banner" | "panel" | "hidden"; @@ -68,6 +68,8 @@ export interface ProboRootElement extends ProboElement { readonly reopenWidget: string; readonly consentDraft: ConsentDraft; readonly gpcApplied: boolean; + readonly regulation: Regulation | null; + readonly consentMode: "OPT_IN" | "OPT_OUT" | null; setState(state: ProboState): void; updateDraft(category: string, value: boolean): void; } diff --git a/packages/cookie-banner/src/components/cookie-banner-root.ts b/packages/cookie-banner/src/components/cookie-banner-root.ts index 75826c2c2..cfa53d2da 100644 --- a/packages/cookie-banner/src/components/cookie-banner-root.ts +++ b/packages/cookie-banner/src/components/cookie-banner-root.ts @@ -13,7 +13,7 @@ // PERFORMANCE OF THIS SOFTWARE. import { CookieBannerClient } from "../client"; -import type { BannerConfig } from "../types"; +import type { BannerConfig, Regulation } from "../types"; import { ProboElement } from "./base"; import type { ProboState, ProboRootElement, ConsentDraft } from "./base"; @@ -57,6 +57,16 @@ export class ProboCookieBannerRoot extends ProboElement implements ProboRootElem return this._client?.gpcApplied ?? false; } + get regulation(): Regulation | null { + return this._client?.regulation ?? null; + } + + get consentMode(): "OPT_IN" | "OPT_OUT" | null { + const mode = this._config?.consent_mode; + if (mode === "OPT_IN" || mode === "OPT_OUT") return mode; + return null; + } + attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void { if (name === "reopen-widget" && oldValue !== newValue) { this.dispatchEvent( diff --git a/packages/cookie-banner/src/themed-banner/themed-banner.ts b/packages/cookie-banner/src/themed-banner/themed-banner.ts index cdbcdf523..d005aaa69 100644 --- a/packages/cookie-banner/src/themed-banner/themed-banner.ts +++ b/packages/cookie-banner/src/themed-banner/themed-banner.ts @@ -130,6 +130,7 @@ export class ProboThemedBanner extends HTMLElement { const detail = (e as CustomEvent).detail; const config = detail.config as BannerConfig; this.applyTexts(config); + this.applyLayout(config); if (!config.show_branding) { this.shadow.querySelectorAll("[data-branding]").forEach(el => { (el as HTMLElement).setAttribute("hidden", ""); @@ -224,6 +225,20 @@ export class ProboThemedBanner extends HTMLElement { } } + private applyLayout(config: BannerConfig): void { + const texts = config.texts ?? {}; + + const hideIfEmpty = (selector: string, textKey: string): void => { + if (texts[textKey]) return; + this.shadow.querySelectorAll(selector).forEach(el => { + (el as HTMLElement).hidden = true; + }); + }; + + hideIfEmpty("probo-banner probo-reject-button", "button_reject_all"); + hideIfEmpty("probo-banner probo-customize-button", "button_customize"); + } + private esc(str: string): string { return str.replace(/&/g, "&").replace(/"/g, """).replace(//g, ">"); } diff --git a/pkg/cookiebanner/defaults.go b/pkg/cookiebanner/defaults.go index fdbb1d716..d29e3721b 100644 --- a/pkg/cookiebanner/defaults.go +++ b/pkg/cookiebanner/defaults.go @@ -119,6 +119,15 @@ var defaultUIStringsByLanguage = map[string]map[string]string{ "cookie_policy_link_text": "Cookie Policy", "placeholder_text": "This content requires {{category}} cookies.", "placeholder_button": "Manage cookie preferences", + + "banner_title_opt_out": "Cookie Notice", + "banner_description_opt_out": "We use cookies and similar technologies. You can opt out of non-essential cookies. {{cookie_policy_link}}", + "button_acknowledge": "OK", + "button_opt_out": "Do Not Sell or Share My Personal Information", + + "banner_title_notice": "Cookie Notice", + "banner_description_notice": "This site uses cookies to enhance your experience. {{cookie_policy_link}}", + "button_dismiss": "Got it", }, "fr": { "banner_title": "Préférences de cookies", @@ -137,6 +146,15 @@ var defaultUIStringsByLanguage = map[string]map[string]string{ "cookie_policy_link_text": "Politique relative aux cookies", "placeholder_text": "Ce contenu nécessite les cookies {{category}}.", "placeholder_button": "Gérer les préférences de cookies", + + "banner_title_opt_out": "Avis sur les cookies", + "banner_description_opt_out": "Nous utilisons des cookies et technologies similaires. Vous pouvez refuser les cookies non essentiels. {{cookie_policy_link}}", + "button_acknowledge": "OK", + "button_opt_out": "Ne pas vendre ni partager mes informations personnelles", + + "banner_title_notice": "Avis sur les cookies", + "banner_description_notice": "Ce site utilise des cookies pour améliorer votre expérience. {{cookie_policy_link}}", + "button_dismiss": "Compris", }, "de": { "banner_title": "Cookie-Einstellungen", @@ -155,6 +173,15 @@ var defaultUIStringsByLanguage = map[string]map[string]string{ "cookie_policy_link_text": "Cookie-Richtlinie", "placeholder_text": "Dieser Inhalt erfordert {{category}}-Cookies.", "placeholder_button": "Cookie-Einstellungen verwalten", + + "banner_title_opt_out": "Cookie-Hinweis", + "banner_description_opt_out": "Wir verwenden Cookies und ähnliche Technologien. Sie können nicht wesentliche Cookies ablehnen. {{cookie_policy_link}}", + "button_acknowledge": "OK", + "button_opt_out": "Meine persönlichen Daten nicht verkaufen oder weitergeben", + + "banner_title_notice": "Cookie-Hinweis", + "banner_description_notice": "Diese Website verwendet Cookies, um Ihre Erfahrung zu verbessern. {{cookie_policy_link}}", + "button_dismiss": "Verstanden", }, "es": { "banner_title": "Preferencias de cookies", @@ -173,5 +200,14 @@ var defaultUIStringsByLanguage = map[string]map[string]string{ "cookie_policy_link_text": "Política de cookies", "placeholder_text": "Este contenido requiere cookies de {{category}}.", "placeholder_button": "Gestionar preferencias de cookies", + + "banner_title_opt_out": "Aviso de cookies", + "banner_description_opt_out": "Utilizamos cookies y tecnologías similares. Puede optar por no recibir cookies no esenciales. {{cookie_policy_link}}", + "button_acknowledge": "OK", + "button_opt_out": "No vender ni compartir mi información personal", + + "banner_title_notice": "Aviso de cookies", + "banner_description_notice": "Este sitio utiliza cookies para mejorar su experiencia. {{cookie_policy_link}}", + "button_dismiss": "Entendido", }, } diff --git a/pkg/cookiebanner/service.go b/pkg/cookiebanner/service.go index 71bc6c853..cdc15dcd9 100644 --- a/pkg/cookiebanner/service.go +++ b/pkg/cookiebanner/service.go @@ -171,6 +171,7 @@ type ( CookiePolicyURL string `json:"cookie_policy_url"` ConsentExpiryDays int `json:"consent_expiry_days"` ConsentMode string `json:"consent_mode"` + Regulation Regulation `json:"regulation"` ShowBranding bool `json:"show_branding"` Categories []coredata.CookieBannerVersionSnapshotCategory `json:"categories"` Texts map[string]string `json:"texts"` @@ -1527,6 +1528,7 @@ func (s *Service) GetActiveBannerConfig( ctx context.Context, bannerID gid.GID, lang string, + regulation Regulation, ) (*BannerConfig, error) { var config *BannerConfig @@ -1576,6 +1578,12 @@ func (s *Service) GetActiveBannerConfig( return nil, err } + config.Regulation = regulation + if cm := ConsentModeForRegulation(regulation); cm != "" { + config.ConsentMode = cm + } + remapTextsForConsentMode(config.Texts, config.ConsentMode, regulation) + return config, nil } @@ -1644,6 +1652,37 @@ func buildBannerConfig( } } +// remapTextsForConsentMode overrides the generic banner text keys with +// mode-specific variants so the client renders the appropriate copy +// without needing consent-mode awareness itself. +func remapTextsForConsentMode(texts map[string]string, consentMode string, regulation Regulation) { + if texts == nil { + return + } + + switch { + case consentMode == ConsentModeOptOut: + remapTextKey(texts, "banner_title_opt_out", "banner_title") + remapTextKey(texts, "banner_description_opt_out", "banner_description") + remapTextKey(texts, "button_acknowledge", "button_accept_all") + remapTextKey(texts, "button_opt_out", "button_customize") + texts["button_reject_all"] = "" + + case regulation == RegulationNone: + remapTextKey(texts, "banner_title_notice", "banner_title") + remapTextKey(texts, "banner_description_notice", "banner_description") + remapTextKey(texts, "button_dismiss", "button_accept_all") + texts["button_reject_all"] = "" + texts["button_customize"] = "" + } +} + +func remapTextKey(texts map[string]string, src, dst string) { + if v, ok := texts[src]; ok && v != "" { + texts[dst] = v + } +} + func (s *Service) SetShowBranding( ctx context.Context, bannerID gid.GID, diff --git a/pkg/server/api/cookiebanner/v1/handler.go b/pkg/server/api/cookiebanner/v1/handler.go index 8c782bad4..cf37ec2fb 100644 --- a/pkg/server/api/cookiebanner/v1/handler.go +++ b/pkg/server/api/cookiebanner/v1/handler.go @@ -63,11 +63,6 @@ func NewMux( return r } -type configResponse struct { - *cookiebanner.BannerConfig - Regulation cookiebanner.Regulation `json:"regulation"` -} - func (h *Handler) handleGetConfig(w http.ResponseWriter, r *http.Request) { bannerID, err := gid.ParseGID(chi.URLParam(r, "bannerID")) if err != nil { @@ -76,8 +71,9 @@ func (h *Handler) handleGetConfig(w http.ResponseWriter, r *http.Request) { } lang := r.URL.Query().Get("lang") + regulation := h.resolveRegulation(r) - config, err := h.cookieBannerSvc.GetActiveBannerConfig(r.Context(), bannerID, lang) + config, err := h.cookieBannerSvc.GetActiveBannerConfig(r.Context(), bannerID, lang, regulation) if err != nil { if errors.Is(err, cookiebanner.ErrBannerNotFound) { jsonutil.RenderNotFound(w, fmt.Errorf("banner not found")) @@ -92,15 +88,7 @@ func (h *Handler) handleGetConfig(w http.ResponseWriter, r *http.Request) { return } - regulation := h.resolveRegulation(r) - if cm := cookiebanner.ConsentModeForRegulation(regulation); cm != "" { - config.ConsentMode = cm - } - - httpserver.RenderJSON(w, http.StatusOK, configResponse{ - BannerConfig: config, - Regulation: regulation, - }) + httpserver.RenderJSON(w, http.StatusOK, config) } func (h *Handler) resolveRegulation(r *http.Request) cookiebanner.Regulation {