Initial commit: breakpilot-compliance - Compliance SDK Platform
Services: Admin-Compliance, Backend-Compliance, AI-Compliance-SDK, Consent-SDK, Developer-Portal, PCA-Platform, DSMS Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
511
consent-sdk/src/react/index.tsx
Normal file
511
consent-sdk/src/react/index.tsx
Normal file
@@ -0,0 +1,511 @@
|
||||
/**
|
||||
* React Integration fuer @breakpilot/consent-sdk
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* import { ConsentProvider, useConsent, ConsentBanner } from '@breakpilot/consent-sdk/react';
|
||||
*
|
||||
* function App() {
|
||||
* return (
|
||||
* <ConsentProvider config={config}>
|
||||
* <ConsentBanner />
|
||||
* <MainContent />
|
||||
* </ConsentProvider>
|
||||
* );
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
useCallback,
|
||||
useMemo,
|
||||
type ReactNode,
|
||||
type FC,
|
||||
} from 'react';
|
||||
import { ConsentManager } from '../core/ConsentManager';
|
||||
import type {
|
||||
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);
|
||||
|
||||
// =============================================================================
|
||||
// Provider
|
||||
// =============================================================================
|
||||
|
||||
interface ConsentProviderProps {
|
||||
/** SDK-Konfiguration */
|
||||
config: ConsentConfig;
|
||||
|
||||
/** Kinder-Komponenten */
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* ConsentProvider - Stellt Consent-Kontext bereit
|
||||
*/
|
||||
export const ConsentProvider: FC<ConsentProviderProps> = ({
|
||||
config,
|
||||
children,
|
||||
}) => {
|
||||
const [manager, setManager] = useState<ConsentManager | null>(null);
|
||||
const [consent, setConsent] = useState<ConsentState | null>(null);
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isBannerVisible, setIsBannerVisible] = useState(false);
|
||||
|
||||
// Manager erstellen und initialisieren
|
||||
useEffect(() => {
|
||||
const consentManager = new ConsentManager(config);
|
||||
setManager(consentManager);
|
||||
|
||||
// Events abonnieren
|
||||
const unsubChange = consentManager.on('change', (newConsent) => {
|
||||
setConsent(newConsent);
|
||||
});
|
||||
|
||||
const unsubBannerShow = consentManager.on('banner_show', () => {
|
||||
setIsBannerVisible(true);
|
||||
});
|
||||
|
||||
const unsubBannerHide = consentManager.on('banner_hide', () => {
|
||||
setIsBannerVisible(false);
|
||||
});
|
||||
|
||||
// Initialisieren
|
||||
consentManager
|
||||
.init()
|
||||
.then(() => {
|
||||
setConsent(consentManager.getConsent());
|
||||
setIsInitialized(true);
|
||||
setIsLoading(false);
|
||||
setIsBannerVisible(consentManager.isBannerVisible());
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to initialize ConsentManager:', error);
|
||||
setIsLoading(false);
|
||||
});
|
||||
|
||||
// Cleanup
|
||||
return () => {
|
||||
unsubChange();
|
||||
unsubBannerShow();
|
||||
unsubBannerHide();
|
||||
};
|
||||
}, [config]);
|
||||
|
||||
// Callback-Funktionen
|
||||
const hasConsent = useCallback(
|
||||
(category: ConsentCategory): boolean => {
|
||||
return manager?.hasConsent(category) ?? category === 'essential';
|
||||
},
|
||||
[manager]
|
||||
);
|
||||
|
||||
const acceptAll = useCallback(async () => {
|
||||
await manager?.acceptAll();
|
||||
}, [manager]);
|
||||
|
||||
const rejectAll = useCallback(async () => {
|
||||
await manager?.rejectAll();
|
||||
}, [manager]);
|
||||
|
||||
const saveSelection = useCallback(
|
||||
async (categories: Partial<ConsentCategories>) => {
|
||||
await manager?.setConsent(categories);
|
||||
manager?.hideBanner();
|
||||
},
|
||||
[manager]
|
||||
);
|
||||
|
||||
const showBanner = useCallback(() => {
|
||||
manager?.showBanner();
|
||||
}, [manager]);
|
||||
|
||||
const hideBanner = useCallback(() => {
|
||||
manager?.hideBanner();
|
||||
}, [manager]);
|
||||
|
||||
const showSettings = useCallback(() => {
|
||||
manager?.showSettings();
|
||||
}, [manager]);
|
||||
|
||||
const needsConsent = useMemo(() => {
|
||||
return manager?.needsConsent() ?? true;
|
||||
}, [manager, consent]);
|
||||
|
||||
// Context-Wert
|
||||
const contextValue = useMemo<ConsentContextValue>(
|
||||
() => ({
|
||||
manager,
|
||||
consent,
|
||||
isInitialized,
|
||||
isLoading,
|
||||
isBannerVisible,
|
||||
needsConsent,
|
||||
hasConsent,
|
||||
acceptAll,
|
||||
rejectAll,
|
||||
saveSelection,
|
||||
showBanner,
|
||||
hideBanner,
|
||||
showSettings,
|
||||
}),
|
||||
[
|
||||
manager,
|
||||
consent,
|
||||
isInitialized,
|
||||
isLoading,
|
||||
isBannerVisible,
|
||||
needsConsent,
|
||||
hasConsent,
|
||||
acceptAll,
|
||||
rejectAll,
|
||||
saveSelection,
|
||||
showBanner,
|
||||
hideBanner,
|
||||
showSettings,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<ConsentContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</ConsentContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// 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
|
||||
// =============================================================================
|
||||
|
||||
export { ConsentContext };
|
||||
export type { ConsentContextValue, ConsentBannerRenderProps };
|
||||
Reference in New Issue
Block a user