Files
breakpilot-compliance/admin-compliance/lib/sdk/einwilligungen/generator/cookie-banner-embed.ts
T
Benjamin Admin cb2d503e84 feat: Google Consent Mode v2 + Developer Portal cookie banner docs
Phase A: Google Consent Mode v2 in cookie-banner-embed.ts
- gtag('consent', 'default', {...denied}) before banner loads
- gtag('consent', 'update', {...}) after user decision
- Automatic mapping: statistics→analytics_storage, marketing→ad_storage

Phase B: 5 Developer Portal pages at /sdk/consent/cookie-banner/
- Overview page with 4 cards
- Integration Guide: 3-step setup, script-tag, categories
- Google Consent Mode: automatic integration, parameter mapping
- Script Blocking: type=text/plain pattern, GA/FB/Hotjar examples
- Compliance Checklist: 12 points, 9 automatic

Sidebar navigation extended with Cookie-Banner section.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-02 17:13:34 +02:00

501 lines
14 KiB
TypeScript

/**
* Cookie Banner — Embed Code Generation (CSS, HTML, JS)
*
* Generates the embeddable cookie banner code from configuration.
*/
import {
CookieBannerConfig,
CookieBannerStyling,
CookieBannerEmbedCode,
LocalizedText,
SupportedLanguage,
} from '../types'
// =============================================================================
// MAIN EXPORT
// =============================================================================
export function generateEmbedCode(
config: CookieBannerConfig,
privacyPolicyUrl: string = '/datenschutz'
): CookieBannerEmbedCode {
const css = generateCSS(config.styling)
const html = generateHTML(config, privacyPolicyUrl)
const js = generateJS(config)
const scriptTag = `<script src="/cookie-banner.js" data-tenant="${config.tenantId}"></script>`
return { html, css, js, scriptTag }
}
// =============================================================================
// CSS GENERATION
// =============================================================================
function generateCSS(styling: CookieBannerStyling): string {
const positionStyles: Record<string, string> = {
BOTTOM: 'bottom: 0; left: 0; right: 0;',
TOP: 'top: 0; left: 0; right: 0;',
CENTER: 'top: 50%; left: 50%; transform: translate(-50%, -50%);',
}
const isDark = styling.theme === 'DARK'
const bgColor = isDark ? '#1e293b' : styling.backgroundColor || '#ffffff'
const textColor = isDark ? '#f1f5f9' : styling.textColor || '#1e293b'
const borderColor = isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)'
return `
/* Cookie Banner Styles */
.cookie-banner-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.4);
z-index: 9998;
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
}
.cookie-banner-overlay.active {
opacity: 1;
visibility: visible;
}
.cookie-banner {
position: fixed;
${positionStyles[styling.position]}
z-index: 9999;
background: ${bgColor};
color: ${textColor};
border-radius: ${styling.borderRadius || 12}px;
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.1);
padding: 24px;
max-width: ${styling.maxWidth}px;
margin: ${styling.position === 'CENTER' ? '0' : '16px'};
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
transform: translateY(100%);
opacity: 0;
transition: all 0.3s ease;
}
.cookie-banner.active {
transform: translateY(0);
opacity: 1;
}
.cookie-banner-title {
font-size: 18px;
font-weight: 600;
margin-bottom: 12px;
}
.cookie-banner-description {
font-size: 14px;
line-height: 1.5;
margin-bottom: 16px;
opacity: 0.8;
}
.cookie-banner-buttons {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.cookie-banner-btn {
flex: 1;
min-width: 120px;
padding: 12px 20px;
border-radius: ${(styling.borderRadius || 12) / 2}px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
border: none;
}
.cookie-banner-btn-primary {
background: ${styling.primaryColor};
color: white;
}
.cookie-banner-btn-primary:hover {
filter: brightness(1.1);
}
.cookie-banner-btn-secondary {
background: ${styling.secondaryColor || borderColor};
color: ${textColor};
}
.cookie-banner-btn-secondary:hover {
filter: brightness(0.95);
}
.cookie-banner-link {
display: block;
margin-top: 16px;
font-size: 12px;
color: ${styling.primaryColor};
text-decoration: none;
}
.cookie-banner-link:hover {
text-decoration: underline;
}
/* Category Details */
.cookie-banner-details {
margin-top: 16px;
border-top: 1px solid ${borderColor};
padding-top: 16px;
display: none;
}
.cookie-banner-details.active {
display: block;
}
.cookie-banner-category {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid ${borderColor};
}
.cookie-banner-category:last-child {
border-bottom: none;
}
.cookie-banner-category-info {
flex: 1;
}
.cookie-banner-category-name {
font-weight: 500;
font-size: 14px;
}
.cookie-banner-category-desc {
font-size: 12px;
opacity: 0.7;
margin-top: 4px;
}
.cookie-banner-toggle {
position: relative;
width: 48px;
height: 28px;
background: ${borderColor};
border-radius: 14px;
cursor: pointer;
transition: all 0.2s ease;
}
.cookie-banner-toggle.active {
background: ${styling.primaryColor};
}
.cookie-banner-toggle.disabled {
opacity: 0.5;
cursor: not-allowed;
}
.cookie-banner-toggle::after {
content: '';
position: absolute;
top: 4px;
left: 4px;
width: 20px;
height: 20px;
background: white;
border-radius: 50%;
transition: all 0.2s ease;
}
.cookie-banner-toggle.active::after {
left: 24px;
}
@media (max-width: 640px) {
.cookie-banner {
margin: 0;
border-radius: ${styling.position === 'CENTER' ? (styling.borderRadius || 12) : 0}px;
max-width: 100%;
}
.cookie-banner-buttons {
flex-direction: column;
}
.cookie-banner-btn {
width: 100%;
}
}
`.trim()
}
// =============================================================================
// HTML GENERATION
// =============================================================================
function generateHTML(config: CookieBannerConfig, privacyPolicyUrl: string): string {
const categoriesHTML = config.categories
.map((cat) => {
const isRequired = cat.isRequired
// COMPLIANCE: Only "required" categories may be pre-enabled (EuGH Planet49)
// Non-required categories must NEVER be defaultEnabled
const isEnabled = isRequired ? true : false
return `
<div class="cookie-banner-category" data-category="${cat.id}">
<div class="cookie-banner-category-info">
<div class="cookie-banner-category-name">${cat.name.de}</div>
<div class="cookie-banner-category-desc">${cat.description.de}</div>
</div>
<div class="cookie-banner-toggle ${isEnabled ? 'active' : ''} ${isRequired ? 'disabled' : ''}"
data-category="${cat.id}"
data-required="${isRequired}"></div>
</div>
`
})
.join('')
return `
<div class="cookie-banner-overlay" id="cookieBannerOverlay"></div>
<div class="cookie-banner" id="cookieBanner" role="dialog" aria-labelledby="cookieBannerTitle" aria-modal="true">
<div class="cookie-banner-title" id="cookieBannerTitle">${config.texts.title.de}</div>
<div class="cookie-banner-description">${config.texts.description.de}</div>
<div class="cookie-banner-buttons">
<button class="cookie-banner-btn cookie-banner-btn-secondary" id="cookieBannerReject">
${config.texts.rejectAll.de}
</button>
<button class="cookie-banner-btn cookie-banner-btn-secondary" id="cookieBannerCustomize">
${config.texts.customize.de}
</button>
<button class="cookie-banner-btn cookie-banner-btn-primary" id="cookieBannerAccept">
${config.texts.acceptAll.de}
</button>
</div>
<div class="cookie-banner-details" id="cookieBannerDetails">
${categoriesHTML}
<div class="cookie-banner-buttons" style="margin-top: 16px;">
<button class="cookie-banner-btn cookie-banner-btn-primary" id="cookieBannerSave">
${config.texts.save.de}
</button>
</div>
</div>
<div class="cookie-banner-links">
<a href="${privacyPolicyUrl}" class="cookie-banner-link" target="_blank">
${config.texts.privacyPolicyLink.de}
</a>
<a href="${config.impressumUrl || '/impressum'}" class="cookie-banner-link" target="_blank">
Impressum
</a>
</div>
</div>
<!-- Cookie Settings Re-Open (§7(3) DSGVO — Widerruf so einfach wie Einwilligung) -->
<a href="#" id="cookieBannerReopen" class="cookie-settings-footer-link"
onclick="document.getElementById('cookieBanner').style.display='block';document.getElementById('cookieBannerOverlay').classList.add('active');return false;"
style="position:fixed;bottom:8px;left:8px;z-index:9990;font-size:11px;color:#6b7280;text-decoration:none;background:rgba(255,255,255,0.9);padding:4px 8px;border-radius:4px;border:1px solid #e5e7eb;">
Cookie-Einstellungen
</a>
`.trim()
}
// =============================================================================
// JS GENERATION
// =============================================================================
function generateJS(config: CookieBannerConfig): string {
const categoryIds = config.categories.map((c) => c.id)
const requiredCategories = config.categories.filter((c) => c.isRequired).map((c) => c.id)
return `
(function() {
'use strict';
const COOKIE_NAME = 'cookie_consent';
const COOKIE_EXPIRY_DAYS = 365;
const CATEGORIES = ${JSON.stringify(categoryIds)};
const REQUIRED_CATEGORIES = ${JSON.stringify(requiredCategories)};
// Google Consent Mode v2 — PFLICHT seit Maerz 2024 fuer Google Services in EEA
// Sets default consent state to "denied" BEFORE any Google tags fire
if (typeof gtag === 'function') {
gtag('consent', 'default', {
analytics_storage: 'denied',
ad_storage: 'denied',
ad_user_data: 'denied',
ad_personalization: 'denied',
functionality_storage: 'granted',
security_storage: 'granted',
});
}
function updateGoogleConsentMode(consent) {
if (typeof gtag !== 'function') return;
gtag('consent', 'update', {
analytics_storage: consent.statistics ? 'granted' : 'denied',
ad_storage: consent.marketing ? 'granted' : 'denied',
ad_user_data: consent.marketing ? 'granted' : 'denied',
ad_personalization: consent.marketing ? 'granted' : 'denied',
});
}
function getConsent() {
const cookie = document.cookie.split('; ').find(row => row.startsWith(COOKIE_NAME + '='));
if (!cookie) return null;
try {
return JSON.parse(decodeURIComponent(cookie.split('=')[1]));
} catch {
return null;
}
}
function saveConsent(consent) {
const date = new Date();
date.setTime(date.getTime() + (COOKIE_EXPIRY_DAYS * 24 * 60 * 60 * 1000));
document.cookie = COOKIE_NAME + '=' + encodeURIComponent(JSON.stringify(consent)) +
';expires=' + date.toUTCString() +
';path=/;SameSite=Lax';
window.dispatchEvent(new CustomEvent('cookieConsentUpdated', { detail: consent }));
updateGoogleConsentMode(consent);
}
function hasConsent(category) {
const consent = getConsent();
if (!consent) return REQUIRED_CATEGORIES.includes(category);
return consent[category] === true;
}
function initBanner() {
const banner = document.getElementById('cookieBanner');
const overlay = document.getElementById('cookieBannerOverlay');
const details = document.getElementById('cookieBannerDetails');
if (!banner) return;
const consent = getConsent();
if (consent) return;
setTimeout(() => {
banner.classList.add('active');
overlay.classList.add('active');
}, 500);
document.getElementById('cookieBannerAccept')?.addEventListener('click', () => {
const consent = {};
CATEGORIES.forEach(cat => consent[cat] = true);
saveConsent(consent);
closeBanner();
});
document.getElementById('cookieBannerReject')?.addEventListener('click', () => {
const consent = {};
CATEGORIES.forEach(cat => consent[cat] = REQUIRED_CATEGORIES.includes(cat));
saveConsent(consent);
closeBanner();
});
document.getElementById('cookieBannerCustomize')?.addEventListener('click', () => {
details.classList.toggle('active');
});
document.getElementById('cookieBannerSave')?.addEventListener('click', () => {
const consent = {};
CATEGORIES.forEach(cat => {
const toggle = document.querySelector('.cookie-banner-toggle[data-category="' + cat + '"]');
consent[cat] = toggle?.classList.contains('active') || REQUIRED_CATEGORIES.includes(cat);
});
saveConsent(consent);
closeBanner();
});
document.querySelectorAll('.cookie-banner-toggle').forEach(toggle => {
if (toggle.dataset.required === 'true') return;
toggle.addEventListener('click', () => {
toggle.classList.toggle('active');
});
});
overlay?.addEventListener('click', () => {
// Don't close - user must make a choice
});
}
function closeBanner() {
const banner = document.getElementById('cookieBanner');
const overlay = document.getElementById('cookieBannerOverlay');
banner?.classList.remove('active');
overlay?.classList.remove('active');
}
// Script-Blocking: activate scripts with data-cookie-category ONLY after consent
function activateConsentedScripts() {
const consent = getConsent();
if (!consent) return;
// Find all blocked scripts (type="text/plain" with data-cookie-category)
document.querySelectorAll('script[data-cookie-category][type="text/plain"]').forEach(script => {
const category = script.getAttribute('data-cookie-category');
if (consent[category] === true) {
// Replace type to activate the script
const newScript = document.createElement('script');
if (script.src) newScript.src = script.src;
else newScript.textContent = script.textContent;
newScript.type = 'text/javascript';
script.parentNode.replaceChild(newScript, script);
}
});
// Also fire custom event for programmatic listeners
window.dispatchEvent(new CustomEvent('cookieConsentActivated', { detail: consent }));
}
// Run script activation after consent is saved
window.addEventListener('cookieConsentUpdated', activateConsentedScripts);
window.CookieConsent = {
getConsent,
saveConsent,
hasConsent,
show: () => {
document.getElementById('cookieBanner')?.classList.add('active');
document.getElementById('cookieBannerOverlay')?.classList.add('active');
},
hide: closeBanner,
activateScripts: activateConsentedScripts,
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
initBanner();
activateConsentedScripts();
});
} else {
initBanner();
activateConsentedScripts();
}
})();
/*
* USAGE: Script-Blocking
*
* Instead of:
* <script src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXX"></script>
*
* Use:
* <script type="text/plain" data-cookie-category="statistics"
* src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXX"></script>
*
* The script will only execute AFTER the user consents to "statistics".
*/
`.trim()
}