Adapt cookie banner UI and texts per regulation

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é <emile@getprobo.com>
This commit is contained in:
Émile Ré
2026-05-07 09:50:54 +04:00
parent 4598506076
commit b4bb8715ee
6 changed files with 107 additions and 17 deletions

View File

@@ -13,7 +13,7 @@
// PERFORMANCE OF THIS SOFTWARE. // PERFORMANCE OF THIS SOFTWARE.
import type { CookieBannerClient } from "../client"; import type { CookieBannerClient } from "../client";
import type { BannerConfig } from "../types"; import type { BannerConfig, Regulation } from "../types";
export type ProboState = "loading" | "banner" | "panel" | "hidden"; export type ProboState = "loading" | "banner" | "panel" | "hidden";
@@ -68,6 +68,8 @@ export interface ProboRootElement extends ProboElement {
readonly reopenWidget: string; readonly reopenWidget: string;
readonly consentDraft: ConsentDraft; readonly consentDraft: ConsentDraft;
readonly gpcApplied: boolean; readonly gpcApplied: boolean;
readonly regulation: Regulation | null;
readonly consentMode: "OPT_IN" | "OPT_OUT" | null;
setState(state: ProboState): void; setState(state: ProboState): void;
updateDraft(category: string, value: boolean): void; updateDraft(category: string, value: boolean): void;
} }

View File

@@ -13,7 +13,7 @@
// PERFORMANCE OF THIS SOFTWARE. // PERFORMANCE OF THIS SOFTWARE.
import { CookieBannerClient } from "../client"; import { CookieBannerClient } from "../client";
import type { BannerConfig } from "../types"; import type { BannerConfig, Regulation } from "../types";
import { ProboElement } from "./base"; import { ProboElement } from "./base";
import type { ProboState, ProboRootElement, ConsentDraft } 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; 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 { attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void {
if (name === "reopen-widget" && oldValue !== newValue) { if (name === "reopen-widget" && oldValue !== newValue) {
this.dispatchEvent( this.dispatchEvent(

View File

@@ -130,6 +130,7 @@ export class ProboThemedBanner extends HTMLElement {
const detail = (e as CustomEvent).detail; const detail = (e as CustomEvent).detail;
const config = detail.config as BannerConfig; const config = detail.config as BannerConfig;
this.applyTexts(config); this.applyTexts(config);
this.applyLayout(config);
if (!config.show_branding) { if (!config.show_branding) {
this.shadow.querySelectorAll("[data-branding]").forEach(el => { this.shadow.querySelectorAll("[data-branding]").forEach(el => {
(el as HTMLElement).setAttribute("hidden", ""); (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 { private esc(str: string): string {
return str.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;"); return str.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
} }

View File

@@ -119,6 +119,15 @@ var defaultUIStringsByLanguage = map[string]map[string]string{
"cookie_policy_link_text": "Cookie Policy", "cookie_policy_link_text": "Cookie Policy",
"placeholder_text": "This content requires {{category}} cookies.", "placeholder_text": "This content requires {{category}} cookies.",
"placeholder_button": "Manage cookie preferences", "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": { "fr": {
"banner_title": "Préférences de cookies", "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", "cookie_policy_link_text": "Politique relative aux cookies",
"placeholder_text": "Ce contenu nécessite les cookies {{category}}.", "placeholder_text": "Ce contenu nécessite les cookies {{category}}.",
"placeholder_button": "Gérer les préférences de cookies", "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": { "de": {
"banner_title": "Cookie-Einstellungen", "banner_title": "Cookie-Einstellungen",
@@ -155,6 +173,15 @@ var defaultUIStringsByLanguage = map[string]map[string]string{
"cookie_policy_link_text": "Cookie-Richtlinie", "cookie_policy_link_text": "Cookie-Richtlinie",
"placeholder_text": "Dieser Inhalt erfordert {{category}}-Cookies.", "placeholder_text": "Dieser Inhalt erfordert {{category}}-Cookies.",
"placeholder_button": "Cookie-Einstellungen verwalten", "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": { "es": {
"banner_title": "Preferencias de cookies", "banner_title": "Preferencias de cookies",
@@ -173,5 +200,14 @@ var defaultUIStringsByLanguage = map[string]map[string]string{
"cookie_policy_link_text": "Política de cookies", "cookie_policy_link_text": "Política de cookies",
"placeholder_text": "Este contenido requiere cookies de {{category}}.", "placeholder_text": "Este contenido requiere cookies de {{category}}.",
"placeholder_button": "Gestionar preferencias de cookies", "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",
}, },
} }

View File

@@ -171,6 +171,7 @@ type (
CookiePolicyURL string `json:"cookie_policy_url"` CookiePolicyURL string `json:"cookie_policy_url"`
ConsentExpiryDays int `json:"consent_expiry_days"` ConsentExpiryDays int `json:"consent_expiry_days"`
ConsentMode string `json:"consent_mode"` ConsentMode string `json:"consent_mode"`
Regulation Regulation `json:"regulation"`
ShowBranding bool `json:"show_branding"` ShowBranding bool `json:"show_branding"`
Categories []coredata.CookieBannerVersionSnapshotCategory `json:"categories"` Categories []coredata.CookieBannerVersionSnapshotCategory `json:"categories"`
Texts map[string]string `json:"texts"` Texts map[string]string `json:"texts"`
@@ -1527,6 +1528,7 @@ func (s *Service) GetActiveBannerConfig(
ctx context.Context, ctx context.Context,
bannerID gid.GID, bannerID gid.GID,
lang string, lang string,
regulation Regulation,
) (*BannerConfig, error) { ) (*BannerConfig, error) {
var config *BannerConfig var config *BannerConfig
@@ -1576,6 +1578,12 @@ func (s *Service) GetActiveBannerConfig(
return nil, err return nil, err
} }
config.Regulation = regulation
if cm := ConsentModeForRegulation(regulation); cm != "" {
config.ConsentMode = cm
}
remapTextsForConsentMode(config.Texts, config.ConsentMode, regulation)
return config, nil 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( func (s *Service) SetShowBranding(
ctx context.Context, ctx context.Context,
bannerID gid.GID, bannerID gid.GID,

View File

@@ -63,11 +63,6 @@ func NewMux(
return r return r
} }
type configResponse struct {
*cookiebanner.BannerConfig
Regulation cookiebanner.Regulation `json:"regulation"`
}
func (h *Handler) handleGetConfig(w http.ResponseWriter, r *http.Request) { func (h *Handler) handleGetConfig(w http.ResponseWriter, r *http.Request) {
bannerID, err := gid.ParseGID(chi.URLParam(r, "bannerID")) bannerID, err := gid.ParseGID(chi.URLParam(r, "bannerID"))
if err != nil { if err != nil {
@@ -76,8 +71,9 @@ func (h *Handler) handleGetConfig(w http.ResponseWriter, r *http.Request) {
} }
lang := r.URL.Query().Get("lang") 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 err != nil {
if errors.Is(err, cookiebanner.ErrBannerNotFound) { if errors.Is(err, cookiebanner.ErrBannerNotFound) {
jsonutil.RenderNotFound(w, fmt.Errorf("banner not found")) jsonutil.RenderNotFound(w, fmt.Errorf("banner not found"))
@@ -92,15 +88,7 @@ func (h *Handler) handleGetConfig(w http.ResponseWriter, r *http.Request) {
return return
} }
regulation := h.resolveRegulation(r) httpserver.RenderJSON(w, http.StatusOK, config)
if cm := cookiebanner.ConsentModeForRegulation(regulation); cm != "" {
config.ConsentMode = cm
}
httpserver.RenderJSON(w, http.StatusOK, configResponse{
BannerConfig: config,
Regulation: regulation,
})
} }
func (h *Handler) resolveRegulation(r *http.Request) cookiebanner.Regulation { func (h *Handler) resolveRegulation(r *http.Request) cookiebanner.Regulation {