163 lines
4.9 KiB
TypeScript
163 lines
4.9 KiB
TypeScript
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
|
|
export type OnboardingStep =
|
|
| 'welcome'
|
|
| 'account-types'
|
|
| 'create-account'
|
|
| 'category-types'
|
|
| 'customize-categories'
|
|
| 'smart-rules'
|
|
| 'syncing'
|
|
| 'ai-suggestions'
|
|
| 'import-transactions'
|
|
| 'import-balances'
|
|
| 'categorize-transactions'
|
|
| 'complete';
|
|
|
|
// Primary steps shown in the progress indicator
|
|
// import-transactions and import-balances are sub-steps that don't increment the counter
|
|
const PRIMARY_STEPS: OnboardingStep[] = [
|
|
'welcome',
|
|
'account-types',
|
|
'create-account',
|
|
'category-types',
|
|
'smart-rules',
|
|
'syncing',
|
|
'ai-suggestions',
|
|
'categorize-transactions',
|
|
'complete',
|
|
];
|
|
|
|
// Steps that are sub-steps (shown under the same progress position as 'create-account')
|
|
const SUB_STEPS: OnboardingStep[] = ['import-transactions', 'import-balances'];
|
|
|
|
export interface OnboardingState {
|
|
currentStep: OnboardingStep;
|
|
stepIndex: number;
|
|
totalSteps: number;
|
|
createdAccounts: CreatedAccount[];
|
|
isFirstAccount: boolean;
|
|
hasSelectedConnectedAccount: boolean;
|
|
}
|
|
|
|
export interface CreatedAccount {
|
|
id: string;
|
|
name: string;
|
|
type: string;
|
|
currencyCode: string;
|
|
bankName?: string;
|
|
bankLogo?: string | null;
|
|
connected?: boolean;
|
|
}
|
|
|
|
interface UseOnboardingStateOptions {
|
|
existingAccountsCount?: number;
|
|
initialStep?: OnboardingStep;
|
|
hasConnectedAccount?: boolean;
|
|
}
|
|
|
|
export function useOnboardingState(options: UseOnboardingStateOptions = {}) {
|
|
const {
|
|
existingAccountsCount = 0,
|
|
initialStep,
|
|
hasConnectedAccount = false,
|
|
} = options;
|
|
|
|
const primarySteps = PRIMARY_STEPS;
|
|
|
|
// Determine initial step based on existing state
|
|
const resolvedInitialStep = useMemo((): OnboardingStep => {
|
|
return initialStep ?? 'welcome';
|
|
}, [initialStep]);
|
|
|
|
const [currentStep, setCurrentStep] =
|
|
useState<OnboardingStep>(resolvedInitialStep);
|
|
const [createdAccounts, setCreatedAccounts] = useState<CreatedAccount[]>(
|
|
[],
|
|
);
|
|
const [hasSelectedConnectedAccount, setHasSelectedConnectedAccount] =
|
|
useState(hasConnectedAccount);
|
|
|
|
useEffect(() => {
|
|
if (hasConnectedAccount) {
|
|
setHasSelectedConnectedAccount(true);
|
|
}
|
|
}, [hasConnectedAccount]);
|
|
|
|
// Keep the ?step= query param in sync with the current step so a manual
|
|
// refresh returns the user to the step they were on. Use replaceState to
|
|
// avoid polluting browser history and preserve Inertia's stored page state.
|
|
useEffect(() => {
|
|
if (typeof window === 'undefined') {
|
|
return;
|
|
}
|
|
const url = new URL(window.location.href);
|
|
if (url.searchParams.get('step') === currentStep) {
|
|
return;
|
|
}
|
|
url.searchParams.set('step', currentStep);
|
|
window.history.replaceState(window.history.state, '', url.toString());
|
|
}, [currentStep]);
|
|
|
|
// Calculate step index for progress indicator
|
|
// Sub-steps (import-transactions, import-balances) use the same index as 'create-account'
|
|
const stepIndex = useMemo(() => {
|
|
if (SUB_STEPS.includes(currentStep)) {
|
|
// Sub-steps show under the 'create-account' position
|
|
return primarySteps.indexOf('create-account');
|
|
}
|
|
return primarySteps.indexOf(currentStep);
|
|
}, [currentStep, primarySteps]);
|
|
|
|
const totalSteps = primarySteps.length;
|
|
|
|
const goToStep = useCallback((step: OnboardingStep) => {
|
|
setCurrentStep(step);
|
|
}, []);
|
|
|
|
const goNext = useCallback(() => {
|
|
// Find the next primary step
|
|
const primaryIndex = primarySteps.indexOf(currentStep);
|
|
if (primaryIndex >= 0 && primaryIndex < primarySteps.length - 1) {
|
|
setCurrentStep(primarySteps[primaryIndex + 1]);
|
|
}
|
|
}, [currentStep, primarySteps]);
|
|
|
|
const goBack = useCallback(() => {
|
|
// If we're on a sub-step, go back to create-account
|
|
if (SUB_STEPS.includes(currentStep)) {
|
|
setCurrentStep('create-account');
|
|
return;
|
|
}
|
|
const primaryIndex = primarySteps.indexOf(currentStep);
|
|
if (primaryIndex > 0) {
|
|
setCurrentStep(primarySteps[primaryIndex - 1]);
|
|
}
|
|
}, [currentStep, primarySteps]);
|
|
|
|
const addCreatedAccount = useCallback((account: CreatedAccount) => {
|
|
setCreatedAccounts((prev) => [...prev, account]);
|
|
}, []);
|
|
|
|
const markConnectedAccountSelected = useCallback(() => {
|
|
setHasSelectedConnectedAccount(true);
|
|
}, []);
|
|
|
|
const isFirstAccount =
|
|
createdAccounts.length === 0 && existingAccountsCount === 0;
|
|
|
|
return {
|
|
currentStep,
|
|
stepIndex,
|
|
totalSteps,
|
|
createdAccounts,
|
|
isFirstAccount,
|
|
hasSelectedConnectedAccount,
|
|
goToStep,
|
|
goNext,
|
|
goBack,
|
|
addCreatedAccount,
|
|
markConnectedAccountSelected,
|
|
};
|
|
}
|