Files
fotospiel-app/resources/js/admin/onboarding/store.tsx
2025-11-17 12:24:14 +01:00

222 lines
5.7 KiB
TypeScript

import React from 'react';
import { fetchOnboardingStatus, trackOnboarding } from '../api';
import { useAuth } from '../auth/context';
export type OnboardingProgress = {
welcomeSeen: boolean;
packageSelected: boolean;
eventCreated: boolean;
lastStep?: string | null;
adminAppOpenedAt?: string | null;
selectedPackage?: {
id: number;
name: string;
priceText?: string | null;
isSubscription?: boolean;
} | null;
inviteCreated: boolean;
brandingConfigured: boolean;
};
type OnboardingUpdate = Partial<OnboardingProgress> & {
serverStep?: string;
meta?: Record<string, unknown>;
};
type OnboardingContextValue = {
progress: OnboardingProgress;
setProgress: (updater: (prev: OnboardingProgress) => OnboardingProgress) => void;
markStep: (step: OnboardingUpdate) => void;
reset: () => void;
};
const DEFAULT_PROGRESS: OnboardingProgress = {
welcomeSeen: false,
packageSelected: false,
eventCreated: false,
lastStep: null,
adminAppOpenedAt: null,
selectedPackage: null,
inviteCreated: false,
brandingConfigured: false,
};
const STORAGE_KEY = 'tenant-admin:onboarding-progress';
const OnboardingProgressContext = React.createContext<OnboardingContextValue | undefined>(undefined);
function readStoredProgress(): OnboardingProgress {
if (typeof window === 'undefined') {
return DEFAULT_PROGRESS;
}
try {
const raw = window.localStorage.getItem(STORAGE_KEY);
if (!raw) {
return DEFAULT_PROGRESS;
}
const parsed = JSON.parse(raw) as Partial<OnboardingProgress>;
return {
...DEFAULT_PROGRESS,
...parsed,
};
} catch (error) {
console.warn('[OnboardingProgress] Failed to parse stored value', error);
return DEFAULT_PROGRESS;
}
}
function writeStoredProgress(progress: OnboardingProgress) {
if (typeof window === 'undefined') {
return;
}
try {
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(progress));
} catch (error) {
console.warn('[OnboardingProgress] Failed to persist value', error);
}
}
export function OnboardingProgressProvider({ children }: { children: React.ReactNode }) {
const [progress, setProgressState] = React.useState<OnboardingProgress>(() => readStoredProgress());
const [synced, setSynced] = React.useState(false);
const { status } = useAuth();
React.useEffect(() => {
if (status !== 'authenticated') {
if (synced) {
setSynced(false);
}
return;
}
if (synced) {
return;
}
let cancelled = false;
fetchOnboardingStatus().then((status) => {
if (cancelled) {
return;
}
if (!status) {
setSynced(true);
return;
}
const steps = status.steps ?? {};
setProgressState((prev) => {
const next: OnboardingProgress = {
...prev,
adminAppOpenedAt: steps.admin_app_opened_at ?? prev.adminAppOpenedAt ?? null,
eventCreated: Boolean(steps.event_created ?? prev.eventCreated),
packageSelected: Boolean(steps.selected_packages ?? prev.packageSelected),
inviteCreated: Boolean(steps.invite_created ?? prev.inviteCreated),
brandingConfigured: Boolean(steps.branding_completed ?? prev.brandingConfigured),
};
writeStoredProgress(next);
return next;
});
if (!steps.admin_app_opened_at) {
const timestamp = new Date().toISOString();
trackOnboarding('admin_app_opened').catch(() => {});
setProgressState((prev) => {
const next = { ...prev, adminAppOpenedAt: timestamp };
writeStoredProgress(next);
return next;
});
}
setSynced(true);
}).catch(() => {
if (!cancelled) {
setSynced(true);
}
});
return () => {
cancelled = true;
};
}, [status, synced]);
const setProgress = React.useCallback((updater: (prev: OnboardingProgress) => OnboardingProgress) => {
setProgressState((prev) => {
const next = updater(prev);
writeStoredProgress(next);
return next;
});
}, []);
const markStep = React.useCallback((step: OnboardingUpdate) => {
const { serverStep, meta, ...rest } = step;
setProgress((prev) => {
const derived: Partial<OnboardingProgress> = {};
switch (serverStep) {
case 'package_selected':
derived.packageSelected = true;
break;
case 'event_created':
derived.eventCreated = true;
break;
case 'invite_created':
derived.inviteCreated = true;
break;
case 'branding_configured':
derived.brandingConfigured = true;
break;
default:
break;
}
const next: OnboardingProgress = {
...prev,
...rest,
...derived,
lastStep: typeof rest.lastStep === 'undefined' ? prev.lastStep : rest.lastStep,
};
if (serverStep === 'admin_app_opened' && !next.adminAppOpenedAt) {
next.adminAppOpenedAt = new Date().toISOString();
}
return next;
});
if (serverStep) {
trackOnboarding(serverStep, meta).catch(() => {});
}
}, [setProgress]);
const reset = React.useCallback(() => {
setProgress(() => DEFAULT_PROGRESS);
}, [setProgress]);
const value = React.useMemo<OnboardingContextValue>(() => ({
progress,
setProgress,
markStep,
reset,
}), [progress, setProgress, markStep, reset]);
return (
<OnboardingProgressContext.Provider value={value}>
{children}
</OnboardingProgressContext.Provider>
);
}
export function useOnboardingProgress() {
const context = React.useContext(OnboardingProgressContext);
if (!context) {
throw new Error('useOnboardingProgress must be used within OnboardingProgressProvider');
}
return context;
}