diff --git a/apps/dashboard/app/(authenticated)/layout.tsx b/apps/dashboard/app/(authenticated)/layout.tsx index a2aa041..563cb51 100644 --- a/apps/dashboard/app/(authenticated)/layout.tsx +++ b/apps/dashboard/app/(authenticated)/layout.tsx @@ -6,6 +6,7 @@ import { DashboardLayout } from "@/base/components/layout/dashboard" import { useAuth } from "@/shared/hooks/use-auth" import { navGroups } from "@/config/navGroups" import { getAuthCookies } from "@/modules/auth/auth.actions" +import { OnboardingGate } from "@/shared/components/onboarding-gate" import { redirect } from "next/navigation" @@ -38,7 +39,7 @@ export default async function AuthenticatedLayout({ return ( } user={userInfo}> - {children} + {children} ) } diff --git a/apps/dashboard/modules/settings/company/settings-form.tsx b/apps/dashboard/modules/settings/company/settings-form.tsx index 9946e60..a3b0371 100644 --- a/apps/dashboard/modules/settings/company/settings-form.tsx +++ b/apps/dashboard/modules/settings/company/settings-form.tsx @@ -4,7 +4,7 @@ import { AlertTriangle, Save } from "lucide-react" import { useEffect } from "react" import { useForm } from "react-hook-form" import { zodResolver } from "@hookform/resolvers/zod" -import { useQuery } from "@tanstack/react-query" +import { useQuery, useQueryClient } from "@tanstack/react-query" import { toast } from "sonner" import { Button } from "@/shared/components/ui/button" @@ -122,6 +122,7 @@ function mapFormToPayload(values: SettingsFormValues) { export function SettingsForm() { const api = useAuthApi() + const queryClient = useQueryClient() const form = useForm({ resolver: zodResolver(settingsFormSchema) as any, @@ -154,6 +155,9 @@ export function SettingsForm() { mutationFn: (values: SettingsFormValues) => api.settings.update(mapFormToPayload(values)), onSuccess: () => { toast.success("Settings saved successfully") + // Refresh the settings query so the onboarding gate releases once + // the company info has been filled in. + queryClient.invalidateQueries({ queryKey: [SETTINGS_ROUTES.INDEX] }) }, onError: () => { toast.error("Failed to save settings") diff --git a/apps/dashboard/shared/components/onboarding-gate.tsx b/apps/dashboard/shared/components/onboarding-gate.tsx new file mode 100644 index 0000000..99a920a --- /dev/null +++ b/apps/dashboard/shared/components/onboarding-gate.tsx @@ -0,0 +1,72 @@ +"use client" + +import { useEffect, useMemo } from "react" +import { usePathname, useRouter } from "next/navigation" +import { useQuery } from "@tanstack/react-query" +import { Loader2 } from "lucide-react" + +import { useAuthApi } from "@/shared/useApi" +import { SETTINGS_ROUTES } from "@garage/api" + +const COMPANY_SETUP_PATH = "/settings/company" + +function FullScreenLoader({ label }: { label: string }) { + return ( +
+ +

{label}

+
+ ) +} + +/** + * Onboarding gate. + * + * After login/activation the workspace's company information must be filled in + * before the rest of the app is usable. While the settings record loads we show + * a full-screen loader; if the company isn't set up yet we force the user onto + * the company-settings page and block navigation to any other route until it is. + */ +export function OnboardingGate({ children }: { children: React.ReactNode }) { + const api = useAuthApi() + const router = useRouter() + const pathname = usePathname() + + const { data, isLoading, isError } = useQuery({ + queryKey: [SETTINGS_ROUTES.INDEX], + queryFn: () => api.settings.fetch(), + staleTime: 5 * 60 * 1000, + retry: 1, + }) + + const isCompanyFilled = useMemo(() => { + const raw = (data as any)?.data + const record = Array.isArray(raw) ? raw[0] : raw + return Boolean(record?.id) && Boolean(String(record?.name ?? "").trim()) + }, [data]) + + const onCompanyPage = pathname?.startsWith(COMPANY_SETUP_PATH) ?? false + + // Onboarding is required once we know (loaded, no error) the company isn't set up. + // Fail open on error so a transient API failure can never lock the user out. + const mustOnboard = !isLoading && !isError && !isCompanyFilled + + useEffect(() => { + if (mustOnboard && !onCompanyPage) { + router.replace(COMPANY_SETUP_PATH) + } + }, [mustOnboard, onCompanyPage, router]) + + // Initial load — determine onboarding status before showing anything. + if (isLoading) { + return + } + + // Needs onboarding and not yet on the company page → show loader while redirecting, + // so other pages never flash or become usable. + if (mustOnboard && !onCompanyPage) { + return + } + + return <>{children} +}