Add guest analytics consent nudge
This commit is contained in:
236
resources/js/guest/components/GuestAnalyticsNudge.tsx
Normal file
236
resources/js/guest/components/GuestAnalyticsNudge.tsx
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { useConsent } from '@/contexts/consent';
|
||||||
|
import { useTranslation } from '../i18n/useTranslation';
|
||||||
|
import { isUploadPath, shouldShowAnalyticsNudge } from '../lib/analyticsConsent';
|
||||||
|
|
||||||
|
const PROMPT_STORAGE_KEY = 'fotospiel.guest.analyticsPrompt';
|
||||||
|
const SNOOZE_MS = 60 * 60 * 1000;
|
||||||
|
const ACTIVE_IDLE_LIMIT_MS = 20_000;
|
||||||
|
|
||||||
|
type PromptStorage = {
|
||||||
|
snoozedUntil?: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
function readSnoozedUntil(): number | null {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const raw = window.localStorage.getItem(PROMPT_STORAGE_KEY);
|
||||||
|
if (!raw) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const parsed = JSON.parse(raw) as PromptStorage;
|
||||||
|
return typeof parsed.snoozedUntil === 'number' ? parsed.snoozedUntil : null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeSnoozedUntil(value: number | null) {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload: PromptStorage = { snoozedUntil: value };
|
||||||
|
window.localStorage.setItem(PROMPT_STORAGE_KEY, JSON.stringify(payload));
|
||||||
|
} catch {
|
||||||
|
// ignore storage failures
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomInt(min: number, max: number): number {
|
||||||
|
const low = Math.ceil(min);
|
||||||
|
const high = Math.floor(max);
|
||||||
|
return Math.floor(Math.random() * (high - low + 1)) + low;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function GuestAnalyticsNudge({
|
||||||
|
enabled,
|
||||||
|
pathname,
|
||||||
|
}: {
|
||||||
|
enabled: boolean;
|
||||||
|
pathname: string;
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { decisionMade, preferences, savePreferences } = useConsent();
|
||||||
|
const analyticsConsent = Boolean(preferences?.analytics);
|
||||||
|
const [thresholdSeconds] = React.useState(() => randomInt(60, 120));
|
||||||
|
const [thresholdRoutes] = React.useState(() => randomInt(2, 3));
|
||||||
|
const [activeSeconds, setActiveSeconds] = React.useState(0);
|
||||||
|
const [routeCount, setRouteCount] = React.useState(0);
|
||||||
|
const [isOpen, setIsOpen] = React.useState(false);
|
||||||
|
const [snoozedUntil, setSnoozedUntil] = React.useState<number | null>(() => readSnoozedUntil());
|
||||||
|
const lastPathRef = React.useRef(pathname);
|
||||||
|
const lastActivityAtRef = React.useRef(Date.now());
|
||||||
|
const visibleRef = React.useRef(typeof document === 'undefined' ? true : document.visibilityState === 'visible');
|
||||||
|
|
||||||
|
const isUpload = isUploadPath(pathname);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const previousPath = lastPathRef.current;
|
||||||
|
const currentPath = pathname;
|
||||||
|
lastPathRef.current = currentPath;
|
||||||
|
|
||||||
|
if (previousPath === currentPath) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isUploadPath(previousPath) || isUploadPath(currentPath)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setRouteCount((count) => count + 1);
|
||||||
|
}, [pathname]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleActivity = () => {
|
||||||
|
lastActivityAtRef.current = Date.now();
|
||||||
|
};
|
||||||
|
|
||||||
|
const events: Array<keyof WindowEventMap> = [
|
||||||
|
'pointerdown',
|
||||||
|
'pointermove',
|
||||||
|
'keydown',
|
||||||
|
'scroll',
|
||||||
|
'touchstart',
|
||||||
|
];
|
||||||
|
|
||||||
|
events.forEach((event) => window.addEventListener(event, handleActivity, { passive: true }));
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
events.forEach((event) => window.removeEventListener(event, handleActivity));
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (typeof document === 'undefined') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleVisibility = () => {
|
||||||
|
visibleRef.current = document.visibilityState === 'visible';
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('visibilitychange', handleVisibility);
|
||||||
|
|
||||||
|
return () => document.removeEventListener('visibilitychange', handleVisibility);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const interval = window.setInterval(() => {
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
if (!visibleRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isUploadPath(lastPathRef.current)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (now - lastActivityAtRef.current > ACTIVE_IDLE_LIMIT_MS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setActiveSeconds((seconds) => seconds + 1);
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
return () => window.clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!enabled || analyticsConsent || decisionMade) {
|
||||||
|
setIsOpen(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldOpen = shouldShowAnalyticsNudge({
|
||||||
|
decisionMade,
|
||||||
|
analyticsConsent,
|
||||||
|
snoozedUntil,
|
||||||
|
now: Date.now(),
|
||||||
|
activeSeconds,
|
||||||
|
routeCount,
|
||||||
|
thresholdSeconds,
|
||||||
|
thresholdRoutes,
|
||||||
|
isUpload,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (shouldOpen) {
|
||||||
|
setIsOpen(true);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
enabled,
|
||||||
|
analyticsConsent,
|
||||||
|
decisionMade,
|
||||||
|
snoozedUntil,
|
||||||
|
activeSeconds,
|
||||||
|
routeCount,
|
||||||
|
thresholdSeconds,
|
||||||
|
thresholdRoutes,
|
||||||
|
isUpload,
|
||||||
|
]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (isUpload) {
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
}, [isUpload]);
|
||||||
|
|
||||||
|
if (!enabled || decisionMade || analyticsConsent || !isOpen || isUpload) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSnooze = () => {
|
||||||
|
const until = Date.now() + SNOOZE_MS;
|
||||||
|
setSnoozedUntil(until);
|
||||||
|
writeSnoozedUntil(until);
|
||||||
|
setIsOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAllow = () => {
|
||||||
|
savePreferences({ analytics: true });
|
||||||
|
writeSnoozedUntil(null);
|
||||||
|
setIsOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="pointer-events-none fixed inset-x-0 z-40 px-4"
|
||||||
|
style={{ bottom: 'calc(env(safe-area-inset-bottom, 0px) + 96px)' }}
|
||||||
|
>
|
||||||
|
<div className="pointer-events-auto mx-auto max-w-lg rounded-2xl border border-slate-200/80 bg-white/95 p-4 shadow-xl backdrop-blur dark:border-slate-700/60 dark:bg-slate-900/95">
|
||||||
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-sm font-semibold text-foreground">
|
||||||
|
{t('consent.analytics.title')}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{t('consent.analytics.body')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
<Button type="button" size="sm" variant="ghost" onClick={handleSnooze}>
|
||||||
|
{t('consent.analytics.later')}
|
||||||
|
</Button>
|
||||||
|
<Button type="button" size="sm" onClick={handleAllow}>
|
||||||
|
{t('consent.analytics.allow')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@ import type { LocaleCode } from '../i18n/messages';
|
|||||||
import { useHapticsPreference } from '../hooks/useHapticsPreference';
|
import { useHapticsPreference } from '../hooks/useHapticsPreference';
|
||||||
import { triggerHaptic } from '../lib/haptics';
|
import { triggerHaptic } from '../lib/haptics';
|
||||||
import { getHelpSlugForPathname } from '../lib/helpRouting';
|
import { getHelpSlugForPathname } from '../lib/helpRouting';
|
||||||
|
import { useConsent } from '@/contexts/consent';
|
||||||
|
|
||||||
const legalPages = [
|
const legalPages = [
|
||||||
{ slug: 'impressum', translationKey: 'settings.legal.section.impressum' },
|
{ slug: 'impressum', translationKey: 'settings.legal.section.impressum' },
|
||||||
@@ -270,6 +271,8 @@ function HomeView({
|
|||||||
}: HomeViewProps) {
|
}: HomeViewProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { enabled: hapticsEnabled, setEnabled: setHapticsEnabled, supported: hapticsSupported } = useHapticsPreference();
|
const { enabled: hapticsEnabled, setEnabled: setHapticsEnabled, supported: hapticsSupported } = useHapticsPreference();
|
||||||
|
const { preferences, savePreferences } = useConsent();
|
||||||
|
const matomoEnabled = typeof window !== 'undefined' && Boolean((window as any).__MATOMO_GUEST__?.enabled);
|
||||||
const legalLinks = React.useMemo(
|
const legalLinks = React.useMemo(
|
||||||
() =>
|
() =>
|
||||||
legalPages.map((page) => ({
|
legalPages.map((page) => ({
|
||||||
@@ -389,6 +392,26 @@ function HomeView({
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{matomoEnabled ? (
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<CardTitle>{t('settings.analytics.title')}</CardTitle>
|
||||||
|
<CardDescription>{t('settings.analytics.description')}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between gap-4">
|
||||||
|
<span className="text-sm font-medium">{t('settings.analytics.label')}</span>
|
||||||
|
<Switch
|
||||||
|
checked={Boolean(preferences?.analytics)}
|
||||||
|
onCheckedChange={(checked) => savePreferences({ analytics: checked })}
|
||||||
|
aria-label={t('settings.analytics.label')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground">{t('settings.analytics.note')}</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-3">
|
||||||
<CardTitle>
|
<CardTitle>
|
||||||
|
|||||||
@@ -28,6 +28,14 @@ export const messages: Record<LocaleCode, NestedMessages> = {
|
|||||||
loading: 'Lädt...',
|
loading: 'Lädt...',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
consent: {
|
||||||
|
analytics: {
|
||||||
|
title: 'Hilf uns, die App zu verbessern',
|
||||||
|
body: 'Erlaube anonyme Statistik (Matomo). Du kannst das jederzeit in den Einstellungen ändern.',
|
||||||
|
allow: 'Erlauben',
|
||||||
|
later: 'Nicht jetzt',
|
||||||
|
},
|
||||||
|
},
|
||||||
navigation: {
|
navigation: {
|
||||||
home: 'Start',
|
home: 'Start',
|
||||||
tasks: 'Aufgaben',
|
tasks: 'Aufgaben',
|
||||||
@@ -700,6 +708,12 @@ export const messages: Record<LocaleCode, NestedMessages> = {
|
|||||||
label: 'Vibrationen aktivieren',
|
label: 'Vibrationen aktivieren',
|
||||||
unsupported: 'Auf diesem Gerät nicht verfügbar.',
|
unsupported: 'Auf diesem Gerät nicht verfügbar.',
|
||||||
},
|
},
|
||||||
|
analytics: {
|
||||||
|
title: 'Anonyme Statistik',
|
||||||
|
description: 'Hilft uns, die Guest-App zu verbessern (Matomo, ohne Tracking-Cookies).',
|
||||||
|
label: 'Statistik zulassen',
|
||||||
|
note: 'Du kannst deine Entscheidung jederzeit hier ändern.',
|
||||||
|
},
|
||||||
legal: {
|
legal: {
|
||||||
title: 'Rechtliches',
|
title: 'Rechtliches',
|
||||||
description: 'Die rechtlich verbindlichen Texte sind jederzeit hier abrufbar.',
|
description: 'Die rechtlich verbindlichen Texte sind jederzeit hier abrufbar.',
|
||||||
@@ -772,6 +786,14 @@ export const messages: Record<LocaleCode, NestedMessages> = {
|
|||||||
loading: 'Loading...',
|
loading: 'Loading...',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
consent: {
|
||||||
|
analytics: {
|
||||||
|
title: 'Help us improve the app',
|
||||||
|
body: 'Allow anonymous analytics (Matomo). You can change this anytime in settings.',
|
||||||
|
allow: 'Allow',
|
||||||
|
later: 'Not now',
|
||||||
|
},
|
||||||
|
},
|
||||||
navigation: {
|
navigation: {
|
||||||
home: 'Home',
|
home: 'Home',
|
||||||
tasks: 'Tasks',
|
tasks: 'Tasks',
|
||||||
@@ -1441,6 +1463,12 @@ export const messages: Record<LocaleCode, NestedMessages> = {
|
|||||||
label: 'Enable vibrations',
|
label: 'Enable vibrations',
|
||||||
unsupported: 'Not available on this device.',
|
unsupported: 'Not available on this device.',
|
||||||
},
|
},
|
||||||
|
analytics: {
|
||||||
|
title: 'Anonymous analytics',
|
||||||
|
description: 'Helps us improve the Guest app (Matomo, no tracking cookies).',
|
||||||
|
label: 'Allow analytics',
|
||||||
|
note: 'You can change this anytime here.',
|
||||||
|
},
|
||||||
legal: {
|
legal: {
|
||||||
title: 'Legal',
|
title: 'Legal',
|
||||||
description: 'The legally binding documents are always available here.',
|
description: 'The legally binding documents are always available here.',
|
||||||
|
|||||||
44
resources/js/guest/lib/__tests__/analyticsConsent.test.ts
Normal file
44
resources/js/guest/lib/__tests__/analyticsConsent.test.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { isUploadPath, shouldShowAnalyticsNudge } from '../analyticsConsent';
|
||||||
|
|
||||||
|
describe('isUploadPath', () => {
|
||||||
|
it('detects upload routes', () => {
|
||||||
|
expect(isUploadPath('/e/abc/upload')).toBe(true);
|
||||||
|
expect(isUploadPath('/e/abc/upload/queue')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores non-upload routes', () => {
|
||||||
|
expect(isUploadPath('/e/abc/gallery')).toBe(false);
|
||||||
|
expect(isUploadPath('/settings')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('shouldShowAnalyticsNudge', () => {
|
||||||
|
const baseState = {
|
||||||
|
decisionMade: false,
|
||||||
|
analyticsConsent: false,
|
||||||
|
snoozedUntil: null,
|
||||||
|
now: 1000,
|
||||||
|
activeSeconds: 60,
|
||||||
|
routeCount: 2,
|
||||||
|
thresholdSeconds: 60,
|
||||||
|
thresholdRoutes: 2,
|
||||||
|
isUpload: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
it('returns true when thresholds are met', () => {
|
||||||
|
expect(shouldShowAnalyticsNudge(baseState)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false when consent decision is made', () => {
|
||||||
|
expect(shouldShowAnalyticsNudge({ ...baseState, decisionMade: true })).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false when snoozed', () => {
|
||||||
|
expect(shouldShowAnalyticsNudge({ ...baseState, snoozedUntil: 2000 })).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false on upload routes', () => {
|
||||||
|
expect(shouldShowAnalyticsNudge({ ...baseState, isUpload: true })).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
34
resources/js/guest/lib/analyticsConsent.ts
Normal file
34
resources/js/guest/lib/analyticsConsent.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
export type AnalyticsNudgeState = {
|
||||||
|
decisionMade: boolean;
|
||||||
|
analyticsConsent: boolean;
|
||||||
|
snoozedUntil: number | null;
|
||||||
|
now: number;
|
||||||
|
activeSeconds: number;
|
||||||
|
routeCount: number;
|
||||||
|
thresholdSeconds: number;
|
||||||
|
thresholdRoutes: number;
|
||||||
|
isUpload: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function isUploadPath(pathname: string): boolean {
|
||||||
|
return /\/upload(?:\/|$)/.test(pathname);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shouldShowAnalyticsNudge(state: AnalyticsNudgeState): boolean {
|
||||||
|
if (state.decisionMade || state.analyticsConsent) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.isUpload) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.snoozedUntil && state.snoozedUntil > state.now) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
state.activeSeconds >= state.thresholdSeconds &&
|
||||||
|
state.routeCount >= state.thresholdRoutes
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import '../../css/app.css';
|
|||||||
import { enableGuestDemoMode, shouldEnableGuestDemoMode } from './demo/demoMode';
|
import { enableGuestDemoMode, shouldEnableGuestDemoMode } from './demo/demoMode';
|
||||||
import { Sentry, initSentry } from '@/lib/sentry';
|
import { Sentry, initSentry } from '@/lib/sentry';
|
||||||
import { AppearanceProvider, initializeTheme } from '@/hooks/use-appearance';
|
import { AppearanceProvider, initializeTheme } from '@/hooks/use-appearance';
|
||||||
|
import { ConsentProvider } from '@/contexts/consent';
|
||||||
|
|
||||||
const GuestFallback: React.FC<{ message: string }> = ({ message }) => (
|
const GuestFallback: React.FC<{ message: string }> = ({ message }) => (
|
||||||
<div className="flex min-h-screen items-center justify-center text-sm text-muted-foreground">
|
<div className="flex min-h-screen items-center justify-center text-sm text-muted-foreground">
|
||||||
@@ -50,15 +51,17 @@ const appRoot = async () => {
|
|||||||
<Sentry.ErrorBoundary fallback={<GuestFallback message="Erlebnisse können nicht geladen werden." />}>
|
<Sentry.ErrorBoundary fallback={<GuestFallback message="Erlebnisse können nicht geladen werden." />}>
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<AppearanceProvider>
|
<AppearanceProvider>
|
||||||
<LocaleProvider>
|
<ConsentProvider>
|
||||||
<ToastProvider>
|
<LocaleProvider>
|
||||||
<MatomoTracker config={matomoConfig} />
|
<ToastProvider>
|
||||||
<PwaManager />
|
<MatomoTracker config={matomoConfig} />
|
||||||
<Suspense fallback={<GuestFallback message="Erlebnisse werden geladen …" />}>
|
<PwaManager />
|
||||||
<RouterProvider router={router} />
|
<Suspense fallback={<GuestFallback message="Erlebnisse werden geladen …" />}>
|
||||||
</Suspense>
|
<RouterProvider router={router} />
|
||||||
</ToastProvider>
|
</Suspense>
|
||||||
</LocaleProvider>
|
</ToastProvider>
|
||||||
|
</LocaleProvider>
|
||||||
|
</ConsentProvider>
|
||||||
</AppearanceProvider>
|
</AppearanceProvider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
</Sentry.ErrorBoundary>
|
</Sentry.ErrorBoundary>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import type { EventBrandingPayload, FetchEventErrorCode } from './services/event
|
|||||||
import { NotificationCenterProvider } from './context/NotificationCenterContext';
|
import { NotificationCenterProvider } from './context/NotificationCenterContext';
|
||||||
import RouteErrorElement from '@/components/RouteErrorElement';
|
import RouteErrorElement from '@/components/RouteErrorElement';
|
||||||
import { isTaskModeEnabled } from './lib/engagement';
|
import { isTaskModeEnabled } from './lib/engagement';
|
||||||
|
import GuestAnalyticsNudge from './components/GuestAnalyticsNudge';
|
||||||
|
|
||||||
const LandingPage = React.lazy(() => import('./pages/LandingPage'));
|
const LandingPage = React.lazy(() => import('./pages/LandingPage'));
|
||||||
const ProfileSetupPage = React.lazy(() => import('./pages/ProfileSetupPage'));
|
const ProfileSetupPage = React.lazy(() => import('./pages/ProfileSetupPage'));
|
||||||
@@ -40,6 +41,8 @@ const NotFoundPage = React.lazy(() => import('./pages/NotFoundPage'));
|
|||||||
|
|
||||||
function HomeLayout() {
|
function HomeLayout() {
|
||||||
const { token } = useParams();
|
const { token } = useParams();
|
||||||
|
const location = useLocation();
|
||||||
|
const matomoEnabled = typeof window !== 'undefined' && Boolean((window as any).__MATOMO_GUEST__?.enabled);
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return (
|
return (
|
||||||
@@ -49,6 +52,7 @@ function HomeLayout() {
|
|||||||
<RouteTransition />
|
<RouteTransition />
|
||||||
</div>
|
</div>
|
||||||
<BottomNav />
|
<BottomNav />
|
||||||
|
<GuestAnalyticsNudge enabled={matomoEnabled} pathname={location.pathname} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -56,6 +60,7 @@ function HomeLayout() {
|
|||||||
return (
|
return (
|
||||||
<GuestIdentityProvider eventKey={token}>
|
<GuestIdentityProvider eventKey={token}>
|
||||||
<EventBoundary token={token} />
|
<EventBoundary token={token} />
|
||||||
|
<GuestAnalyticsNudge enabled={matomoEnabled} pathname={location.pathname} />
|
||||||
</GuestIdentityProvider>
|
</GuestIdentityProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -158,6 +163,8 @@ function TaskGuard({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
function SetupLayout() {
|
function SetupLayout() {
|
||||||
const { token } = useParams<{ token: string }>();
|
const { token } = useParams<{ token: string }>();
|
||||||
|
const location = useLocation();
|
||||||
|
const matomoEnabled = typeof window !== 'undefined' && Boolean((window as any).__MATOMO_GUEST__?.enabled);
|
||||||
const { event } = useEventData();
|
const { event } = useEventData();
|
||||||
if (!token) return null;
|
if (!token) return null;
|
||||||
const eventLocale = event && isLocaleCode(event.default_locale) ? event.default_locale : DEFAULT_LOCALE;
|
const eventLocale = event && isLocaleCode(event.default_locale) ? event.default_locale : DEFAULT_LOCALE;
|
||||||
@@ -177,6 +184,7 @@ function SetupLayout() {
|
|||||||
</EventStatsProvider>
|
</EventStatsProvider>
|
||||||
</EventBrandingProvider>
|
</EventBrandingProvider>
|
||||||
</LocaleProvider>
|
</LocaleProvider>
|
||||||
|
<GuestAnalyticsNudge enabled={matomoEnabled} pathname={location.pathname} />
|
||||||
</GuestIdentityProvider>
|
</GuestIdentityProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -327,6 +335,8 @@ function getErrorContent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function SimpleLayout({ title, children }: { title: string; children: React.ReactNode }) {
|
function SimpleLayout({ title, children }: { title: string; children: React.ReactNode }) {
|
||||||
|
const location = useLocation();
|
||||||
|
const matomoEnabled = typeof window !== 'undefined' && Boolean((window as any).__MATOMO_GUEST__?.enabled);
|
||||||
return (
|
return (
|
||||||
<EventBrandingProvider>
|
<EventBrandingProvider>
|
||||||
<div className="pb-16">
|
<div className="pb-16">
|
||||||
@@ -335,6 +345,7 @@ function SimpleLayout({ title, children }: { title: string; children: React.Reac
|
|||||||
<RouteTransition>{children}</RouteTransition>
|
<RouteTransition>{children}</RouteTransition>
|
||||||
</div>
|
</div>
|
||||||
<BottomNav />
|
<BottomNav />
|
||||||
|
<GuestAnalyticsNudge enabled={matomoEnabled} pathname={location.pathname} />
|
||||||
</div>
|
</div>
|
||||||
</EventBrandingProvider>
|
</EventBrandingProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user