222 lines
5.7 KiB
TypeScript
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;
|
|
}
|