import { useState, useCallback } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import toast from 'react-hot-toast'; import api from '@/services/api'; import { useAuthStore } from '@/stores'; // ==================== Types ==================== export type OnboardingStep = 'company' | 'invite' | 'plan' | 'complete'; export interface OnboardingState { currentStep: OnboardingStep; completedSteps: OnboardingStep[]; companyData: CompanyData | null; invitedUsers: InvitedUser[]; selectedPlanId: string | null; } export interface CompanyData { name: string; slug: string; domain?: string; logo_url?: string; industry?: string; size?: string; timezone?: string; } export interface InvitedUser { email: string; role: string; status: 'pending' | 'sent' | 'error'; } export interface Plan { id: string; name: string; display_name: string; description: string; price_monthly: number; price_yearly: number; features: string[]; is_popular?: boolean; } // ==================== API Functions ==================== const onboardingApi = { getStatus: async () => { const response = await api.get('/onboarding/status'); return response.data; }, updateCompany: async (data: CompanyData) => { const response = await api.patch('/tenants/current', data); return response.data; }, inviteUsers: async (users: { email: string; role: string }[]) => { const results = await Promise.allSettled( users.map((user) => api.post('/users/invite', user)) ); return results; }, getPlans: async (): Promise => { const response = await api.get('/plans'); return response.data; }, selectPlan: async (planId: string) => { const response = await api.post('/billing/subscription', { plan_id: planId }); return response.data; }, completeOnboarding: async () => { const response = await api.post('/onboarding/complete'); return response.data; }, }; // ==================== Query Keys ==================== export const onboardingKeys = { all: ['onboarding'] as const, status: () => [...onboardingKeys.all, 'status'] as const, plans: () => [...onboardingKeys.all, 'plans'] as const, }; // ==================== Hooks ==================== const STEPS: OnboardingStep[] = ['company', 'invite', 'plan', 'complete']; const STORAGE_KEY = 'onboarding_state'; function loadState(): Partial { try { const saved = localStorage.getItem(STORAGE_KEY); return saved ? JSON.parse(saved) : {}; } catch { return {}; } } function saveState(state: Partial) { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } catch { // Ignore storage errors } } function clearState() { try { localStorage.removeItem(STORAGE_KEY); } catch { // Ignore storage errors } } export function useOnboarding() { const queryClient = useQueryClient(); const { user } = useAuthStore(); const savedState = loadState(); const [state, setState] = useState({ currentStep: savedState.currentStep || 'company', completedSteps: savedState.completedSteps || [], companyData: savedState.companyData || null, invitedUsers: savedState.invitedUsers || [], selectedPlanId: savedState.selectedPlanId || null, }); // Update state and persist const updateState = useCallback((updates: Partial) => { setState((prev) => { const newState = { ...prev, ...updates }; saveState(newState); return newState; }); }, []); // Navigation const goToStep = useCallback((step: OnboardingStep) => { updateState({ currentStep: step }); }, [updateState]); const nextStep = useCallback(() => { const currentIndex = STEPS.indexOf(state.currentStep); if (currentIndex < STEPS.length - 1) { const nextStepName = STEPS[currentIndex + 1]; updateState({ currentStep: nextStepName, completedSteps: [...new Set([...state.completedSteps, state.currentStep])], }); } }, [state.currentStep, state.completedSteps, updateState]); const prevStep = useCallback(() => { const currentIndex = STEPS.indexOf(state.currentStep); if (currentIndex > 0) { updateState({ currentStep: STEPS[currentIndex - 1] }); } }, [state.currentStep, updateState]); const canGoNext = useCallback(() => { switch (state.currentStep) { case 'company': return !!state.companyData?.name && !!state.companyData?.slug; case 'invite': return true; // Optional step case 'plan': return !!state.selectedPlanId; case 'complete': return false; default: return false; } }, [state]); const canGoPrev = useCallback(() => { return STEPS.indexOf(state.currentStep) > 0 && state.currentStep !== 'complete'; }, [state.currentStep]); const getStepIndex = useCallback(() => { return STEPS.indexOf(state.currentStep); }, [state.currentStep]); const getTotalSteps = useCallback(() => { return STEPS.length; }, []); const isStepCompleted = useCallback((step: OnboardingStep) => { return state.completedSteps.includes(step); }, [state.completedSteps]); // Reset const resetOnboarding = useCallback(() => { clearState(); setState({ currentStep: 'company', completedSteps: [], companyData: null, invitedUsers: [], selectedPlanId: null, }); }, []); return { state, updateState, goToStep, nextStep, prevStep, canGoNext, canGoPrev, getStepIndex, getTotalSteps, isStepCompleted, resetOnboarding, steps: STEPS, }; } // ==================== Data Hooks ==================== export function usePlans() { return useQuery({ queryKey: onboardingKeys.plans(), queryFn: onboardingApi.getPlans, staleTime: 1000 * 60 * 5, // 5 minutes }); } export function useUpdateCompany() { const queryClient = useQueryClient(); return useMutation({ mutationFn: onboardingApi.updateCompany, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['tenant'] }); toast.success('Company information saved!'); }, onError: () => { toast.error('Failed to save company information'); }, }); } export function useInviteUsers() { return useMutation({ mutationFn: onboardingApi.inviteUsers, onSuccess: (results) => { const successful = results.filter((r) => r.status === 'fulfilled').length; const failed = results.filter((r) => r.status === 'rejected').length; if (successful > 0) { toast.success(`${successful} invitation${successful > 1 ? 's' : ''} sent!`); } if (failed > 0) { toast.error(`${failed} invitation${failed > 1 ? 's' : ''} failed`); } }, onError: () => { toast.error('Failed to send invitations'); }, }); } export function useSelectPlan() { const queryClient = useQueryClient(); return useMutation({ mutationFn: onboardingApi.selectPlan, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['subscription'] }); toast.success('Plan selected successfully!'); }, onError: () => { toast.error('Failed to select plan'); }, }); } export function useCompleteOnboarding() { const queryClient = useQueryClient(); return useMutation({ mutationFn: onboardingApi.completeOnboarding, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['user'] }); queryClient.invalidateQueries({ queryKey: ['tenant'] }); toast.success('Welcome aboard! Your setup is complete.'); }, onError: () => { toast.error('Failed to complete onboarding'); }, }); }