Files
fotospiel-app/resources/js/contexts/consent.tsx
Codex Agent a949c8d3af - Wired the checkout wizard for Google “comfort login”: added Socialite controller + dependency, new Google env
hooks in config/services.php/.env.example, and updated wizard steps/controllers to store session payloads,
attach packages, and surface localized success/error states.
- Retooled payment handling for both Stripe and PayPal, adding richer status management in CheckoutController/
PayPalController, fallback flows in the wizard’s PaymentStep.tsx, and fresh feature tests for intent
creation, webhooks, and the wizard CTA.
- Introduced a consent-aware Matomo analytics stack: new consent context, cookie-banner UI, useAnalytics/
useCtaExperiment hooks, and MatomoTracker component, then instrumented marketing pages (Home, Packages,
Checkout) with localized copy and experiment tracking.
- Polished package presentation across marketing UIs by centralizing formatting in PresentsPackages, surfacing
localized description tables/placeholders, tuning badges/layouts, and syncing guest/marketing translations.
- Expanded docs & reference material (docs/prp/*, TODOs, public gallery overview) and added a Playwright smoke
test for the hero CTA while reconciling outstanding checklist items.
2025-10-19 11:41:03 +02:00

186 lines
4.4 KiB
TypeScript

import React, {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from 'react';
const CONSENT_STORAGE_KEY = 'fotospiel.consent';
const CONSENT_VERSION = '2025-10-17-1';
export type ConsentCategory = 'functional' | 'analytics';
export type ConsentPreferences = Record<ConsentCategory, boolean>;
interface StoredConsent {
version: string;
preferences: ConsentPreferences;
decisionMade: boolean;
updatedAt: string | null;
}
const defaultPreferences: ConsentPreferences = {
functional: true,
analytics: false,
};
const defaultState: StoredConsent = {
version: CONSENT_VERSION,
preferences: { ...defaultPreferences },
decisionMade: false,
updatedAt: null,
};
interface ConsentContextValue {
preferences: ConsentPreferences;
decisionMade: boolean;
showBanner: boolean;
acceptAll: () => void;
rejectAll: () => void;
savePreferences: (preferences: Partial<ConsentPreferences>) => void;
hasConsent: (category: ConsentCategory) => boolean;
openPreferences: () => void;
closePreferences: () => void;
isPreferencesOpen: boolean;
}
const ConsentContext = createContext<ConsentContextValue | undefined>(undefined);
function normalizeState(state: StoredConsent | null): StoredConsent {
if (!state || state.version !== CONSENT_VERSION) {
return { ...defaultState };
}
return {
version: CONSENT_VERSION,
decisionMade: state.decisionMade ?? false,
updatedAt: state.updatedAt ?? null,
preferences: {
...defaultPreferences,
...state.preferences,
functional: true,
},
};
}
function getInitialState(): StoredConsent {
if (typeof window === 'undefined') {
return { ...defaultState };
}
try {
const raw = window.localStorage.getItem(CONSENT_STORAGE_KEY);
if (!raw) {
return { ...defaultState };
}
const parsed = JSON.parse(raw) as StoredConsent;
return normalizeState(parsed);
} catch {
return { ...defaultState };
}
}
export const ConsentProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [state, setState] = useState<StoredConsent>(() => getInitialState());
const [isPreferencesOpen, setPreferencesOpen] = useState(false);
useEffect(() => {
if (typeof window === 'undefined') {
return;
}
window.localStorage.setItem(CONSENT_STORAGE_KEY, JSON.stringify(state));
}, [state]);
const acceptAll = useCallback(() => {
setState({
version: CONSENT_VERSION,
preferences: { functional: true, analytics: true },
decisionMade: true,
updatedAt: new Date().toISOString(),
});
setPreferencesOpen(false);
}, []);
const rejectAll = useCallback(() => {
setState({
version: CONSENT_VERSION,
preferences: { functional: true, analytics: false },
decisionMade: true,
updatedAt: new Date().toISOString(),
});
setPreferencesOpen(false);
}, []);
const savePreferences = useCallback((preferences: Partial<ConsentPreferences>) => {
setState((prev) => ({
version: CONSENT_VERSION,
preferences: {
...defaultPreferences,
...prev.preferences,
...preferences,
functional: true,
},
decisionMade: true,
updatedAt: new Date().toISOString(),
}));
setPreferencesOpen(false);
}, []);
const hasConsent = useCallback(
(category: ConsentCategory) => {
return Boolean(state.preferences?.[category]);
},
[state.preferences],
);
const openPreferences = useCallback(() => {
setPreferencesOpen(true);
}, []);
const closePreferences = useCallback(() => {
setPreferencesOpen(false);
}, []);
const value = useMemo<ConsentContextValue>(
() => ({
preferences: state.preferences,
decisionMade: state.decisionMade,
showBanner: !state.decisionMade,
acceptAll,
rejectAll,
savePreferences,
hasConsent,
openPreferences,
closePreferences,
isPreferencesOpen,
}),
[
state.preferences,
state.decisionMade,
acceptAll,
rejectAll,
savePreferences,
hasConsent,
openPreferences,
closePreferences,
isPreferencesOpen,
],
);
return <ConsentContext.Provider value={value}>{children}</ConsentContext.Provider>;
};
export const useConsent = (): ConsentContextValue => {
const context = useContext(ConsentContext);
if (!context) {
throw new Error('useConsent must be used within a ConsentProvider');
}
return context;
};