Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3.8 KiB
Onboarding Feature Refactor — Design Spec
2026-06-19
Goal
Reorganise src/features/onboarding/ into clean sub-directories. Replace ad-hoc useState(loading) + try/finally patterns with useMutation/useQuery hooks. Steps become pure JSX shells.
Directory Structure (target)
onboarding/
actions/ ← server actions (unchanged)
constants/
steps.ts ← STEP_ORDER array + STEP_IDS const
hooks/
use-create-agent.ts
use-delete-agent.ts
use-agent-status.ts ← useQuery, 3s poll
use-create-org.ts
use-create-project.ts
use-update-account.ts
use-add-notifier.ts
use-remove-notifier.ts
use-add-storage.ts
use-remove-storage.ts
use-generate-edge-key.ts
use-mark-onboarding-done.ts
schemas/
account.schema.ts ← BaseSchema + WithPasswordSchema
types/
index.ts ← content of onboarding.types.ts (moved)
utils/ ← reserved, empty for now
steps/ ← existing components, thinned to JSX
onboarding-checklist.tsx
onboarding-shell.tsx
onboarding-state.ts
onboarding-stepper.tsx
onboarding-steps.tsx
is-onboarding-done.ts
onboarding.types.ts ← deleted once types/index.ts takes over
Hook Contract
Every hook calls useOnboarding() internally. Steps pass nothing — they only import the hook.
Mutation hooks return UseMutationResult directly so steps get mutate, isPending, isError etc. from TanStack Query natively.
// Pattern
export const useCreateAgent = () => {
const { state, updateContext } = useOnboarding();
return useMutation({
mutationFn: async (name: string) => {
const orgId = (state?.context.flowData.org as any)?.id;
if (!orgId) throw new Error("Missing org ID");
const result = await createAgentAction({ organizationId: orgId, data: { name, description: "" } });
if (!result?.data?.data) throw new Error(result?.serverError ?? "Failed to create agent");
const newAgent: OnboardingAgent = { id: result.data.data.id, name: result.data.data.name };
const agents = [...((state?.context.flowData.agents ?? []) as OnboardingAgent[]), newAgent];
await updateContext({ flowData: { ...state?.context.flowData, agents } });
return newAgent;
},
onError: (err: Error) => toast.error(err.message),
});
};
Query hooks return UseQueryResult:
export const useAgentStatus = () => {
const { state } = useOnboarding();
const agentId = (state?.context.flowData.agents as OnboardingAgent[])?.[0]?.id;
return useQuery({
queryKey: ["onboarding-agent-status", agentId],
queryFn: async () => { /* getAgentStatusAction */ },
refetchInterval: 3_000,
enabled: !!agentId,
});
};
Constants
constants/steps.ts exports:
STEP_ORDER: string[]— the full ordered list used by the shell for Back/Next logicSTEP_IDS— typed const object{ LOGIN: "login", ... }for safe step references
Schemas
schemas/account.schema.ts exports BaseSchema and WithPasswordSchema (currently inline in step-account-info.tsx).
Types
types/index.ts is a verbatim move of onboarding.types.ts. All existing imports updated from @/features/onboarding/onboarding.types → @/features/onboarding/types.
Steps After Refactor
Each step file shrinks to: imports + JSX + hook call. No direct action imports, no manual loading state.
Example diff for step-agent-create.tsx:
- Remove:
createAgentAction,deleteAgentAction,useState(adding), manual try/finally - Add:
useCreateAgent(),useDeleteAgent()
Out of Scope
onboarding-state.tsserver logic — untouchedonboarding-steps.tsxstep registry — untouched- UI/UX changes to any step
- Adding new steps