/** * BreakPilot Compliance SDK - Embed Script * * Usage: * * */ import { ComplianceClient } from '@breakpilot/compliance-sdk-core' import type { ConsentPurpose, CookieBannerPosition, CookieBannerTheme, DSRRequestType, } from '@breakpilot/compliance-sdk-types' // ============================================================================ // Types // ============================================================================ export interface BreakPilotSDKConfig { apiEndpoint: string apiKey: string tenantId?: string autoInjectBanner?: boolean bannerConfig?: BannerConfig onConsentChange?: (consents: Record) => void onReady?: () => void onError?: (error: Error) => void debug?: boolean } export interface BannerConfig { position?: CookieBannerPosition theme?: CookieBannerTheme language?: 'de' | 'en' privacyPolicyUrl?: string imprintUrl?: string texts?: { title?: string description?: string acceptAll?: string rejectAll?: string settings?: string save?: string } customColors?: { background?: string text?: string primary?: string secondary?: string } } // ============================================================================ // Internal State // ============================================================================ let _client: ComplianceClient | null = null let _config: BreakPilotSDKConfig | null = null let _consents: Record = { ESSENTIAL: true, FUNCTIONAL: false, ANALYTICS: false, MARKETING: false, PERSONALIZATION: false, THIRD_PARTY: false, } let _bannerElement: HTMLElement | null = null let _isInitialized = false // ============================================================================ // Translations // ============================================================================ const TRANSLATIONS = { de: { title: 'Cookie-Einwilligung', description: 'Wir verwenden Cookies, um Ihre Erfahrung zu verbessern. Weitere Informationen finden Sie in unserer Datenschutzerklärung.', acceptAll: 'Alle akzeptieren', rejectAll: 'Nur notwendige', settings: 'Einstellungen', save: 'Speichern', privacy: 'Datenschutz', imprint: 'Impressum', categories: { ESSENTIAL: { name: 'Notwendig', description: 'Erforderlich für die Grundfunktionen' }, FUNCTIONAL: { name: 'Funktional', description: 'Verbesserte Funktionen' }, ANALYTICS: { name: 'Analyse', description: 'Nutzungsstatistiken' }, MARKETING: { name: 'Marketing', description: 'Personalisierte Werbung' }, PERSONALIZATION: { name: 'Personalisierung', description: 'Angepasste Inhalte' }, THIRD_PARTY: { name: 'Drittanbieter', description: 'Externe Dienste' }, }, }, en: { title: 'Cookie Consent', description: 'We use cookies to improve your experience. For more information, please see our privacy policy.', acceptAll: 'Accept All', rejectAll: 'Reject Non-Essential', settings: 'Settings', save: 'Save', privacy: 'Privacy Policy', imprint: 'Imprint', categories: { ESSENTIAL: { name: 'Essential', description: 'Required for basic functionality' }, FUNCTIONAL: { name: 'Functional', description: 'Enhanced features' }, ANALYTICS: { name: 'Analytics', description: 'Usage statistics' }, MARKETING: { name: 'Marketing', description: 'Personalized advertising' }, PERSONALIZATION: { name: 'Personalization', description: 'Customized content' }, THIRD_PARTY: { name: 'Third Party', description: 'External services' }, }, }, } // ============================================================================ // Utility Functions // ============================================================================ function log(message: string, ...args: unknown[]): void { if (_config?.debug) { console.log(`[BreakPilotSDK] ${message}`, ...args) } } function getStoredConsents(): Record | null { try { const stored = localStorage.getItem('breakpilot_consents') return stored ? JSON.parse(stored) : null } catch { return null } } function storeConsents(consents: Record): void { try { localStorage.setItem('breakpilot_consents', JSON.stringify(consents)) localStorage.setItem('breakpilot_consents_timestamp', Date.now().toString()) } catch { log('Failed to store consents') } } function createElement( tag: K, styles: Partial = {}, attributes: Record = {} ): HTMLElementTagNameMap[K] { const el = document.createElement(tag) Object.assign(el.style, styles) Object.entries(attributes).forEach(([key, value]) => el.setAttribute(key, value)) return el } // ============================================================================ // Banner Implementation // ============================================================================ function createBanner(): HTMLElement { const config = _config! const bannerConfig = config.bannerConfig || {} const lang = bannerConfig.language || 'de' const t = TRANSLATIONS[lang] const position = bannerConfig.position || 'BOTTOM' const theme = bannerConfig.theme || 'LIGHT' const isDark = theme === 'DARK' const bgColor = bannerConfig.customColors?.background || (isDark ? '#1a1a1a' : '#ffffff') const textColor = bannerConfig.customColors?.text || (isDark ? '#ffffff' : '#1a1a1a') const primaryColor = bannerConfig.customColors?.primary || (isDark ? '#ffffff' : '#1a1a1a') const secondaryColor = bannerConfig.customColors?.secondary || (isDark ? '#ffffff' : '#1a1a1a') // Container const container = createElement( 'div', { position: 'fixed', zIndex: '99999', left: position === 'CENTER' ? '50%' : '0', right: position === 'CENTER' ? 'auto' : '0', top: position === 'TOP' ? '0' : position === 'CENTER' ? '50%' : 'auto', bottom: position === 'BOTTOM' ? '0' : 'auto', transform: position === 'CENTER' ? 'translate(-50%, -50%)' : 'none', maxWidth: position === 'CENTER' ? '500px' : 'none', backgroundColor: bgColor, color: textColor, padding: '20px', boxShadow: '0 -2px 10px rgba(0, 0, 0, 0.1)', fontFamily: 'system-ui, -apple-system, sans-serif', fontSize: '14px', lineHeight: '1.5', }, { id: 'breakpilot-consent-banner', role: 'dialog', 'aria-label': t.title } ) // Title const title = createElement('h3', { margin: '0 0 10px', fontSize: '18px', fontWeight: '600', }) title.textContent = bannerConfig.texts?.title || t.title // Description const description = createElement('p', { margin: '0 0 15px', opacity: '0.8', }) description.textContent = bannerConfig.texts?.description || t.description // Buttons container const buttonsContainer = createElement('div', { display: 'flex', flexWrap: 'wrap', gap: '10px', alignItems: 'center', }) // Accept All button const acceptBtn = createElement( 'button', { padding: '10px 20px', borderRadius: '4px', border: 'none', cursor: 'pointer', fontWeight: '500', backgroundColor: primaryColor, color: isDark ? '#1a1a1a' : '#ffffff', }, { type: 'button' } ) acceptBtn.textContent = bannerConfig.texts?.acceptAll || t.acceptAll acceptBtn.onclick = () => handleAcceptAll() // Reject button const rejectBtn = createElement( 'button', { padding: '10px 20px', borderRadius: '4px', backgroundColor: 'transparent', border: `1px solid ${secondaryColor}`, color: textColor, cursor: 'pointer', fontWeight: '500', }, { type: 'button' } ) rejectBtn.textContent = bannerConfig.texts?.rejectAll || t.rejectAll rejectBtn.onclick = () => handleRejectAll() // Settings button const settingsBtn = createElement( 'button', { padding: '10px 20px', borderRadius: '4px', backgroundColor: 'transparent', border: `1px solid ${secondaryColor}`, color: textColor, cursor: 'pointer', fontWeight: '500', }, { type: 'button' } ) settingsBtn.textContent = bannerConfig.texts?.settings || t.settings settingsBtn.onclick = () => showSettingsPanel() // Links container const linksContainer = createElement('div', { marginLeft: 'auto', fontSize: '12px', }) const privacyLink = createElement('a', { marginRight: '15px', color: textColor, textDecoration: 'none', }) privacyLink.href = bannerConfig.privacyPolicyUrl || '/privacy' privacyLink.textContent = t.privacy const imprintLink = createElement('a', { color: textColor, textDecoration: 'none', }) imprintLink.href = bannerConfig.imprintUrl || '/imprint' imprintLink.textContent = t.imprint // Assemble linksContainer.appendChild(privacyLink) linksContainer.appendChild(imprintLink) buttonsContainer.appendChild(acceptBtn) buttonsContainer.appendChild(rejectBtn) buttonsContainer.appendChild(settingsBtn) buttonsContainer.appendChild(linksContainer) container.appendChild(title) container.appendChild(description) container.appendChild(buttonsContainer) return container } function showSettingsPanel(): void { if (!_bannerElement) return const config = _config! const bannerConfig = config.bannerConfig || {} const lang = bannerConfig.language || 'de' const t = TRANSLATIONS[lang] const theme = bannerConfig.theme || 'LIGHT' const isDark = theme === 'DARK' const textColor = bannerConfig.customColors?.text || (isDark ? '#ffffff' : '#1a1a1a') // Clear banner content _bannerElement.innerHTML = '' // Title const title = createElement('h3', { margin: '0 0 15px', fontSize: '18px', fontWeight: '600', }) title.textContent = t.settings _bannerElement.appendChild(title) // Categories const categories: ConsentPurpose[] = [ 'ESSENTIAL', 'FUNCTIONAL', 'ANALYTICS', 'MARKETING', ] categories.forEach(category => { const catInfo = t.categories[category] const row = createElement('div', { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '10px 0', borderBottom: `1px solid ${isDark ? '#333' : '#eee'}`, }) const labelContainer = createElement('div', {}) const labelName = createElement('div', { fontWeight: '500' }) labelName.textContent = catInfo.name const labelDesc = createElement('div', { fontSize: '12px', opacity: '0.7' }) labelDesc.textContent = catInfo.description labelContainer.appendChild(labelName) labelContainer.appendChild(labelDesc) const checkbox = createElement( 'input', { width: '20px', height: '20px', cursor: category === 'ESSENTIAL' ? 'not-allowed' : 'pointer', }, { type: 'checkbox', 'data-category': category, } ) checkbox.checked = _consents[category] checkbox.disabled = category === 'ESSENTIAL' checkbox.onchange = () => { if (category !== 'ESSENTIAL') { _consents[category] = checkbox.checked } } row.appendChild(labelContainer) row.appendChild(checkbox) _bannerElement!.appendChild(row) }) // Buttons const buttonsContainer = createElement('div', { display: 'flex', flexWrap: 'wrap', gap: '10px', marginTop: '15px', }) const primaryColor = bannerConfig.customColors?.primary || (isDark ? '#ffffff' : '#1a1a1a') const secondaryColor = bannerConfig.customColors?.secondary || (isDark ? '#ffffff' : '#1a1a1a') const saveBtn = createElement( 'button', { padding: '10px 20px', borderRadius: '4px', border: 'none', cursor: 'pointer', fontWeight: '500', backgroundColor: primaryColor, color: isDark ? '#1a1a1a' : '#ffffff', }, { type: 'button' } ) saveBtn.textContent = bannerConfig.texts?.save || t.save saveBtn.onclick = () => handleSaveSettings() const backBtn = createElement( 'button', { padding: '10px 20px', borderRadius: '4px', backgroundColor: 'transparent', border: `1px solid ${secondaryColor}`, color: textColor, cursor: 'pointer', fontWeight: '500', }, { type: 'button' } ) backBtn.textContent = 'Zurück' backBtn.onclick = () => showMainBanner() buttonsContainer.appendChild(saveBtn) buttonsContainer.appendChild(backBtn) _bannerElement.appendChild(buttonsContainer) } function showMainBanner(): void { if (_bannerElement) { _bannerElement.remove() } _bannerElement = createBanner() document.body.appendChild(_bannerElement) } function hideBanner(): void { if (_bannerElement) { _bannerElement.remove() _bannerElement = null } } // ============================================================================ // Consent Handlers // ============================================================================ function handleAcceptAll(): void { _consents = { ESSENTIAL: true, FUNCTIONAL: true, ANALYTICS: true, MARKETING: true, PERSONALIZATION: true, THIRD_PARTY: true, } applyConsents() } function handleRejectAll(): void { _consents = { ESSENTIAL: true, FUNCTIONAL: false, ANALYTICS: false, MARKETING: false, PERSONALIZATION: false, THIRD_PARTY: false, } applyConsents() } function handleSaveSettings(): void { applyConsents() } function applyConsents(): void { storeConsents(_consents) hideBanner() _config?.onConsentChange?.(_consents) log('Consents applied:', _consents) } // ============================================================================ // Public API // ============================================================================ function init(config: BreakPilotSDKConfig): void { if (_isInitialized) { log('SDK already initialized') return } _config = config log('Initializing with config:', config) try { _client = new ComplianceClient({ apiEndpoint: config.apiEndpoint, apiKey: config.apiKey, tenantId: config.tenantId, }) // Check for stored consents const storedConsents = getStoredConsents() if (storedConsents) { _consents = storedConsents log('Loaded stored consents:', _consents) config.onConsentChange?.(storedConsents) } else if (config.autoInjectBanner !== false) { // Show banner if no stored consents showMainBanner() } _isInitialized = true config.onReady?.() log('SDK initialized successfully') } catch (error) { const err = error instanceof Error ? error : new Error(String(error)) config.onError?.(err) log('SDK initialization failed:', err) } } function getConsents(): Record { return { ..._consents } } function updateConsent(purpose: ConsentPurpose, granted: boolean): void { if (purpose === 'ESSENTIAL') { log('Cannot change essential consent') return } _consents[purpose] = granted storeConsents(_consents) _config?.onConsentChange?.(_consents) log('Consent updated:', purpose, granted) } function showBanner(): void { showMainBanner() } function hasConsent(purpose: ConsentPurpose): boolean { return _consents[purpose] } function getClient(): ComplianceClient | null { return _client } async function submitDSR( type: DSRRequestType, email: string, name: string ): Promise<{ success: boolean; requestId?: string; error?: string }> { if (!_client) { return { success: false, error: 'SDK not initialized' } } try { // This would call the DSR endpoint log('Submitting DSR:', { type, email, name }) // In a real implementation, this would use the client to submit return { success: true, requestId: `dsr_${Date.now()}` } } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error' return { success: false, error: message } } } function destroy(): void { hideBanner() _client = null _config = null _isInitialized = false log('SDK destroyed') } // ============================================================================ // Export for IIFE // ============================================================================ export const BreakPilotSDK = { init, getConsents, updateConsent, showBanner, hideBanner, hasConsent, getClient, submitDSR, destroy, version: '0.0.1', } // Auto-attach to window for script tag usage if (typeof window !== 'undefined') { ;(window as unknown as Record).BreakPilotSDK = BreakPilotSDK } export default BreakPilotSDK