Files
portabase/docs/superpowers/specs/2026-06-19-onboarding-refactor-design.md
T
2026-06-19 10:14:44 +02:00

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 logic
  • STEP_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.ts server logic — untouched
  • onboarding-steps.tsx step registry — untouched
  • UI/UX changes to any step
  • Adding new steps