refactor(consent-sdk): split ConsentManager + framework adapters under 500 LOC
Phase 4: extract config defaults, Google Consent Mode helper, and framework adapter internals into sibling files so every source file is under the hard cap. Public API surface preserved; all 135 tests green, tsup build + tsc typecheck clean. - core/ConsentManager 525 -> 467 LOC (extract config + google helpers) - react/index 511 LOC -> 199 LOC barrel + components/hooks/context - vue/index 511 LOC -> 32 LOC barrel + components/composables/context/plugin - angular/index 509 LOC -> 45 LOC barrel + interface/service/module/templates Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,9 @@
|
|||||||
/**
|
/**
|
||||||
* Angular Integration fuer @breakpilot/consent-sdk
|
* Angular Integration fuer @breakpilot/consent-sdk
|
||||||
*
|
*
|
||||||
|
* Phase 4 refactor: thin barrel. Interface, service, module definition, and
|
||||||
|
* template snippets live in sibling files.
|
||||||
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```typescript
|
* ```typescript
|
||||||
* // app.module.ts
|
* // app.module.ts
|
||||||
@@ -16,494 +19,27 @@
|
|||||||
* })
|
* })
|
||||||
* export class AppModule {}
|
* export class AppModule {}
|
||||||
* ```
|
* ```
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* Angular hat ein komplexeres Build-System (ngc, ng-packagr). Diese Dateien
|
||||||
|
* definieren die Schnittstelle — fuer Production muss ein separates Angular
|
||||||
|
* Library Package erstellt werden (`ng generate library @breakpilot/consent-sdk-angular`).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// =============================================================================
|
export type { IConsentService } from './interface';
|
||||||
// NOTE: Angular SDK Structure
|
export { ConsentServiceBase } from './service';
|
||||||
// =============================================================================
|
export {
|
||||||
//
|
CONSENT_CONFIG,
|
||||||
// Angular hat ein komplexeres Build-System (ngc, ng-packagr).
|
CONSENT_SERVICE,
|
||||||
// Diese Datei definiert die Schnittstelle - fuer Production muss ein
|
ConsentModuleDefinition,
|
||||||
// separates Angular Library Package erstellt werden:
|
consentServiceFactory,
|
||||||
//
|
type ConsentModuleConfig,
|
||||||
// ng generate library @breakpilot/consent-sdk-angular
|
} from './module';
|
||||||
//
|
export { CONSENT_BANNER_TEMPLATE, CONSENT_GATE_USAGE } from './templates';
|
||||||
// Die folgende Implementation ist fuer direkten Import vorgesehen.
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
import { ConsentManager } from '../core/ConsentManager';
|
export type {
|
||||||
import type {
|
ConsentCategories,
|
||||||
|
ConsentCategory,
|
||||||
ConsentConfig,
|
ConsentConfig,
|
||||||
ConsentState,
|
ConsentState,
|
||||||
ConsentCategory,
|
|
||||||
ConsentCategories,
|
|
||||||
} from '../types';
|
} from '../types';
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Angular Service Interface
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ConsentService Interface fuer Angular DI
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* @Component({...})
|
|
||||||
* export class MyComponent {
|
|
||||||
* constructor(private consent: ConsentService) {
|
|
||||||
* if (this.consent.hasConsent('analytics')) {
|
|
||||||
* // Analytics laden
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export interface IConsentService {
|
|
||||||
/** Initialisiert? */
|
|
||||||
readonly isInitialized: boolean;
|
|
||||||
|
|
||||||
/** Laedt noch? */
|
|
||||||
readonly isLoading: boolean;
|
|
||||||
|
|
||||||
/** Banner sichtbar? */
|
|
||||||
readonly isBannerVisible: boolean;
|
|
||||||
|
|
||||||
/** Aktueller Consent-Zustand */
|
|
||||||
readonly consent: ConsentState | null;
|
|
||||||
|
|
||||||
/** Muss Consent eingeholt werden? */
|
|
||||||
readonly needsConsent: boolean;
|
|
||||||
|
|
||||||
/** Prueft Consent fuer Kategorie */
|
|
||||||
hasConsent(category: ConsentCategory): boolean;
|
|
||||||
|
|
||||||
/** Alle akzeptieren */
|
|
||||||
acceptAll(): Promise<void>;
|
|
||||||
|
|
||||||
/** Alle ablehnen */
|
|
||||||
rejectAll(): Promise<void>;
|
|
||||||
|
|
||||||
/** Auswahl speichern */
|
|
||||||
saveSelection(categories: Partial<ConsentCategories>): Promise<void>;
|
|
||||||
|
|
||||||
/** Banner anzeigen */
|
|
||||||
showBanner(): void;
|
|
||||||
|
|
||||||
/** Banner ausblenden */
|
|
||||||
hideBanner(): void;
|
|
||||||
|
|
||||||
/** Einstellungen oeffnen */
|
|
||||||
showSettings(): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// ConsentService Implementation
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ConsentService - Angular Service Wrapper
|
|
||||||
*
|
|
||||||
* Diese Klasse kann als Angular Service registriert werden:
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* // consent.service.ts
|
|
||||||
* import { Injectable } from '@angular/core';
|
|
||||||
* import { ConsentServiceBase } from '@breakpilot/consent-sdk/angular';
|
|
||||||
*
|
|
||||||
* @Injectable({ providedIn: 'root' })
|
|
||||||
* export class ConsentService extends ConsentServiceBase {
|
|
||||||
* constructor() {
|
|
||||||
* super({
|
|
||||||
* apiEndpoint: environment.consentApiEndpoint,
|
|
||||||
* siteId: environment.siteId,
|
|
||||||
* });
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export class ConsentServiceBase implements IConsentService {
|
|
||||||
private manager: ConsentManager;
|
|
||||||
private _consent: ConsentState | null = null;
|
|
||||||
private _isInitialized = false;
|
|
||||||
private _isLoading = true;
|
|
||||||
private _isBannerVisible = false;
|
|
||||||
|
|
||||||
// Callbacks fuer Angular Change Detection
|
|
||||||
private changeCallbacks: Array<(consent: ConsentState) => void> = [];
|
|
||||||
private bannerShowCallbacks: Array<() => void> = [];
|
|
||||||
private bannerHideCallbacks: Array<() => void> = [];
|
|
||||||
|
|
||||||
constructor(config: ConsentConfig) {
|
|
||||||
this.manager = new ConsentManager(config);
|
|
||||||
this.setupEventListeners();
|
|
||||||
this.initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Getters
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
get isInitialized(): boolean {
|
|
||||||
return this._isInitialized;
|
|
||||||
}
|
|
||||||
|
|
||||||
get isLoading(): boolean {
|
|
||||||
return this._isLoading;
|
|
||||||
}
|
|
||||||
|
|
||||||
get isBannerVisible(): boolean {
|
|
||||||
return this._isBannerVisible;
|
|
||||||
}
|
|
||||||
|
|
||||||
get consent(): ConsentState | null {
|
|
||||||
return this._consent;
|
|
||||||
}
|
|
||||||
|
|
||||||
get needsConsent(): boolean {
|
|
||||||
return this.manager.needsConsent();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Methods
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
hasConsent(category: ConsentCategory): boolean {
|
|
||||||
return this.manager.hasConsent(category);
|
|
||||||
}
|
|
||||||
|
|
||||||
async acceptAll(): Promise<void> {
|
|
||||||
await this.manager.acceptAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
async rejectAll(): Promise<void> {
|
|
||||||
await this.manager.rejectAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
async saveSelection(categories: Partial<ConsentCategories>): Promise<void> {
|
|
||||||
await this.manager.setConsent(categories);
|
|
||||||
this.manager.hideBanner();
|
|
||||||
}
|
|
||||||
|
|
||||||
showBanner(): void {
|
|
||||||
this.manager.showBanner();
|
|
||||||
}
|
|
||||||
|
|
||||||
hideBanner(): void {
|
|
||||||
this.manager.hideBanner();
|
|
||||||
}
|
|
||||||
|
|
||||||
showSettings(): void {
|
|
||||||
this.manager.showSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Change Detection Support
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registriert Callback fuer Consent-Aenderungen
|
|
||||||
* (fuer Angular Change Detection)
|
|
||||||
*/
|
|
||||||
onConsentChange(callback: (consent: ConsentState) => void): () => void {
|
|
||||||
this.changeCallbacks.push(callback);
|
|
||||||
return () => {
|
|
||||||
const index = this.changeCallbacks.indexOf(callback);
|
|
||||||
if (index > -1) {
|
|
||||||
this.changeCallbacks.splice(index, 1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registriert Callback wenn Banner angezeigt wird
|
|
||||||
*/
|
|
||||||
onBannerShow(callback: () => void): () => void {
|
|
||||||
this.bannerShowCallbacks.push(callback);
|
|
||||||
return () => {
|
|
||||||
const index = this.bannerShowCallbacks.indexOf(callback);
|
|
||||||
if (index > -1) {
|
|
||||||
this.bannerShowCallbacks.splice(index, 1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registriert Callback wenn Banner ausgeblendet wird
|
|
||||||
*/
|
|
||||||
onBannerHide(callback: () => void): () => void {
|
|
||||||
this.bannerHideCallbacks.push(callback);
|
|
||||||
return () => {
|
|
||||||
const index = this.bannerHideCallbacks.indexOf(callback);
|
|
||||||
if (index > -1) {
|
|
||||||
this.bannerHideCallbacks.splice(index, 1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Internal
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
private setupEventListeners(): void {
|
|
||||||
this.manager.on('change', (consent) => {
|
|
||||||
this._consent = consent;
|
|
||||||
this.changeCallbacks.forEach((cb) => cb(consent));
|
|
||||||
});
|
|
||||||
|
|
||||||
this.manager.on('banner_show', () => {
|
|
||||||
this._isBannerVisible = true;
|
|
||||||
this.bannerShowCallbacks.forEach((cb) => cb());
|
|
||||||
});
|
|
||||||
|
|
||||||
this.manager.on('banner_hide', () => {
|
|
||||||
this._isBannerVisible = false;
|
|
||||||
this.bannerHideCallbacks.forEach((cb) => cb());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async initialize(): Promise<void> {
|
|
||||||
try {
|
|
||||||
await this.manager.init();
|
|
||||||
this._consent = this.manager.getConsent();
|
|
||||||
this._isInitialized = true;
|
|
||||||
this._isBannerVisible = this.manager.isBannerVisible();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to initialize ConsentManager:', error);
|
|
||||||
} finally {
|
|
||||||
this._isLoading = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Angular Module Configuration
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Konfiguration fuer ConsentModule.forRoot()
|
|
||||||
*/
|
|
||||||
export interface ConsentModuleConfig extends ConsentConfig {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Token fuer Dependency Injection
|
|
||||||
* Verwendung mit Angular @Inject():
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* constructor(@Inject(CONSENT_CONFIG) private config: ConsentConfig) {}
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export const CONSENT_CONFIG = 'CONSENT_CONFIG';
|
|
||||||
export const CONSENT_SERVICE = 'CONSENT_SERVICE';
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Factory Functions fuer Angular DI
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Factory fuer ConsentService
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* // app.module.ts
|
|
||||||
* providers: [
|
|
||||||
* { provide: CONSENT_CONFIG, useValue: { apiEndpoint: '...', siteId: '...' } },
|
|
||||||
* { provide: CONSENT_SERVICE, useFactory: consentServiceFactory, deps: [CONSENT_CONFIG] },
|
|
||||||
* ]
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export function consentServiceFactory(config: ConsentConfig): ConsentServiceBase {
|
|
||||||
return new ConsentServiceBase(config);
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Angular Module Definition (Template)
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ConsentModule - Angular Module
|
|
||||||
*
|
|
||||||
* Dies ist eine Template-Definition. Fuer echte Angular-Nutzung
|
|
||||||
* muss ein separates Angular Library Package erstellt werden.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* // In einem Angular Library Package:
|
|
||||||
* @NgModule({
|
|
||||||
* declarations: [ConsentBannerComponent, ConsentGateDirective],
|
|
||||||
* exports: [ConsentBannerComponent, ConsentGateDirective],
|
|
||||||
* })
|
|
||||||
* export class ConsentModule {
|
|
||||||
* static forRoot(config: ConsentModuleConfig): ModuleWithProviders<ConsentModule> {
|
|
||||||
* return {
|
|
||||||
* ngModule: ConsentModule,
|
|
||||||
* providers: [
|
|
||||||
* { provide: CONSENT_CONFIG, useValue: config },
|
|
||||||
* { provide: CONSENT_SERVICE, useFactory: consentServiceFactory, deps: [CONSENT_CONFIG] },
|
|
||||||
* ],
|
|
||||||
* };
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export const ConsentModuleDefinition = {
|
|
||||||
/**
|
|
||||||
* Providers fuer Root-Module
|
|
||||||
*/
|
|
||||||
forRoot: (config: ConsentModuleConfig) => ({
|
|
||||||
provide: CONSENT_CONFIG,
|
|
||||||
useValue: config,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Component Templates (fuer Angular Library)
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ConsentBannerComponent Template
|
|
||||||
*
|
|
||||||
* Fuer Angular Library Implementation:
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* @Component({
|
|
||||||
* selector: 'bp-consent-banner',
|
|
||||||
* template: CONSENT_BANNER_TEMPLATE,
|
|
||||||
* styles: [CONSENT_BANNER_STYLES],
|
|
||||||
* })
|
|
||||||
* export class ConsentBannerComponent {
|
|
||||||
* constructor(public consent: ConsentService) {}
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export const CONSENT_BANNER_TEMPLATE = `
|
|
||||||
<div
|
|
||||||
*ngIf="consent.isBannerVisible"
|
|
||||||
class="bp-consent-banner"
|
|
||||||
role="dialog"
|
|
||||||
aria-modal="true"
|
|
||||||
aria-label="Cookie-Einstellungen"
|
|
||||||
>
|
|
||||||
<div class="bp-consent-banner-content">
|
|
||||||
<h2>Datenschutzeinstellungen</h2>
|
|
||||||
<p>
|
|
||||||
Wir nutzen Cookies und ähnliche Technologien, um Ihnen ein optimales
|
|
||||||
Nutzererlebnis zu bieten.
|
|
||||||
</p>
|
|
||||||
<div class="bp-consent-banner-actions">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="bp-consent-btn bp-consent-btn-reject"
|
|
||||||
(click)="consent.rejectAll()"
|
|
||||||
>
|
|
||||||
Alle ablehnen
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="bp-consent-btn bp-consent-btn-settings"
|
|
||||||
(click)="consent.showSettings()"
|
|
||||||
>
|
|
||||||
Einstellungen
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="bp-consent-btn bp-consent-btn-accept"
|
|
||||||
(click)="consent.acceptAll()"
|
|
||||||
>
|
|
||||||
Alle akzeptieren
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ConsentGateDirective Template
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* @Directive({
|
|
||||||
* selector: '[bpConsentGate]',
|
|
||||||
* })
|
|
||||||
* export class ConsentGateDirective implements OnInit, OnDestroy {
|
|
||||||
* @Input('bpConsentGate') category!: ConsentCategory;
|
|
||||||
*
|
|
||||||
* private unsubscribe?: () => void;
|
|
||||||
*
|
|
||||||
* constructor(
|
|
||||||
* private templateRef: TemplateRef<any>,
|
|
||||||
* private viewContainer: ViewContainerRef,
|
|
||||||
* private consent: ConsentService
|
|
||||||
* ) {}
|
|
||||||
*
|
|
||||||
* ngOnInit() {
|
|
||||||
* this.updateView();
|
|
||||||
* this.unsubscribe = this.consent.onConsentChange(() => this.updateView());
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* ngOnDestroy() {
|
|
||||||
* this.unsubscribe?.();
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* private updateView() {
|
|
||||||
* if (this.consent.hasConsent(this.category)) {
|
|
||||||
* this.viewContainer.createEmbeddedView(this.templateRef);
|
|
||||||
* } else {
|
|
||||||
* this.viewContainer.clear();
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export const CONSENT_GATE_USAGE = `
|
|
||||||
<!-- Verwendung in Templates -->
|
|
||||||
<div *bpConsentGate="'analytics'">
|
|
||||||
<analytics-component></analytics-component>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Mit else Template -->
|
|
||||||
<ng-container *bpConsentGate="'marketing'; else placeholder">
|
|
||||||
<marketing-component></marketing-component>
|
|
||||||
</ng-container>
|
|
||||||
<ng-template #placeholder>
|
|
||||||
<p>Bitte akzeptieren Sie Marketing-Cookies.</p>
|
|
||||||
</ng-template>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// RxJS Observable Wrapper (Optional)
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* RxJS Observable Wrapper fuer ConsentService
|
|
||||||
*
|
|
||||||
* Fuer Projekte die RxJS bevorzugen:
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* import { BehaviorSubject, Observable } from 'rxjs';
|
|
||||||
*
|
|
||||||
* export class ConsentServiceRx extends ConsentServiceBase {
|
|
||||||
* private consentSubject = new BehaviorSubject<ConsentState | null>(null);
|
|
||||||
* private bannerVisibleSubject = new BehaviorSubject<boolean>(false);
|
|
||||||
*
|
|
||||||
* consent$ = this.consentSubject.asObservable();
|
|
||||||
* isBannerVisible$ = this.bannerVisibleSubject.asObservable();
|
|
||||||
*
|
|
||||||
* constructor(config: ConsentConfig) {
|
|
||||||
* super(config);
|
|
||||||
* this.onConsentChange((c) => this.consentSubject.next(c));
|
|
||||||
* this.onBannerShow(() => this.bannerVisibleSubject.next(true));
|
|
||||||
* this.onBannerHide(() => this.bannerVisibleSubject.next(false));
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Exports
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
export type { ConsentConfig, ConsentState, ConsentCategory, ConsentCategories };
|
|
||||||
|
|||||||
64
consent-sdk/src/angular/interface.ts
Normal file
64
consent-sdk/src/angular/interface.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
/**
|
||||||
|
* Angular IConsentService — interface for DI.
|
||||||
|
*
|
||||||
|
* Phase 4: extracted from angular/index.ts.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ConsentCategories,
|
||||||
|
ConsentCategory,
|
||||||
|
ConsentState,
|
||||||
|
} from '../types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ConsentService Interface fuer Angular DI
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* @Component({...})
|
||||||
|
* export class MyComponent {
|
||||||
|
* constructor(private consent: ConsentService) {
|
||||||
|
* if (this.consent.hasConsent('analytics')) {
|
||||||
|
* // Analytics laden
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export interface IConsentService {
|
||||||
|
/** Initialisiert? */
|
||||||
|
readonly isInitialized: boolean;
|
||||||
|
|
||||||
|
/** Laedt noch? */
|
||||||
|
readonly isLoading: boolean;
|
||||||
|
|
||||||
|
/** Banner sichtbar? */
|
||||||
|
readonly isBannerVisible: boolean;
|
||||||
|
|
||||||
|
/** Aktueller Consent-Zustand */
|
||||||
|
readonly consent: ConsentState | null;
|
||||||
|
|
||||||
|
/** Muss Consent eingeholt werden? */
|
||||||
|
readonly needsConsent: boolean;
|
||||||
|
|
||||||
|
/** Prueft Consent fuer Kategorie */
|
||||||
|
hasConsent(category: ConsentCategory): boolean;
|
||||||
|
|
||||||
|
/** Alle akzeptieren */
|
||||||
|
acceptAll(): Promise<void>;
|
||||||
|
|
||||||
|
/** Alle ablehnen */
|
||||||
|
rejectAll(): Promise<void>;
|
||||||
|
|
||||||
|
/** Auswahl speichern */
|
||||||
|
saveSelection(categories: Partial<ConsentCategories>): Promise<void>;
|
||||||
|
|
||||||
|
/** Banner anzeigen */
|
||||||
|
showBanner(): void;
|
||||||
|
|
||||||
|
/** Banner ausblenden */
|
||||||
|
hideBanner(): void;
|
||||||
|
|
||||||
|
/** Einstellungen oeffnen */
|
||||||
|
showSettings(): void;
|
||||||
|
}
|
||||||
79
consent-sdk/src/angular/module.ts
Normal file
79
consent-sdk/src/angular/module.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
/**
|
||||||
|
* Angular Module configuration — DI tokens, factory, module definition.
|
||||||
|
*
|
||||||
|
* Phase 4: extracted from angular/index.ts.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { ConsentConfig } from '../types';
|
||||||
|
import { ConsentServiceBase } from './service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Konfiguration fuer ConsentModule.forRoot()
|
||||||
|
*/
|
||||||
|
export interface ConsentModuleConfig extends ConsentConfig {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Token fuer Dependency Injection
|
||||||
|
* Verwendung mit Angular @Inject():
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* constructor(@Inject(CONSENT_CONFIG) private config: ConsentConfig) {}
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const CONSENT_CONFIG = 'CONSENT_CONFIG';
|
||||||
|
export const CONSENT_SERVICE = 'CONSENT_SERVICE';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory fuer ConsentService
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* // app.module.ts
|
||||||
|
* providers: [
|
||||||
|
* { provide: CONSENT_CONFIG, useValue: { apiEndpoint: '...', siteId: '...' } },
|
||||||
|
* { provide: CONSENT_SERVICE, useFactory: consentServiceFactory, deps: [CONSENT_CONFIG] },
|
||||||
|
* ]
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function consentServiceFactory(
|
||||||
|
config: ConsentConfig
|
||||||
|
): ConsentServiceBase {
|
||||||
|
return new ConsentServiceBase(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ConsentModule - Angular Module
|
||||||
|
*
|
||||||
|
* Dies ist eine Template-Definition. Fuer echte Angular-Nutzung
|
||||||
|
* muss ein separates Angular Library Package erstellt werden.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* // In einem Angular Library Package:
|
||||||
|
* @NgModule({
|
||||||
|
* declarations: [ConsentBannerComponent, ConsentGateDirective],
|
||||||
|
* exports: [ConsentBannerComponent, ConsentGateDirective],
|
||||||
|
* })
|
||||||
|
* export class ConsentModule {
|
||||||
|
* static forRoot(config: ConsentModuleConfig): ModuleWithProviders<ConsentModule> {
|
||||||
|
* return {
|
||||||
|
* ngModule: ConsentModule,
|
||||||
|
* providers: [
|
||||||
|
* { provide: CONSENT_CONFIG, useValue: config },
|
||||||
|
* { provide: CONSENT_SERVICE, useFactory: consentServiceFactory, deps: [CONSENT_CONFIG] },
|
||||||
|
* ],
|
||||||
|
* };
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const ConsentModuleDefinition = {
|
||||||
|
/**
|
||||||
|
* Providers fuer Root-Module
|
||||||
|
*/
|
||||||
|
forRoot: (config: ConsentModuleConfig) => ({
|
||||||
|
provide: CONSENT_CONFIG,
|
||||||
|
useValue: config,
|
||||||
|
}),
|
||||||
|
};
|
||||||
190
consent-sdk/src/angular/service.ts
Normal file
190
consent-sdk/src/angular/service.ts
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
/**
|
||||||
|
* ConsentServiceBase — Angular Service Wrapper.
|
||||||
|
*
|
||||||
|
* Phase 4: extracted from angular/index.ts.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ConsentManager } from '../core/ConsentManager';
|
||||||
|
import type {
|
||||||
|
ConsentCategories,
|
||||||
|
ConsentCategory,
|
||||||
|
ConsentConfig,
|
||||||
|
ConsentState,
|
||||||
|
} from '../types';
|
||||||
|
import type { IConsentService } from './interface';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ConsentService - Angular Service Wrapper
|
||||||
|
*
|
||||||
|
* Diese Klasse kann als Angular Service registriert werden:
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* // consent.service.ts
|
||||||
|
* import { Injectable } from '@angular/core';
|
||||||
|
* import { ConsentServiceBase } from '@breakpilot/consent-sdk/angular';
|
||||||
|
*
|
||||||
|
* @Injectable({ providedIn: 'root' })
|
||||||
|
* export class ConsentService extends ConsentServiceBase {
|
||||||
|
* constructor() {
|
||||||
|
* super({
|
||||||
|
* apiEndpoint: environment.consentApiEndpoint,
|
||||||
|
* siteId: environment.siteId,
|
||||||
|
* });
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export class ConsentServiceBase implements IConsentService {
|
||||||
|
private manager: ConsentManager;
|
||||||
|
private _consent: ConsentState | null = null;
|
||||||
|
private _isInitialized = false;
|
||||||
|
private _isLoading = true;
|
||||||
|
private _isBannerVisible = false;
|
||||||
|
|
||||||
|
// Callbacks fuer Angular Change Detection
|
||||||
|
private changeCallbacks: Array<(consent: ConsentState) => void> = [];
|
||||||
|
private bannerShowCallbacks: Array<() => void> = [];
|
||||||
|
private bannerHideCallbacks: Array<() => void> = [];
|
||||||
|
|
||||||
|
constructor(config: ConsentConfig) {
|
||||||
|
this.manager = new ConsentManager(config);
|
||||||
|
this.setupEventListeners();
|
||||||
|
this.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Getters
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
get isInitialized(): boolean {
|
||||||
|
return this._isInitialized;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isLoading(): boolean {
|
||||||
|
return this._isLoading;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isBannerVisible(): boolean {
|
||||||
|
return this._isBannerVisible;
|
||||||
|
}
|
||||||
|
|
||||||
|
get consent(): ConsentState | null {
|
||||||
|
return this._consent;
|
||||||
|
}
|
||||||
|
|
||||||
|
get needsConsent(): boolean {
|
||||||
|
return this.manager.needsConsent();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Methods
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
hasConsent(category: ConsentCategory): boolean {
|
||||||
|
return this.manager.hasConsent(category);
|
||||||
|
}
|
||||||
|
|
||||||
|
async acceptAll(): Promise<void> {
|
||||||
|
await this.manager.acceptAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
async rejectAll(): Promise<void> {
|
||||||
|
await this.manager.rejectAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveSelection(categories: Partial<ConsentCategories>): Promise<void> {
|
||||||
|
await this.manager.setConsent(categories);
|
||||||
|
this.manager.hideBanner();
|
||||||
|
}
|
||||||
|
|
||||||
|
showBanner(): void {
|
||||||
|
this.manager.showBanner();
|
||||||
|
}
|
||||||
|
|
||||||
|
hideBanner(): void {
|
||||||
|
this.manager.hideBanner();
|
||||||
|
}
|
||||||
|
|
||||||
|
showSettings(): void {
|
||||||
|
this.manager.showSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Change Detection Support
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registriert Callback fuer Consent-Aenderungen
|
||||||
|
* (fuer Angular Change Detection)
|
||||||
|
*/
|
||||||
|
onConsentChange(callback: (consent: ConsentState) => void): () => void {
|
||||||
|
this.changeCallbacks.push(callback);
|
||||||
|
return () => {
|
||||||
|
const index = this.changeCallbacks.indexOf(callback);
|
||||||
|
if (index > -1) {
|
||||||
|
this.changeCallbacks.splice(index, 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registriert Callback wenn Banner angezeigt wird
|
||||||
|
*/
|
||||||
|
onBannerShow(callback: () => void): () => void {
|
||||||
|
this.bannerShowCallbacks.push(callback);
|
||||||
|
return () => {
|
||||||
|
const index = this.bannerShowCallbacks.indexOf(callback);
|
||||||
|
if (index > -1) {
|
||||||
|
this.bannerShowCallbacks.splice(index, 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registriert Callback wenn Banner ausgeblendet wird
|
||||||
|
*/
|
||||||
|
onBannerHide(callback: () => void): () => void {
|
||||||
|
this.bannerHideCallbacks.push(callback);
|
||||||
|
return () => {
|
||||||
|
const index = this.bannerHideCallbacks.indexOf(callback);
|
||||||
|
if (index > -1) {
|
||||||
|
this.bannerHideCallbacks.splice(index, 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Internal
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
private setupEventListeners(): void {
|
||||||
|
this.manager.on('change', (consent) => {
|
||||||
|
this._consent = consent;
|
||||||
|
this.changeCallbacks.forEach((cb) => cb(consent));
|
||||||
|
});
|
||||||
|
|
||||||
|
this.manager.on('banner_show', () => {
|
||||||
|
this._isBannerVisible = true;
|
||||||
|
this.bannerShowCallbacks.forEach((cb) => cb());
|
||||||
|
});
|
||||||
|
|
||||||
|
this.manager.on('banner_hide', () => {
|
||||||
|
this._isBannerVisible = false;
|
||||||
|
this.bannerHideCallbacks.forEach((cb) => cb());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async initialize(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await this.manager.init();
|
||||||
|
this._consent = this.manager.getConsent();
|
||||||
|
this._isInitialized = true;
|
||||||
|
this._isBannerVisible = this.manager.isBannerVisible();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to initialize ConsentManager:', error);
|
||||||
|
} finally {
|
||||||
|
this._isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
142
consent-sdk/src/angular/templates.ts
Normal file
142
consent-sdk/src/angular/templates.ts
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
/**
|
||||||
|
* Angular component templates — Banner + Gate directive reference snippets.
|
||||||
|
*
|
||||||
|
* Phase 4: extracted from angular/index.ts.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ConsentBannerComponent Template
|
||||||
|
*
|
||||||
|
* Fuer Angular Library Implementation:
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* @Component({
|
||||||
|
* selector: 'bp-consent-banner',
|
||||||
|
* template: CONSENT_BANNER_TEMPLATE,
|
||||||
|
* styles: [CONSENT_BANNER_STYLES],
|
||||||
|
* })
|
||||||
|
* export class ConsentBannerComponent {
|
||||||
|
* constructor(public consent: ConsentService) {}
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const CONSENT_BANNER_TEMPLATE = `
|
||||||
|
<div
|
||||||
|
*ngIf="consent.isBannerVisible"
|
||||||
|
class="bp-consent-banner"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-label="Cookie-Einstellungen"
|
||||||
|
>
|
||||||
|
<div class="bp-consent-banner-content">
|
||||||
|
<h2>Datenschutzeinstellungen</h2>
|
||||||
|
<p>
|
||||||
|
Wir nutzen Cookies und ähnliche Technologien, um Ihnen ein optimales
|
||||||
|
Nutzererlebnis zu bieten.
|
||||||
|
</p>
|
||||||
|
<div class="bp-consent-banner-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="bp-consent-btn bp-consent-btn-reject"
|
||||||
|
(click)="consent.rejectAll()"
|
||||||
|
>
|
||||||
|
Alle ablehnen
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="bp-consent-btn bp-consent-btn-settings"
|
||||||
|
(click)="consent.showSettings()"
|
||||||
|
>
|
||||||
|
Einstellungen
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="bp-consent-btn bp-consent-btn-accept"
|
||||||
|
(click)="consent.acceptAll()"
|
||||||
|
>
|
||||||
|
Alle akzeptieren
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ConsentGateDirective Template
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* @Directive({
|
||||||
|
* selector: '[bpConsentGate]',
|
||||||
|
* })
|
||||||
|
* export class ConsentGateDirective implements OnInit, OnDestroy {
|
||||||
|
* @Input('bpConsentGate') category!: ConsentCategory;
|
||||||
|
*
|
||||||
|
* private unsubscribe?: () => void;
|
||||||
|
*
|
||||||
|
* constructor(
|
||||||
|
* private templateRef: TemplateRef<any>,
|
||||||
|
* private viewContainer: ViewContainerRef,
|
||||||
|
* private consent: ConsentService
|
||||||
|
* ) {}
|
||||||
|
*
|
||||||
|
* ngOnInit() {
|
||||||
|
* this.updateView();
|
||||||
|
* this.unsubscribe = this.consent.onConsentChange(() => this.updateView());
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* ngOnDestroy() {
|
||||||
|
* this.unsubscribe?.();
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* private updateView() {
|
||||||
|
* if (this.consent.hasConsent(this.category)) {
|
||||||
|
* this.viewContainer.createEmbeddedView(this.templateRef);
|
||||||
|
* } else {
|
||||||
|
* this.viewContainer.clear();
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const CONSENT_GATE_USAGE = `
|
||||||
|
<!-- Verwendung in Templates -->
|
||||||
|
<div *bpConsentGate="'analytics'">
|
||||||
|
<analytics-component></analytics-component>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mit else Template -->
|
||||||
|
<ng-container *bpConsentGate="'marketing'; else placeholder">
|
||||||
|
<marketing-component></marketing-component>
|
||||||
|
</ng-container>
|
||||||
|
<ng-template #placeholder>
|
||||||
|
<p>Bitte akzeptieren Sie Marketing-Cookies.</p>
|
||||||
|
</ng-template>
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RxJS Observable Wrapper fuer ConsentService
|
||||||
|
*
|
||||||
|
* Fuer Projekte die RxJS bevorzugen:
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* import { BehaviorSubject, Observable } from 'rxjs';
|
||||||
|
*
|
||||||
|
* export class ConsentServiceRx extends ConsentServiceBase {
|
||||||
|
* private consentSubject = new BehaviorSubject<ConsentState | null>(null);
|
||||||
|
* private bannerVisibleSubject = new BehaviorSubject<boolean>(false);
|
||||||
|
*
|
||||||
|
* consent$ = this.consentSubject.asObservable();
|
||||||
|
* isBannerVisible$ = this.bannerVisibleSubject.asObservable();
|
||||||
|
*
|
||||||
|
* constructor(config: ConsentConfig) {
|
||||||
|
* super(config);
|
||||||
|
* this.onConsentChange((c) => this.consentSubject.next(c));
|
||||||
|
* this.onBannerShow(() => this.bannerVisibleSubject.next(true));
|
||||||
|
* this.onBannerHide(() => this.bannerVisibleSubject.next(false));
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
@@ -20,45 +20,11 @@ import { ConsentAPI } from './ConsentAPI';
|
|||||||
import { EventEmitter } from '../utils/EventEmitter';
|
import { EventEmitter } from '../utils/EventEmitter';
|
||||||
import { generateFingerprint } from '../utils/fingerprint';
|
import { generateFingerprint } from '../utils/fingerprint';
|
||||||
import { SDK_VERSION } from '../version';
|
import { SDK_VERSION } from '../version';
|
||||||
|
import {
|
||||||
/**
|
DEFAULT_CONSENT,
|
||||||
* Default-Konfiguration
|
mergeConsentConfig,
|
||||||
*/
|
} from './consent-manager-config';
|
||||||
const DEFAULT_CONFIG: Partial<ConsentConfig> = {
|
import { updateGoogleConsentMode as applyGoogleConsent } from './consent-manager-google';
|
||||||
language: 'de',
|
|
||||||
fallbackLanguage: 'en',
|
|
||||||
ui: {
|
|
||||||
position: 'bottom',
|
|
||||||
layout: 'modal',
|
|
||||||
theme: 'auto',
|
|
||||||
zIndex: 999999,
|
|
||||||
blockScrollOnModal: true,
|
|
||||||
},
|
|
||||||
consent: {
|
|
||||||
required: true,
|
|
||||||
rejectAllVisible: true,
|
|
||||||
acceptAllVisible: true,
|
|
||||||
granularControl: true,
|
|
||||||
vendorControl: false,
|
|
||||||
rememberChoice: true,
|
|
||||||
rememberDays: 365,
|
|
||||||
geoTargeting: false,
|
|
||||||
recheckAfterDays: 180,
|
|
||||||
},
|
|
||||||
categories: ['essential', 'functional', 'analytics', 'marketing', 'social'],
|
|
||||||
debug: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default Consent-State (nur Essential aktiv)
|
|
||||||
*/
|
|
||||||
const DEFAULT_CONSENT: ConsentCategories = {
|
|
||||||
essential: true,
|
|
||||||
functional: false,
|
|
||||||
analytics: false,
|
|
||||||
marketing: false,
|
|
||||||
social: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ConsentManager - Zentrale Klasse fuer Consent-Verwaltung
|
* ConsentManager - Zentrale Klasse fuer Consent-Verwaltung
|
||||||
@@ -389,15 +355,10 @@ export class ConsentManager {
|
|||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Konfiguration zusammenfuehren
|
* Konfiguration zusammenfuehren — delegates to the extracted helper.
|
||||||
*/
|
*/
|
||||||
private mergeConfig(config: ConsentConfig): ConsentConfig {
|
private mergeConfig(config: ConsentConfig): ConsentConfig {
|
||||||
return {
|
return mergeConsentConfig(config);
|
||||||
...DEFAULT_CONFIG,
|
|
||||||
...config,
|
|
||||||
ui: { ...DEFAULT_CONFIG.ui, ...config.ui },
|
|
||||||
consent: { ...DEFAULT_CONFIG.consent, ...config.consent },
|
|
||||||
} as ConsentConfig;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -434,32 +395,13 @@ export class ConsentManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Google Consent Mode v2 aktualisieren
|
* Google Consent Mode v2 aktualisieren — delegates to the extracted helper.
|
||||||
*/
|
*/
|
||||||
private updateGoogleConsentMode(): void {
|
private updateGoogleConsentMode(): void {
|
||||||
if (typeof window === 'undefined' || !this.currentConsent) {
|
if (applyGoogleConsent(this.currentConsent)) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const gtag = (window as unknown as { gtag?: (...args: unknown[]) => void }).gtag;
|
|
||||||
if (typeof gtag !== 'function') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { categories } = this.currentConsent;
|
|
||||||
|
|
||||||
gtag('consent', 'update', {
|
|
||||||
ad_storage: categories.marketing ? 'granted' : 'denied',
|
|
||||||
ad_user_data: categories.marketing ? 'granted' : 'denied',
|
|
||||||
ad_personalization: categories.marketing ? 'granted' : 'denied',
|
|
||||||
analytics_storage: categories.analytics ? 'granted' : 'denied',
|
|
||||||
functionality_storage: categories.functional ? 'granted' : 'denied',
|
|
||||||
personalization_storage: categories.functional ? 'granted' : 'denied',
|
|
||||||
security_storage: 'granted',
|
|
||||||
});
|
|
||||||
|
|
||||||
this.log('Google Consent Mode updated');
|
this.log('Google Consent Mode updated');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pruefen ob Consent abgelaufen
|
* Pruefen ob Consent abgelaufen
|
||||||
|
|||||||
58
consent-sdk/src/core/consent-manager-config.ts
Normal file
58
consent-sdk/src/core/consent-manager-config.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
/**
|
||||||
|
* ConsentManager default configuration + merge helpers.
|
||||||
|
*
|
||||||
|
* Phase 4: extracted from ConsentManager.ts to keep the main class under 500 LOC.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { ConsentCategories, ConsentConfig } from '../types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default configuration applied when a consumer omits optional fields.
|
||||||
|
*/
|
||||||
|
export const DEFAULT_CONFIG: Partial<ConsentConfig> = {
|
||||||
|
language: 'de',
|
||||||
|
fallbackLanguage: 'en',
|
||||||
|
ui: {
|
||||||
|
position: 'bottom',
|
||||||
|
layout: 'modal',
|
||||||
|
theme: 'auto',
|
||||||
|
zIndex: 999999,
|
||||||
|
blockScrollOnModal: true,
|
||||||
|
},
|
||||||
|
consent: {
|
||||||
|
required: true,
|
||||||
|
rejectAllVisible: true,
|
||||||
|
acceptAllVisible: true,
|
||||||
|
granularControl: true,
|
||||||
|
vendorControl: false,
|
||||||
|
rememberChoice: true,
|
||||||
|
rememberDays: 365,
|
||||||
|
geoTargeting: false,
|
||||||
|
recheckAfterDays: 180,
|
||||||
|
},
|
||||||
|
categories: ['essential', 'functional', 'analytics', 'marketing', 'social'],
|
||||||
|
debug: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default consent state — only essential category is active.
|
||||||
|
*/
|
||||||
|
export const DEFAULT_CONSENT: ConsentCategories = {
|
||||||
|
essential: true,
|
||||||
|
functional: false,
|
||||||
|
analytics: false,
|
||||||
|
marketing: false,
|
||||||
|
social: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge a user-supplied config onto DEFAULT_CONFIG, preserving nested objects.
|
||||||
|
*/
|
||||||
|
export function mergeConsentConfig(config: ConsentConfig): ConsentConfig {
|
||||||
|
return {
|
||||||
|
...DEFAULT_CONFIG,
|
||||||
|
...config,
|
||||||
|
ui: { ...DEFAULT_CONFIG.ui, ...config.ui },
|
||||||
|
consent: { ...DEFAULT_CONFIG.consent, ...config.consent },
|
||||||
|
} as ConsentConfig;
|
||||||
|
}
|
||||||
38
consent-sdk/src/core/consent-manager-google.ts
Normal file
38
consent-sdk/src/core/consent-manager-google.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* Google Consent Mode v2 integration helper.
|
||||||
|
*
|
||||||
|
* Phase 4: extracted from ConsentManager.ts. Updates gtag() with the
|
||||||
|
* current consent category state whenever consent changes.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { ConsentState } from '../types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update Google Consent Mode v2 based on the current consent categories.
|
||||||
|
* No-op when running outside the browser or when gtag is not loaded.
|
||||||
|
* Returns true if the gtag update was actually applied.
|
||||||
|
*/
|
||||||
|
export function updateGoogleConsentMode(consent: ConsentState | null): boolean {
|
||||||
|
if (typeof window === 'undefined' || !consent) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const gtag = (window as unknown as { gtag?: (...args: unknown[]) => void }).gtag;
|
||||||
|
if (typeof gtag !== 'function') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { categories } = consent;
|
||||||
|
|
||||||
|
gtag('consent', 'update', {
|
||||||
|
ad_storage: categories.marketing ? 'granted' : 'denied',
|
||||||
|
ad_user_data: categories.marketing ? 'granted' : 'denied',
|
||||||
|
ad_personalization: categories.marketing ? 'granted' : 'denied',
|
||||||
|
analytics_storage: categories.analytics ? 'granted' : 'denied',
|
||||||
|
functionality_storage: categories.functional ? 'granted' : 'denied',
|
||||||
|
personalization_storage: categories.functional ? 'granted' : 'denied',
|
||||||
|
security_storage: 'granted',
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
190
consent-sdk/src/react/components.tsx
Normal file
190
consent-sdk/src/react/components.tsx
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
/**
|
||||||
|
* React UI components for the consent SDK.
|
||||||
|
*
|
||||||
|
* Phase 4: extracted from index.tsx to keep the main file under 500 LOC.
|
||||||
|
* Exports ConsentGate, ConsentPlaceholder, and ConsentBanner (all headless).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { FC, ReactNode } from 'react';
|
||||||
|
import type {
|
||||||
|
ConsentCategories,
|
||||||
|
ConsentCategory,
|
||||||
|
ConsentState,
|
||||||
|
} from '../types';
|
||||||
|
import { useConsent } from './hooks';
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// ConsentGate
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
interface ConsentGateProps {
|
||||||
|
/** Erforderliche Kategorie */
|
||||||
|
category: ConsentCategory;
|
||||||
|
/** Inhalt bei Consent */
|
||||||
|
children: ReactNode;
|
||||||
|
/** Inhalt ohne Consent */
|
||||||
|
placeholder?: ReactNode;
|
||||||
|
/** Fallback waehrend Laden */
|
||||||
|
fallback?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ConsentGate - zeigt Inhalt nur bei Consent.
|
||||||
|
*/
|
||||||
|
export const ConsentGate: FC<ConsentGateProps> = ({
|
||||||
|
category,
|
||||||
|
children,
|
||||||
|
placeholder = null,
|
||||||
|
fallback = null,
|
||||||
|
}) => {
|
||||||
|
const { hasConsent, isLoading } = useConsent();
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <>{fallback}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasConsent(category)) {
|
||||||
|
return <>{placeholder}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{children}</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// ConsentPlaceholder
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
interface ConsentPlaceholderProps {
|
||||||
|
category: ConsentCategory;
|
||||||
|
message?: string;
|
||||||
|
buttonText?: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ConsentPlaceholder - Placeholder fuer blockierten Inhalt.
|
||||||
|
*/
|
||||||
|
export const ConsentPlaceholder: FC<ConsentPlaceholderProps> = ({
|
||||||
|
category,
|
||||||
|
message,
|
||||||
|
buttonText,
|
||||||
|
className = '',
|
||||||
|
}) => {
|
||||||
|
const { showSettings } = useConsent();
|
||||||
|
|
||||||
|
const categoryNames: Record<ConsentCategory, string> = {
|
||||||
|
essential: 'Essentielle Cookies',
|
||||||
|
functional: 'Funktionale Cookies',
|
||||||
|
analytics: 'Statistik-Cookies',
|
||||||
|
marketing: 'Marketing-Cookies',
|
||||||
|
social: 'Social Media-Cookies',
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultMessage = `Dieser Inhalt erfordert ${categoryNames[category]}.`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`bp-consent-placeholder ${className}`}>
|
||||||
|
<p>{message || defaultMessage}</p>
|
||||||
|
<button type="button" onClick={showSettings}>
|
||||||
|
{buttonText || 'Cookie-Einstellungen oeffnen'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// ConsentBanner (headless)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export interface ConsentBannerRenderProps {
|
||||||
|
isVisible: boolean;
|
||||||
|
consent: ConsentState | null;
|
||||||
|
needsConsent: boolean;
|
||||||
|
onAcceptAll: () => void;
|
||||||
|
onRejectAll: () => void;
|
||||||
|
onSaveSelection: (categories: Partial<ConsentCategories>) => void;
|
||||||
|
onShowSettings: () => void;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ConsentBannerProps {
|
||||||
|
render?: (props: ConsentBannerRenderProps) => ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ConsentBanner - Headless Banner-Komponente.
|
||||||
|
* Kann mit eigener UI gerendert werden oder nutzt Default-UI.
|
||||||
|
*/
|
||||||
|
export const ConsentBanner: FC<ConsentBannerProps> = ({ render, className }) => {
|
||||||
|
const {
|
||||||
|
consent,
|
||||||
|
isBannerVisible,
|
||||||
|
needsConsent,
|
||||||
|
acceptAll,
|
||||||
|
rejectAll,
|
||||||
|
saveSelection,
|
||||||
|
showSettings,
|
||||||
|
hideBanner,
|
||||||
|
} = useConsent();
|
||||||
|
|
||||||
|
const renderProps: ConsentBannerRenderProps = {
|
||||||
|
isVisible: isBannerVisible,
|
||||||
|
consent,
|
||||||
|
needsConsent,
|
||||||
|
onAcceptAll: acceptAll,
|
||||||
|
onRejectAll: rejectAll,
|
||||||
|
onSaveSelection: saveSelection,
|
||||||
|
onShowSettings: showSettings,
|
||||||
|
onClose: hideBanner,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (render) {
|
||||||
|
return <>{render(renderProps)}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isBannerVisible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`bp-consent-banner ${className || ''}`}
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-label="Cookie-Einstellungen"
|
||||||
|
>
|
||||||
|
<div className="bp-consent-banner-content">
|
||||||
|
<h2>Datenschutzeinstellungen</h2>
|
||||||
|
<p>
|
||||||
|
Wir nutzen Cookies und aehnliche Technologien, um Ihnen ein optimales
|
||||||
|
Nutzererlebnis zu bieten.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="bp-consent-banner-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="bp-consent-btn bp-consent-btn-reject"
|
||||||
|
onClick={rejectAll}
|
||||||
|
>
|
||||||
|
Alle ablehnen
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="bp-consent-btn bp-consent-btn-settings"
|
||||||
|
onClick={showSettings}
|
||||||
|
>
|
||||||
|
Einstellungen
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="bp-consent-btn bp-consent-btn-accept"
|
||||||
|
onClick={acceptAll}
|
||||||
|
>
|
||||||
|
Alle akzeptieren
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
44
consent-sdk/src/react/context.ts
Normal file
44
consent-sdk/src/react/context.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
/**
|
||||||
|
* Consent context definition — shared by the provider and hooks.
|
||||||
|
*
|
||||||
|
* Phase 4: extracted from index.tsx.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createContext } from 'react';
|
||||||
|
import type { ConsentManager } from '../core/ConsentManager';
|
||||||
|
import type {
|
||||||
|
ConsentCategories,
|
||||||
|
ConsentCategory,
|
||||||
|
ConsentState,
|
||||||
|
} from '../types';
|
||||||
|
|
||||||
|
export interface ConsentContextValue {
|
||||||
|
/** ConsentManager Instanz */
|
||||||
|
manager: ConsentManager | null;
|
||||||
|
/** Aktueller Consent-State */
|
||||||
|
consent: ConsentState | null;
|
||||||
|
/** Ist SDK initialisiert? */
|
||||||
|
isInitialized: boolean;
|
||||||
|
/** Wird geladen? */
|
||||||
|
isLoading: boolean;
|
||||||
|
/** Ist Banner sichtbar? */
|
||||||
|
isBannerVisible: boolean;
|
||||||
|
/** Wird Consent benoetigt? */
|
||||||
|
needsConsent: boolean;
|
||||||
|
/** Consent fuer Kategorie pruefen */
|
||||||
|
hasConsent: (category: ConsentCategory) => boolean;
|
||||||
|
/** Alle akzeptieren */
|
||||||
|
acceptAll: () => Promise<void>;
|
||||||
|
/** Alle ablehnen */
|
||||||
|
rejectAll: () => Promise<void>;
|
||||||
|
/** Auswahl speichern */
|
||||||
|
saveSelection: (categories: Partial<ConsentCategories>) => Promise<void>;
|
||||||
|
/** Banner anzeigen */
|
||||||
|
showBanner: () => void;
|
||||||
|
/** Banner verstecken */
|
||||||
|
hideBanner: () => void;
|
||||||
|
/** Einstellungen oeffnen */
|
||||||
|
showSettings: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ConsentContext = createContext<ConsentContextValue | null>(null);
|
||||||
43
consent-sdk/src/react/hooks.ts
Normal file
43
consent-sdk/src/react/hooks.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
/**
|
||||||
|
* React hooks for the consent SDK.
|
||||||
|
*
|
||||||
|
* Phase 4: extracted from index.tsx to keep the main file under 500 LOC.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useContext } from 'react';
|
||||||
|
import type { ConsentCategory } from '../types';
|
||||||
|
import type { ConsentManager } from '../core/ConsentManager';
|
||||||
|
import { ConsentContext, type ConsentContextValue } from './context';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* useConsent - Consent-Hook.
|
||||||
|
* Overloads: call without args for the full context; pass a category to also get `allowed`.
|
||||||
|
*/
|
||||||
|
export function useConsent(): ConsentContextValue;
|
||||||
|
export function useConsent(
|
||||||
|
category: ConsentCategory
|
||||||
|
): ConsentContextValue & { allowed: boolean };
|
||||||
|
export function useConsent(category?: ConsentCategory) {
|
||||||
|
const context = useContext(ConsentContext);
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useConsent must be used within a ConsentProvider');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (category) {
|
||||||
|
return {
|
||||||
|
...context,
|
||||||
|
allowed: context.hasConsent(category),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* useConsentManager - Direkter Zugriff auf ConsentManager.
|
||||||
|
*/
|
||||||
|
export function useConsentManager(): ConsentManager | null {
|
||||||
|
const context = useContext(ConsentContext);
|
||||||
|
return context?.manager ?? null;
|
||||||
|
}
|
||||||
@@ -14,72 +14,35 @@
|
|||||||
* );
|
* );
|
||||||
* }
|
* }
|
||||||
* ```
|
* ```
|
||||||
|
*
|
||||||
|
* Phase 4 refactor: provider stays here; hooks + components live in sibling
|
||||||
|
* files. Context definition is in ./context so hooks and provider can share it
|
||||||
|
* without circular imports.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
createContext,
|
|
||||||
useContext,
|
|
||||||
useEffect,
|
useEffect,
|
||||||
useState,
|
useState,
|
||||||
useCallback,
|
useCallback,
|
||||||
useMemo,
|
useMemo,
|
||||||
type ReactNode,
|
|
||||||
type FC,
|
type FC,
|
||||||
|
type ReactNode,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { ConsentManager } from '../core/ConsentManager';
|
import { ConsentManager } from '../core/ConsentManager';
|
||||||
import type {
|
import type {
|
||||||
|
ConsentCategories,
|
||||||
|
ConsentCategory,
|
||||||
ConsentConfig,
|
ConsentConfig,
|
||||||
ConsentState,
|
ConsentState,
|
||||||
ConsentCategory,
|
|
||||||
ConsentCategories,
|
|
||||||
} from '../types';
|
} from '../types';
|
||||||
|
import { ConsentContext, type ConsentContextValue } from './context';
|
||||||
// =============================================================================
|
import { useConsent, useConsentManager } from './hooks';
|
||||||
// Context
|
import {
|
||||||
// =============================================================================
|
ConsentBanner,
|
||||||
|
ConsentGate,
|
||||||
interface ConsentContextValue {
|
ConsentPlaceholder,
|
||||||
/** ConsentManager Instanz */
|
type ConsentBannerRenderProps,
|
||||||
manager: ConsentManager | null;
|
} from './components';
|
||||||
|
|
||||||
/** Aktueller Consent-State */
|
|
||||||
consent: ConsentState | null;
|
|
||||||
|
|
||||||
/** Ist SDK initialisiert? */
|
|
||||||
isInitialized: boolean;
|
|
||||||
|
|
||||||
/** Wird geladen? */
|
|
||||||
isLoading: boolean;
|
|
||||||
|
|
||||||
/** Ist Banner sichtbar? */
|
|
||||||
isBannerVisible: boolean;
|
|
||||||
|
|
||||||
/** Wird Consent benoetigt? */
|
|
||||||
needsConsent: boolean;
|
|
||||||
|
|
||||||
/** Consent fuer Kategorie pruefen */
|
|
||||||
hasConsent: (category: ConsentCategory) => boolean;
|
|
||||||
|
|
||||||
/** Alle akzeptieren */
|
|
||||||
acceptAll: () => Promise<void>;
|
|
||||||
|
|
||||||
/** Alle ablehnen */
|
|
||||||
rejectAll: () => Promise<void>;
|
|
||||||
|
|
||||||
/** Auswahl speichern */
|
|
||||||
saveSelection: (categories: Partial<ConsentCategories>) => Promise<void>;
|
|
||||||
|
|
||||||
/** Banner anzeigen */
|
|
||||||
showBanner: () => void;
|
|
||||||
|
|
||||||
/** Banner verstecken */
|
|
||||||
hideBanner: () => void;
|
|
||||||
|
|
||||||
/** Einstellungen oeffnen */
|
|
||||||
showSettings: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ConsentContext = createContext<ConsentContextValue | null>(null);
|
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Provider
|
// Provider
|
||||||
@@ -88,13 +51,12 @@ const ConsentContext = createContext<ConsentContextValue | null>(null);
|
|||||||
interface ConsentProviderProps {
|
interface ConsentProviderProps {
|
||||||
/** SDK-Konfiguration */
|
/** SDK-Konfiguration */
|
||||||
config: ConsentConfig;
|
config: ConsentConfig;
|
||||||
|
|
||||||
/** Kinder-Komponenten */
|
/** Kinder-Komponenten */
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ConsentProvider - Stellt Consent-Kontext bereit
|
* ConsentProvider - Stellt Consent-Kontext bereit.
|
||||||
*/
|
*/
|
||||||
export const ConsentProvider: FC<ConsentProviderProps> = ({
|
export const ConsentProvider: FC<ConsentProviderProps> = ({
|
||||||
config,
|
config,
|
||||||
@@ -228,284 +190,10 @@ export const ConsentProvider: FC<ConsentProviderProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Hooks
|
// Re-exports for the public @breakpilot/consent-sdk/react entrypoint
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* useConsent - Hook fuer Consent-Zugriff
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```tsx
|
|
||||||
* const { hasConsent, acceptAll, rejectAll } = useConsent();
|
|
||||||
*
|
|
||||||
* if (hasConsent('analytics')) {
|
|
||||||
* // Analytics laden
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export function useConsent(): ConsentContextValue;
|
|
||||||
export function useConsent(
|
|
||||||
category: ConsentCategory
|
|
||||||
): ConsentContextValue & { allowed: boolean };
|
|
||||||
export function useConsent(category?: ConsentCategory) {
|
|
||||||
const context = useContext(ConsentContext);
|
|
||||||
|
|
||||||
if (!context) {
|
|
||||||
throw new Error('useConsent must be used within a ConsentProvider');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (category) {
|
|
||||||
return {
|
|
||||||
...context,
|
|
||||||
allowed: context.hasConsent(category),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* useConsentManager - Direkter Zugriff auf ConsentManager
|
|
||||||
*/
|
|
||||||
export function useConsentManager(): ConsentManager | null {
|
|
||||||
const context = useContext(ConsentContext);
|
|
||||||
return context?.manager ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Components
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
interface ConsentGateProps {
|
|
||||||
/** Erforderliche Kategorie */
|
|
||||||
category: ConsentCategory;
|
|
||||||
|
|
||||||
/** Inhalt bei Consent */
|
|
||||||
children: ReactNode;
|
|
||||||
|
|
||||||
/** Inhalt ohne Consent */
|
|
||||||
placeholder?: ReactNode;
|
|
||||||
|
|
||||||
/** Fallback waehrend Laden */
|
|
||||||
fallback?: ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ConsentGate - Zeigt Inhalt nur bei Consent
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```tsx
|
|
||||||
* <ConsentGate
|
|
||||||
* category="analytics"
|
|
||||||
* placeholder={<ConsentPlaceholder category="analytics" />}
|
|
||||||
* >
|
|
||||||
* <GoogleAnalytics />
|
|
||||||
* </ConsentGate>
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export const ConsentGate: FC<ConsentGateProps> = ({
|
|
||||||
category,
|
|
||||||
children,
|
|
||||||
placeholder = null,
|
|
||||||
fallback = null,
|
|
||||||
}) => {
|
|
||||||
const { hasConsent, isLoading } = useConsent();
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return <>{fallback}</>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasConsent(category)) {
|
|
||||||
return <>{placeholder}</>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <>{children}</>;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface ConsentPlaceholderProps {
|
|
||||||
/** Kategorie */
|
|
||||||
category: ConsentCategory;
|
|
||||||
|
|
||||||
/** Custom Nachricht */
|
|
||||||
message?: string;
|
|
||||||
|
|
||||||
/** Custom Button-Text */
|
|
||||||
buttonText?: string;
|
|
||||||
|
|
||||||
/** Custom Styling */
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ConsentPlaceholder - Placeholder fuer blockierten Inhalt
|
|
||||||
*/
|
|
||||||
export const ConsentPlaceholder: FC<ConsentPlaceholderProps> = ({
|
|
||||||
category,
|
|
||||||
message,
|
|
||||||
buttonText,
|
|
||||||
className = '',
|
|
||||||
}) => {
|
|
||||||
const { showSettings } = useConsent();
|
|
||||||
|
|
||||||
const categoryNames: Record<ConsentCategory, string> = {
|
|
||||||
essential: 'Essentielle Cookies',
|
|
||||||
functional: 'Funktionale Cookies',
|
|
||||||
analytics: 'Statistik-Cookies',
|
|
||||||
marketing: 'Marketing-Cookies',
|
|
||||||
social: 'Social Media-Cookies',
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultMessage = `Dieser Inhalt erfordert ${categoryNames[category]}.`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`bp-consent-placeholder ${className}`}>
|
|
||||||
<p>{message || defaultMessage}</p>
|
|
||||||
<button type="button" onClick={showSettings}>
|
|
||||||
{buttonText || 'Cookie-Einstellungen oeffnen'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Banner Component (Headless)
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
interface ConsentBannerRenderProps {
|
|
||||||
/** Ist Banner sichtbar? */
|
|
||||||
isVisible: boolean;
|
|
||||||
|
|
||||||
/** Aktueller Consent */
|
|
||||||
consent: ConsentState | null;
|
|
||||||
|
|
||||||
/** Wird Consent benoetigt? */
|
|
||||||
needsConsent: boolean;
|
|
||||||
|
|
||||||
/** Alle akzeptieren */
|
|
||||||
onAcceptAll: () => void;
|
|
||||||
|
|
||||||
/** Alle ablehnen */
|
|
||||||
onRejectAll: () => void;
|
|
||||||
|
|
||||||
/** Auswahl speichern */
|
|
||||||
onSaveSelection: (categories: Partial<ConsentCategories>) => void;
|
|
||||||
|
|
||||||
/** Einstellungen oeffnen */
|
|
||||||
onShowSettings: () => void;
|
|
||||||
|
|
||||||
/** Banner schliessen */
|
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ConsentBannerProps {
|
|
||||||
/** Render-Funktion fuer Custom UI */
|
|
||||||
render?: (props: ConsentBannerRenderProps) => ReactNode;
|
|
||||||
|
|
||||||
/** Custom Styling */
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ConsentBanner - Headless Banner-Komponente
|
|
||||||
*
|
|
||||||
* Kann mit eigener UI gerendert werden oder nutzt Default-UI.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```tsx
|
|
||||||
* // Mit eigener UI
|
|
||||||
* <ConsentBanner
|
|
||||||
* render={({ isVisible, onAcceptAll, onRejectAll }) => (
|
|
||||||
* isVisible && (
|
|
||||||
* <div className="my-banner">
|
|
||||||
* <button onClick={onAcceptAll}>Accept</button>
|
|
||||||
* <button onClick={onRejectAll}>Reject</button>
|
|
||||||
* </div>
|
|
||||||
* )
|
|
||||||
* )}
|
|
||||||
* />
|
|
||||||
*
|
|
||||||
* // Mit Default-UI
|
|
||||||
* <ConsentBanner />
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export const ConsentBanner: FC<ConsentBannerProps> = ({ render, className }) => {
|
|
||||||
const {
|
|
||||||
consent,
|
|
||||||
isBannerVisible,
|
|
||||||
needsConsent,
|
|
||||||
acceptAll,
|
|
||||||
rejectAll,
|
|
||||||
saveSelection,
|
|
||||||
showSettings,
|
|
||||||
hideBanner,
|
|
||||||
} = useConsent();
|
|
||||||
|
|
||||||
const renderProps: ConsentBannerRenderProps = {
|
|
||||||
isVisible: isBannerVisible,
|
|
||||||
consent,
|
|
||||||
needsConsent,
|
|
||||||
onAcceptAll: acceptAll,
|
|
||||||
onRejectAll: rejectAll,
|
|
||||||
onSaveSelection: saveSelection,
|
|
||||||
onShowSettings: showSettings,
|
|
||||||
onClose: hideBanner,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Custom Render
|
|
||||||
if (render) {
|
|
||||||
return <>{render(renderProps)}</>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default UI
|
|
||||||
if (!isBannerVisible) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`bp-consent-banner ${className || ''}`}
|
|
||||||
role="dialog"
|
|
||||||
aria-modal="true"
|
|
||||||
aria-label="Cookie-Einstellungen"
|
|
||||||
>
|
|
||||||
<div className="bp-consent-banner-content">
|
|
||||||
<h2>Datenschutzeinstellungen</h2>
|
|
||||||
<p>
|
|
||||||
Wir nutzen Cookies und aehnliche Technologien, um Ihnen ein optimales
|
|
||||||
Nutzererlebnis zu bieten.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="bp-consent-banner-actions">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="bp-consent-btn bp-consent-btn-reject"
|
|
||||||
onClick={rejectAll}
|
|
||||||
>
|
|
||||||
Alle ablehnen
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="bp-consent-btn bp-consent-btn-settings"
|
|
||||||
onClick={showSettings}
|
|
||||||
>
|
|
||||||
Einstellungen
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="bp-consent-btn bp-consent-btn-accept"
|
|
||||||
onClick={acceptAll}
|
|
||||||
>
|
|
||||||
Alle akzeptieren
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Exports
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
|
export { useConsent, useConsentManager };
|
||||||
|
export { ConsentBanner, ConsentGate, ConsentPlaceholder };
|
||||||
export { ConsentContext };
|
export { ConsentContext };
|
||||||
export type { ConsentContextValue, ConsentBannerRenderProps };
|
export type { ConsentContextValue, ConsentBannerRenderProps };
|
||||||
|
|||||||
191
consent-sdk/src/vue/components.ts
Normal file
191
consent-sdk/src/vue/components.ts
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
/**
|
||||||
|
* Vue consent components: ConsentProvider, ConsentGate, ConsentPlaceholder,
|
||||||
|
* ConsentBanner.
|
||||||
|
*
|
||||||
|
* Phase 4: extracted from vue/index.ts.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { computed, defineComponent, h, type PropType } from 'vue';
|
||||||
|
import type { ConsentCategory, ConsentConfig } from '../types';
|
||||||
|
import { provideConsent, useConsent } from './composables';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ConsentProvider - Wrapper-Komponente.
|
||||||
|
*/
|
||||||
|
export const ConsentProvider = defineComponent({
|
||||||
|
name: 'ConsentProvider',
|
||||||
|
props: {
|
||||||
|
config: {
|
||||||
|
type: Object as PropType<ConsentConfig>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props, { slots }) {
|
||||||
|
provideConsent(props.config);
|
||||||
|
return () => slots.default?.();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ConsentGate - Zeigt Inhalt nur bei Consent.
|
||||||
|
*/
|
||||||
|
export const ConsentGate = defineComponent({
|
||||||
|
name: 'ConsentGate',
|
||||||
|
props: {
|
||||||
|
category: {
|
||||||
|
type: String as PropType<ConsentCategory>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props, { slots }) {
|
||||||
|
const { hasConsent, isLoading } = useConsent();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (isLoading.value) {
|
||||||
|
return slots.fallback?.() ?? null;
|
||||||
|
}
|
||||||
|
if (!hasConsent(props.category)) {
|
||||||
|
return slots.placeholder?.() ?? null;
|
||||||
|
}
|
||||||
|
return slots.default?.();
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ConsentPlaceholder - Placeholder fuer blockierten Inhalt.
|
||||||
|
*/
|
||||||
|
export const ConsentPlaceholder = defineComponent({
|
||||||
|
name: 'ConsentPlaceholder',
|
||||||
|
props: {
|
||||||
|
category: {
|
||||||
|
type: String as PropType<ConsentCategory>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
buttonText: {
|
||||||
|
type: String,
|
||||||
|
default: 'Cookie-Einstellungen öffnen',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const { showSettings } = useConsent();
|
||||||
|
|
||||||
|
const categoryNames: Record<ConsentCategory, string> = {
|
||||||
|
essential: 'Essentielle Cookies',
|
||||||
|
functional: 'Funktionale Cookies',
|
||||||
|
analytics: 'Statistik-Cookies',
|
||||||
|
marketing: 'Marketing-Cookies',
|
||||||
|
social: 'Social Media-Cookies',
|
||||||
|
};
|
||||||
|
|
||||||
|
const displayMessage = computed(() => {
|
||||||
|
return (
|
||||||
|
props.message ||
|
||||||
|
`Dieser Inhalt erfordert ${categoryNames[props.category]}.`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () =>
|
||||||
|
h('div', { class: 'bp-consent-placeholder' }, [
|
||||||
|
h('p', displayMessage.value),
|
||||||
|
h(
|
||||||
|
'button',
|
||||||
|
{
|
||||||
|
type: 'button',
|
||||||
|
onClick: showSettings,
|
||||||
|
},
|
||||||
|
props.buttonText
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ConsentBanner - Cookie-Banner Komponente (headless with default UI).
|
||||||
|
*/
|
||||||
|
export const ConsentBanner = defineComponent({
|
||||||
|
name: 'ConsentBanner',
|
||||||
|
setup(_, { slots }) {
|
||||||
|
const {
|
||||||
|
consent,
|
||||||
|
isBannerVisible,
|
||||||
|
needsConsent,
|
||||||
|
acceptAll,
|
||||||
|
rejectAll,
|
||||||
|
saveSelection,
|
||||||
|
showSettings,
|
||||||
|
hideBanner,
|
||||||
|
} = useConsent();
|
||||||
|
|
||||||
|
const slotProps = computed(() => ({
|
||||||
|
isVisible: isBannerVisible.value,
|
||||||
|
consent: consent.value,
|
||||||
|
needsConsent: needsConsent.value,
|
||||||
|
onAcceptAll: acceptAll,
|
||||||
|
onRejectAll: rejectAll,
|
||||||
|
onSaveSelection: saveSelection,
|
||||||
|
onShowSettings: showSettings,
|
||||||
|
onClose: hideBanner,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (slots.default) {
|
||||||
|
return slots.default(slotProps.value);
|
||||||
|
}
|
||||||
|
if (!isBannerVisible.value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return h(
|
||||||
|
'div',
|
||||||
|
{
|
||||||
|
class: 'bp-consent-banner',
|
||||||
|
role: 'dialog',
|
||||||
|
'aria-modal': 'true',
|
||||||
|
'aria-label': 'Cookie-Einstellungen',
|
||||||
|
},
|
||||||
|
[
|
||||||
|
h('div', { class: 'bp-consent-banner-content' }, [
|
||||||
|
h('h2', 'Datenschutzeinstellungen'),
|
||||||
|
h(
|
||||||
|
'p',
|
||||||
|
'Wir nutzen Cookies und ähnliche Technologien, um Ihnen ein optimales Nutzererlebnis zu bieten.'
|
||||||
|
),
|
||||||
|
h('div', { class: 'bp-consent-banner-actions' }, [
|
||||||
|
h(
|
||||||
|
'button',
|
||||||
|
{
|
||||||
|
type: 'button',
|
||||||
|
class: 'bp-consent-btn bp-consent-btn-reject',
|
||||||
|
onClick: rejectAll,
|
||||||
|
},
|
||||||
|
'Alle ablehnen'
|
||||||
|
),
|
||||||
|
h(
|
||||||
|
'button',
|
||||||
|
{
|
||||||
|
type: 'button',
|
||||||
|
class: 'bp-consent-btn bp-consent-btn-settings',
|
||||||
|
onClick: showSettings,
|
||||||
|
},
|
||||||
|
'Einstellungen'
|
||||||
|
),
|
||||||
|
h(
|
||||||
|
'button',
|
||||||
|
{
|
||||||
|
type: 'button',
|
||||||
|
class: 'bp-consent-btn bp-consent-btn-accept',
|
||||||
|
onClick: acceptAll,
|
||||||
|
},
|
||||||
|
'Alle akzeptieren'
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
135
consent-sdk/src/vue/composables.ts
Normal file
135
consent-sdk/src/vue/composables.ts
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
/**
|
||||||
|
* Vue composables: useConsent + provideConsent.
|
||||||
|
*
|
||||||
|
* Phase 4: extracted from vue/index.ts.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
computed,
|
||||||
|
inject,
|
||||||
|
onMounted,
|
||||||
|
onUnmounted,
|
||||||
|
provide,
|
||||||
|
readonly,
|
||||||
|
ref,
|
||||||
|
type Ref,
|
||||||
|
} from 'vue';
|
||||||
|
import { ConsentManager } from '../core/ConsentManager';
|
||||||
|
import type {
|
||||||
|
ConsentCategories,
|
||||||
|
ConsentCategory,
|
||||||
|
ConsentConfig,
|
||||||
|
ConsentState,
|
||||||
|
} from '../types';
|
||||||
|
import { CONSENT_KEY, type ConsentContext } from './context';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Haupt-Composable fuer Consent-Zugriff.
|
||||||
|
*/
|
||||||
|
export function useConsent(): ConsentContext {
|
||||||
|
const context = inject(CONSENT_KEY);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error(
|
||||||
|
'useConsent() must be used within a component that has called provideConsent() or is wrapped in ConsentProvider'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Consent-Provider einrichten (in App.vue aufrufen).
|
||||||
|
*/
|
||||||
|
export function provideConsent(config: ConsentConfig): ConsentContext {
|
||||||
|
const manager = ref<ConsentManager | null>(null);
|
||||||
|
const consent = ref<ConsentState | null>(null);
|
||||||
|
const isInitialized = ref(false);
|
||||||
|
const isLoading = ref(true);
|
||||||
|
const isBannerVisible = ref(false);
|
||||||
|
|
||||||
|
const needsConsent = computed(() => {
|
||||||
|
return manager.value?.needsConsent() ?? true;
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const consentManager = new ConsentManager(config);
|
||||||
|
manager.value = consentManager;
|
||||||
|
|
||||||
|
const unsubChange = consentManager.on('change', (newConsent) => {
|
||||||
|
consent.value = newConsent;
|
||||||
|
});
|
||||||
|
const unsubBannerShow = consentManager.on('banner_show', () => {
|
||||||
|
isBannerVisible.value = true;
|
||||||
|
});
|
||||||
|
const unsubBannerHide = consentManager.on('banner_hide', () => {
|
||||||
|
isBannerVisible.value = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await consentManager.init();
|
||||||
|
consent.value = consentManager.getConsent();
|
||||||
|
isInitialized.value = true;
|
||||||
|
isBannerVisible.value = consentManager.isBannerVisible();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to initialize ConsentManager:', error);
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
unsubChange();
|
||||||
|
unsubBannerShow();
|
||||||
|
unsubBannerHide();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasConsent = (category: ConsentCategory): boolean => {
|
||||||
|
return manager.value?.hasConsent(category) ?? category === 'essential';
|
||||||
|
};
|
||||||
|
|
||||||
|
const acceptAll = async (): Promise<void> => {
|
||||||
|
await manager.value?.acceptAll();
|
||||||
|
};
|
||||||
|
|
||||||
|
const rejectAll = async (): Promise<void> => {
|
||||||
|
await manager.value?.rejectAll();
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveSelection = async (
|
||||||
|
categories: Partial<ConsentCategories>
|
||||||
|
): Promise<void> => {
|
||||||
|
await manager.value?.setConsent(categories);
|
||||||
|
manager.value?.hideBanner();
|
||||||
|
};
|
||||||
|
|
||||||
|
const showBanner = (): void => {
|
||||||
|
manager.value?.showBanner();
|
||||||
|
};
|
||||||
|
|
||||||
|
const hideBanner = (): void => {
|
||||||
|
manager.value?.hideBanner();
|
||||||
|
};
|
||||||
|
|
||||||
|
const showSettings = (): void => {
|
||||||
|
manager.value?.showSettings();
|
||||||
|
};
|
||||||
|
|
||||||
|
const context: ConsentContext = {
|
||||||
|
manager: readonly(manager) as Ref<ConsentManager | null>,
|
||||||
|
consent: readonly(consent) as Ref<ConsentState | null>,
|
||||||
|
isInitialized: readonly(isInitialized),
|
||||||
|
isLoading: readonly(isLoading),
|
||||||
|
isBannerVisible: readonly(isBannerVisible),
|
||||||
|
needsConsent,
|
||||||
|
hasConsent,
|
||||||
|
acceptAll,
|
||||||
|
rejectAll,
|
||||||
|
saveSelection,
|
||||||
|
showBanner,
|
||||||
|
hideBanner,
|
||||||
|
showSettings,
|
||||||
|
};
|
||||||
|
|
||||||
|
provide(CONSENT_KEY, context);
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
31
consent-sdk/src/vue/context.ts
Normal file
31
consent-sdk/src/vue/context.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* Vue consent context — injection key + shape.
|
||||||
|
*
|
||||||
|
* Phase 4: extracted from vue/index.ts.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { InjectionKey, Ref } from 'vue';
|
||||||
|
import type { ConsentManager } from '../core/ConsentManager';
|
||||||
|
import type {
|
||||||
|
ConsentCategories,
|
||||||
|
ConsentCategory,
|
||||||
|
ConsentState,
|
||||||
|
} from '../types';
|
||||||
|
|
||||||
|
export interface ConsentContext {
|
||||||
|
manager: Ref<ConsentManager | null>;
|
||||||
|
consent: Ref<ConsentState | null>;
|
||||||
|
isInitialized: Ref<boolean>;
|
||||||
|
isLoading: Ref<boolean>;
|
||||||
|
isBannerVisible: Ref<boolean>;
|
||||||
|
needsConsent: Ref<boolean>;
|
||||||
|
hasConsent: (category: ConsentCategory) => boolean;
|
||||||
|
acceptAll: () => Promise<void>;
|
||||||
|
rejectAll: () => Promise<void>;
|
||||||
|
saveSelection: (categories: Partial<ConsentCategories>) => Promise<void>;
|
||||||
|
showBanner: () => void;
|
||||||
|
hideBanner: () => void;
|
||||||
|
showSettings: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CONSENT_KEY: InjectionKey<ConsentContext> = Symbol('consent');
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
/**
|
/**
|
||||||
* Vue 3 Integration fuer @breakpilot/consent-sdk
|
* Vue 3 Integration fuer @breakpilot/consent-sdk
|
||||||
*
|
*
|
||||||
|
* Phase 4 refactor: thin barrel. Composables, components, plugin, and the
|
||||||
|
* injection key live in sibling files.
|
||||||
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```vue
|
* ```vue
|
||||||
* <script setup>
|
* <script setup>
|
||||||
@@ -18,494 +21,12 @@
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
export { CONSENT_KEY, type ConsentContext } from './context';
|
||||||
ref,
|
export { useConsent, provideConsent } from './composables';
|
||||||
computed,
|
export {
|
||||||
readonly,
|
ConsentProvider,
|
||||||
inject,
|
ConsentGate,
|
||||||
provide,
|
ConsentPlaceholder,
|
||||||
onMounted,
|
ConsentBanner,
|
||||||
onUnmounted,
|
} from './components';
|
||||||
defineComponent,
|
export { ConsentPlugin } from './plugin';
|
||||||
h,
|
|
||||||
type Ref,
|
|
||||||
type InjectionKey,
|
|
||||||
type PropType,
|
|
||||||
} from 'vue';
|
|
||||||
import { ConsentManager } from '../core/ConsentManager';
|
|
||||||
import type {
|
|
||||||
ConsentConfig,
|
|
||||||
ConsentState,
|
|
||||||
ConsentCategory,
|
|
||||||
ConsentCategories,
|
|
||||||
} from '../types';
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Injection Key
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
const CONSENT_KEY: InjectionKey<ConsentContext> = Symbol('consent');
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Types
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
interface ConsentContext {
|
|
||||||
manager: Ref<ConsentManager | null>;
|
|
||||||
consent: Ref<ConsentState | null>;
|
|
||||||
isInitialized: Ref<boolean>;
|
|
||||||
isLoading: Ref<boolean>;
|
|
||||||
isBannerVisible: Ref<boolean>;
|
|
||||||
needsConsent: Ref<boolean>;
|
|
||||||
hasConsent: (category: ConsentCategory) => boolean;
|
|
||||||
acceptAll: () => Promise<void>;
|
|
||||||
rejectAll: () => Promise<void>;
|
|
||||||
saveSelection: (categories: Partial<ConsentCategories>) => Promise<void>;
|
|
||||||
showBanner: () => void;
|
|
||||||
hideBanner: () => void;
|
|
||||||
showSettings: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Composable: useConsent
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Haupt-Composable fuer Consent-Zugriff
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```vue
|
|
||||||
* <script setup>
|
|
||||||
* const { hasConsent, acceptAll } = useConsent();
|
|
||||||
*
|
|
||||||
* if (hasConsent('analytics')) {
|
|
||||||
* // Analytics laden
|
|
||||||
* }
|
|
||||||
* </script>
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export function useConsent(): ConsentContext {
|
|
||||||
const context = inject(CONSENT_KEY);
|
|
||||||
|
|
||||||
if (!context) {
|
|
||||||
throw new Error(
|
|
||||||
'useConsent() must be used within a component that has called provideConsent() or is wrapped in ConsentProvider'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Consent-Provider einrichten (in App.vue aufrufen)
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```vue
|
|
||||||
* <script setup>
|
|
||||||
* import { provideConsent } from '@breakpilot/consent-sdk/vue';
|
|
||||||
*
|
|
||||||
* provideConsent({
|
|
||||||
* apiEndpoint: 'https://consent.example.com/api/v1',
|
|
||||||
* siteId: 'site_abc123',
|
|
||||||
* });
|
|
||||||
* </script>
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export function provideConsent(config: ConsentConfig): ConsentContext {
|
|
||||||
const manager = ref<ConsentManager | null>(null);
|
|
||||||
const consent = ref<ConsentState | null>(null);
|
|
||||||
const isInitialized = ref(false);
|
|
||||||
const isLoading = ref(true);
|
|
||||||
const isBannerVisible = ref(false);
|
|
||||||
|
|
||||||
const needsConsent = computed(() => {
|
|
||||||
return manager.value?.needsConsent() ?? true;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialisierung
|
|
||||||
onMounted(async () => {
|
|
||||||
const consentManager = new ConsentManager(config);
|
|
||||||
manager.value = consentManager;
|
|
||||||
|
|
||||||
// Events abonnieren
|
|
||||||
const unsubChange = consentManager.on('change', (newConsent) => {
|
|
||||||
consent.value = newConsent;
|
|
||||||
});
|
|
||||||
|
|
||||||
const unsubBannerShow = consentManager.on('banner_show', () => {
|
|
||||||
isBannerVisible.value = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
const unsubBannerHide = consentManager.on('banner_hide', () => {
|
|
||||||
isBannerVisible.value = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
await consentManager.init();
|
|
||||||
consent.value = consentManager.getConsent();
|
|
||||||
isInitialized.value = true;
|
|
||||||
isBannerVisible.value = consentManager.isBannerVisible();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to initialize ConsentManager:', error);
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cleanup bei Unmount
|
|
||||||
onUnmounted(() => {
|
|
||||||
unsubChange();
|
|
||||||
unsubBannerShow();
|
|
||||||
unsubBannerHide();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Methoden
|
|
||||||
const hasConsent = (category: ConsentCategory): boolean => {
|
|
||||||
return manager.value?.hasConsent(category) ?? category === 'essential';
|
|
||||||
};
|
|
||||||
|
|
||||||
const acceptAll = async (): Promise<void> => {
|
|
||||||
await manager.value?.acceptAll();
|
|
||||||
};
|
|
||||||
|
|
||||||
const rejectAll = async (): Promise<void> => {
|
|
||||||
await manager.value?.rejectAll();
|
|
||||||
};
|
|
||||||
|
|
||||||
const saveSelection = async (categories: Partial<ConsentCategories>): Promise<void> => {
|
|
||||||
await manager.value?.setConsent(categories);
|
|
||||||
manager.value?.hideBanner();
|
|
||||||
};
|
|
||||||
|
|
||||||
const showBanner = (): void => {
|
|
||||||
manager.value?.showBanner();
|
|
||||||
};
|
|
||||||
|
|
||||||
const hideBanner = (): void => {
|
|
||||||
manager.value?.hideBanner();
|
|
||||||
};
|
|
||||||
|
|
||||||
const showSettings = (): void => {
|
|
||||||
manager.value?.showSettings();
|
|
||||||
};
|
|
||||||
|
|
||||||
const context: ConsentContext = {
|
|
||||||
manager: readonly(manager) as Ref<ConsentManager | null>,
|
|
||||||
consent: readonly(consent) as Ref<ConsentState | null>,
|
|
||||||
isInitialized: readonly(isInitialized),
|
|
||||||
isLoading: readonly(isLoading),
|
|
||||||
isBannerVisible: readonly(isBannerVisible),
|
|
||||||
needsConsent,
|
|
||||||
hasConsent,
|
|
||||||
acceptAll,
|
|
||||||
rejectAll,
|
|
||||||
saveSelection,
|
|
||||||
showBanner,
|
|
||||||
hideBanner,
|
|
||||||
showSettings,
|
|
||||||
};
|
|
||||||
|
|
||||||
provide(CONSENT_KEY, context);
|
|
||||||
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Components
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ConsentProvider - Wrapper-Komponente
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```vue
|
|
||||||
* <ConsentProvider :config="config">
|
|
||||||
* <App />
|
|
||||||
* </ConsentProvider>
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export const ConsentProvider = defineComponent({
|
|
||||||
name: 'ConsentProvider',
|
|
||||||
props: {
|
|
||||||
config: {
|
|
||||||
type: Object as PropType<ConsentConfig>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup(props, { slots }) {
|
|
||||||
provideConsent(props.config);
|
|
||||||
return () => slots.default?.();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ConsentGate - Zeigt Inhalt nur bei Consent
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```vue
|
|
||||||
* <ConsentGate category="analytics">
|
|
||||||
* <template #default>
|
|
||||||
* <AnalyticsComponent />
|
|
||||||
* </template>
|
|
||||||
* <template #placeholder>
|
|
||||||
* <p>Bitte akzeptieren Sie Statistik-Cookies.</p>
|
|
||||||
* </template>
|
|
||||||
* </ConsentGate>
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export const ConsentGate = defineComponent({
|
|
||||||
name: 'ConsentGate',
|
|
||||||
props: {
|
|
||||||
category: {
|
|
||||||
type: String as PropType<ConsentCategory>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup(props, { slots }) {
|
|
||||||
const { hasConsent, isLoading } = useConsent();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (isLoading.value) {
|
|
||||||
return slots.fallback?.() ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasConsent(props.category)) {
|
|
||||||
return slots.placeholder?.() ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return slots.default?.();
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ConsentPlaceholder - Placeholder fuer blockierten Inhalt
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```vue
|
|
||||||
* <ConsentPlaceholder category="marketing" />
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export const ConsentPlaceholder = defineComponent({
|
|
||||||
name: 'ConsentPlaceholder',
|
|
||||||
props: {
|
|
||||||
category: {
|
|
||||||
type: String as PropType<ConsentCategory>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
message: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
buttonText: {
|
|
||||||
type: String,
|
|
||||||
default: 'Cookie-Einstellungen öffnen',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
const { showSettings } = useConsent();
|
|
||||||
|
|
||||||
const categoryNames: Record<ConsentCategory, string> = {
|
|
||||||
essential: 'Essentielle Cookies',
|
|
||||||
functional: 'Funktionale Cookies',
|
|
||||||
analytics: 'Statistik-Cookies',
|
|
||||||
marketing: 'Marketing-Cookies',
|
|
||||||
social: 'Social Media-Cookies',
|
|
||||||
};
|
|
||||||
|
|
||||||
const displayMessage = computed(() => {
|
|
||||||
return props.message || `Dieser Inhalt erfordert ${categoryNames[props.category]}.`;
|
|
||||||
});
|
|
||||||
|
|
||||||
return () =>
|
|
||||||
h('div', { class: 'bp-consent-placeholder' }, [
|
|
||||||
h('p', displayMessage.value),
|
|
||||||
h(
|
|
||||||
'button',
|
|
||||||
{
|
|
||||||
type: 'button',
|
|
||||||
onClick: showSettings,
|
|
||||||
},
|
|
||||||
props.buttonText
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ConsentBanner - Cookie-Banner Komponente
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```vue
|
|
||||||
* <ConsentBanner>
|
|
||||||
* <template #default="{ isVisible, onAcceptAll, onRejectAll, onShowSettings }">
|
|
||||||
* <div v-if="isVisible" class="my-banner">
|
|
||||||
* <button @click="onAcceptAll">Accept</button>
|
|
||||||
* <button @click="onRejectAll">Reject</button>
|
|
||||||
* </div>
|
|
||||||
* </template>
|
|
||||||
* </ConsentBanner>
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export const ConsentBanner = defineComponent({
|
|
||||||
name: 'ConsentBanner',
|
|
||||||
setup(_, { slots }) {
|
|
||||||
const {
|
|
||||||
consent,
|
|
||||||
isBannerVisible,
|
|
||||||
needsConsent,
|
|
||||||
acceptAll,
|
|
||||||
rejectAll,
|
|
||||||
saveSelection,
|
|
||||||
showSettings,
|
|
||||||
hideBanner,
|
|
||||||
} = useConsent();
|
|
||||||
|
|
||||||
const slotProps = computed(() => ({
|
|
||||||
isVisible: isBannerVisible.value,
|
|
||||||
consent: consent.value,
|
|
||||||
needsConsent: needsConsent.value,
|
|
||||||
onAcceptAll: acceptAll,
|
|
||||||
onRejectAll: rejectAll,
|
|
||||||
onSaveSelection: saveSelection,
|
|
||||||
onShowSettings: showSettings,
|
|
||||||
onClose: hideBanner,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
// Custom Slot
|
|
||||||
if (slots.default) {
|
|
||||||
return slots.default(slotProps.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default UI
|
|
||||||
if (!isBannerVisible.value) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return h(
|
|
||||||
'div',
|
|
||||||
{
|
|
||||||
class: 'bp-consent-banner',
|
|
||||||
role: 'dialog',
|
|
||||||
'aria-modal': 'true',
|
|
||||||
'aria-label': 'Cookie-Einstellungen',
|
|
||||||
},
|
|
||||||
[
|
|
||||||
h('div', { class: 'bp-consent-banner-content' }, [
|
|
||||||
h('h2', 'Datenschutzeinstellungen'),
|
|
||||||
h(
|
|
||||||
'p',
|
|
||||||
'Wir nutzen Cookies und ähnliche Technologien, um Ihnen ein optimales Nutzererlebnis zu bieten.'
|
|
||||||
),
|
|
||||||
h('div', { class: 'bp-consent-banner-actions' }, [
|
|
||||||
h(
|
|
||||||
'button',
|
|
||||||
{
|
|
||||||
type: 'button',
|
|
||||||
class: 'bp-consent-btn bp-consent-btn-reject',
|
|
||||||
onClick: rejectAll,
|
|
||||||
},
|
|
||||||
'Alle ablehnen'
|
|
||||||
),
|
|
||||||
h(
|
|
||||||
'button',
|
|
||||||
{
|
|
||||||
type: 'button',
|
|
||||||
class: 'bp-consent-btn bp-consent-btn-settings',
|
|
||||||
onClick: showSettings,
|
|
||||||
},
|
|
||||||
'Einstellungen'
|
|
||||||
),
|
|
||||||
h(
|
|
||||||
'button',
|
|
||||||
{
|
|
||||||
type: 'button',
|
|
||||||
class: 'bp-consent-btn bp-consent-btn-accept',
|
|
||||||
onClick: acceptAll,
|
|
||||||
},
|
|
||||||
'Alle akzeptieren'
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
]),
|
|
||||||
]
|
|
||||||
);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Plugin
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Vue Plugin fuer globale Installation
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* import { createApp } from 'vue';
|
|
||||||
* import { ConsentPlugin } from '@breakpilot/consent-sdk/vue';
|
|
||||||
*
|
|
||||||
* const app = createApp(App);
|
|
||||||
* app.use(ConsentPlugin, {
|
|
||||||
* apiEndpoint: 'https://consent.example.com/api/v1',
|
|
||||||
* siteId: 'site_abc123',
|
|
||||||
* });
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export const ConsentPlugin = {
|
|
||||||
install(app: { provide: (key: symbol | string, value: unknown) => void }, config: ConsentConfig) {
|
|
||||||
const manager = new ConsentManager(config);
|
|
||||||
const consent = ref<ConsentState | null>(null);
|
|
||||||
const isInitialized = ref(false);
|
|
||||||
const isLoading = ref(true);
|
|
||||||
const isBannerVisible = ref(false);
|
|
||||||
|
|
||||||
// Initialisieren
|
|
||||||
manager.init().then(() => {
|
|
||||||
consent.value = manager.getConsent();
|
|
||||||
isInitialized.value = true;
|
|
||||||
isLoading.value = false;
|
|
||||||
isBannerVisible.value = manager.isBannerVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Events
|
|
||||||
manager.on('change', (newConsent) => {
|
|
||||||
consent.value = newConsent;
|
|
||||||
});
|
|
||||||
manager.on('banner_show', () => {
|
|
||||||
isBannerVisible.value = true;
|
|
||||||
});
|
|
||||||
manager.on('banner_hide', () => {
|
|
||||||
isBannerVisible.value = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
const context: ConsentContext = {
|
|
||||||
manager: ref(manager) as Ref<ConsentManager | null>,
|
|
||||||
consent: consent as Ref<ConsentState | null>,
|
|
||||||
isInitialized,
|
|
||||||
isLoading,
|
|
||||||
isBannerVisible,
|
|
||||||
needsConsent: computed(() => manager.needsConsent()),
|
|
||||||
hasConsent: (category: ConsentCategory) => manager.hasConsent(category),
|
|
||||||
acceptAll: () => manager.acceptAll(),
|
|
||||||
rejectAll: () => manager.rejectAll(),
|
|
||||||
saveSelection: async (categories: Partial<ConsentCategories>) => {
|
|
||||||
await manager.setConsent(categories);
|
|
||||||
manager.hideBanner();
|
|
||||||
},
|
|
||||||
showBanner: () => manager.showBanner(),
|
|
||||||
hideBanner: () => manager.hideBanner(),
|
|
||||||
showSettings: () => manager.showSettings(),
|
|
||||||
};
|
|
||||||
|
|
||||||
app.provide(CONSENT_KEY, context);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Exports
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
export { CONSENT_KEY };
|
|
||||||
export type { ConsentContext };
|
|
||||||
|
|||||||
74
consent-sdk/src/vue/plugin.ts
Normal file
74
consent-sdk/src/vue/plugin.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
/**
|
||||||
|
* Vue plugin for global installation.
|
||||||
|
*
|
||||||
|
* Phase 4: extracted from vue/index.ts.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { computed, ref, type Ref } from 'vue';
|
||||||
|
import { ConsentManager } from '../core/ConsentManager';
|
||||||
|
import type {
|
||||||
|
ConsentCategories,
|
||||||
|
ConsentCategory,
|
||||||
|
ConsentConfig,
|
||||||
|
ConsentState,
|
||||||
|
} from '../types';
|
||||||
|
import { CONSENT_KEY, type ConsentContext } from './context';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vue Plugin fuer globale Installation.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* app.use(ConsentPlugin, { apiEndpoint: '...', siteId: '...' });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const ConsentPlugin = {
|
||||||
|
install(
|
||||||
|
app: { provide: (key: symbol | string, value: unknown) => void },
|
||||||
|
config: ConsentConfig
|
||||||
|
) {
|
||||||
|
const manager = new ConsentManager(config);
|
||||||
|
const consent = ref<ConsentState | null>(null);
|
||||||
|
const isInitialized = ref(false);
|
||||||
|
const isLoading = ref(true);
|
||||||
|
const isBannerVisible = ref(false);
|
||||||
|
|
||||||
|
manager.init().then(() => {
|
||||||
|
consent.value = manager.getConsent();
|
||||||
|
isInitialized.value = true;
|
||||||
|
isLoading.value = false;
|
||||||
|
isBannerVisible.value = manager.isBannerVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
manager.on('change', (newConsent) => {
|
||||||
|
consent.value = newConsent;
|
||||||
|
});
|
||||||
|
manager.on('banner_show', () => {
|
||||||
|
isBannerVisible.value = true;
|
||||||
|
});
|
||||||
|
manager.on('banner_hide', () => {
|
||||||
|
isBannerVisible.value = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const context: ConsentContext = {
|
||||||
|
manager: ref(manager) as Ref<ConsentManager | null>,
|
||||||
|
consent: consent as Ref<ConsentState | null>,
|
||||||
|
isInitialized,
|
||||||
|
isLoading,
|
||||||
|
isBannerVisible,
|
||||||
|
needsConsent: computed(() => manager.needsConsent()),
|
||||||
|
hasConsent: (category: ConsentCategory) => manager.hasConsent(category),
|
||||||
|
acceptAll: () => manager.acceptAll(),
|
||||||
|
rejectAll: () => manager.rejectAll(),
|
||||||
|
saveSelection: async (categories: Partial<ConsentCategories>) => {
|
||||||
|
await manager.setConsent(categories);
|
||||||
|
manager.hideBanner();
|
||||||
|
},
|
||||||
|
showBanner: () => manager.showBanner(),
|
||||||
|
hideBanner: () => manager.hideBanner(),
|
||||||
|
showSettings: () => manager.showSettings(),
|
||||||
|
};
|
||||||
|
|
||||||
|
app.provide(CONSENT_KEY, context);
|
||||||
|
},
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user