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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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, "<").replace(/>/g, ">");
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user