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
|
||||
*
|
||||
* Phase 4 refactor: thin barrel. Interface, service, module definition, and
|
||||
* template snippets live in sibling files.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // app.module.ts
|
||||
@@ -16,494 +19,27 @@
|
||||
* })
|
||||
* 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`).
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// NOTE: Angular SDK Structure
|
||||
// =============================================================================
|
||||
//
|
||||
// Angular hat ein komplexeres Build-System (ngc, ng-packagr).
|
||||
// Diese Datei definiert die Schnittstelle - fuer Production muss ein
|
||||
// separates Angular Library Package erstellt werden:
|
||||
//
|
||||
// ng generate library @breakpilot/consent-sdk-angular
|
||||
//
|
||||
// Die folgende Implementation ist fuer direkten Import vorgesehen.
|
||||
// =============================================================================
|
||||
export type { IConsentService } from './interface';
|
||||
export { ConsentServiceBase } from './service';
|
||||
export {
|
||||
CONSENT_CONFIG,
|
||||
CONSENT_SERVICE,
|
||||
ConsentModuleDefinition,
|
||||
consentServiceFactory,
|
||||
type ConsentModuleConfig,
|
||||
} from './module';
|
||||
export { CONSENT_BANNER_TEMPLATE, CONSENT_GATE_USAGE } from './templates';
|
||||
|
||||
import { ConsentManager } from '../core/ConsentManager';
|
||||
import type {
|
||||
export type {
|
||||
ConsentCategories,
|
||||
ConsentCategory,
|
||||
ConsentConfig,
|
||||
ConsentState,
|
||||
ConsentCategory,
|
||||
ConsentCategories,
|
||||
} 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 { generateFingerprint } from '../utils/fingerprint';
|
||||
import { SDK_VERSION } from '../version';
|
||||
|
||||
/**
|
||||
* Default-Konfiguration
|
||||
*/
|
||||
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 (nur Essential aktiv)
|
||||
*/
|
||||
const DEFAULT_CONSENT: ConsentCategories = {
|
||||
essential: true,
|
||||
functional: false,
|
||||
analytics: false,
|
||||
marketing: false,
|
||||
social: false,
|
||||
};
|
||||
import {
|
||||
DEFAULT_CONSENT,
|
||||
mergeConsentConfig,
|
||||
} from './consent-manager-config';
|
||||
import { updateGoogleConsentMode as applyGoogleConsent } from './consent-manager-google';
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
return {
|
||||
...DEFAULT_CONFIG,
|
||||
...config,
|
||||
ui: { ...DEFAULT_CONFIG.ui, ...config.ui },
|
||||
consent: { ...DEFAULT_CONFIG.consent, ...config.consent },
|
||||
} as ConsentConfig;
|
||||
return mergeConsentConfig(config);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -434,31 +395,12 @@ export class ConsentManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Google Consent Mode v2 aktualisieren
|
||||
* Google Consent Mode v2 aktualisieren — delegates to the extracted helper.
|
||||
*/
|
||||
private updateGoogleConsentMode(): void {
|
||||
if (typeof window === 'undefined' || !this.currentConsent) {
|
||||
return;
|
||||
if (applyGoogleConsent(this.currentConsent)) {
|
||||
this.log('Google Consent Mode updated');
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
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 {
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
useCallback,
|
||||
useMemo,
|
||||
type ReactNode,
|
||||
type FC,
|
||||
type ReactNode,
|
||||
} from 'react';
|
||||
import { ConsentManager } from '../core/ConsentManager';
|
||||
import type {
|
||||
ConsentCategories,
|
||||
ConsentCategory,
|
||||
ConsentConfig,
|
||||
ConsentState,
|
||||
ConsentCategory,
|
||||
ConsentCategories,
|
||||
} from '../types';
|
||||
|
||||
// =============================================================================
|
||||
// Context
|
||||
// =============================================================================
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const ConsentContext = createContext<ConsentContextValue | null>(null);
|
||||
import { ConsentContext, type ConsentContextValue } from './context';
|
||||
import { useConsent, useConsentManager } from './hooks';
|
||||
import {
|
||||
ConsentBanner,
|
||||
ConsentGate,
|
||||
ConsentPlaceholder,
|
||||
type ConsentBannerRenderProps,
|
||||
} from './components';
|
||||
|
||||
// =============================================================================
|
||||
// Provider
|
||||
@@ -88,13 +51,12 @@ const ConsentContext = createContext<ConsentContextValue | null>(null);
|
||||
interface ConsentProviderProps {
|
||||
/** SDK-Konfiguration */
|
||||
config: ConsentConfig;
|
||||
|
||||
/** Kinder-Komponenten */
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* ConsentProvider - Stellt Consent-Kontext bereit
|
||||
* ConsentProvider - Stellt Consent-Kontext bereit.
|
||||
*/
|
||||
export const ConsentProvider: FC<ConsentProviderProps> = ({
|
||||
config,
|
||||
@@ -228,284 +190,10 @@ export const ConsentProvider: FC<ConsentProviderProps> = ({
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Hooks
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* 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
|
||||
// Re-exports for the public @breakpilot/consent-sdk/react entrypoint
|
||||
// =============================================================================
|
||||
|
||||
export { useConsent, useConsentManager };
|
||||
export { ConsentBanner, ConsentGate, ConsentPlaceholder };
|
||||
export { ConsentContext };
|
||||
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
|
||||
*
|
||||
* Phase 4 refactor: thin barrel. Composables, components, plugin, and the
|
||||
* injection key live in sibling files.
|
||||
*
|
||||
* @example
|
||||
* ```vue
|
||||
* <script setup>
|
||||
@@ -18,494 +21,12 @@
|
||||
* ```
|
||||
*/
|
||||
|
||||
import {
|
||||
ref,
|
||||
computed,
|
||||
readonly,
|
||||
inject,
|
||||
provide,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
defineComponent,
|
||||
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 };
|
||||
export { CONSENT_KEY, type ConsentContext } from './context';
|
||||
export { useConsent, provideConsent } from './composables';
|
||||
export {
|
||||
ConsentProvider,
|
||||
ConsentGate,
|
||||
ConsentPlaceholder,
|
||||
ConsentBanner,
|
||||
} from './components';
|
||||
export { ConsentPlugin } from './plugin';
|
||||
|
||||
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