From 6f76e430f4e6db48645e2158d9a7338ec38856c6 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Mon, 9 Feb 2026 22:20:19 +0530 Subject: [PATCH] refactor: enhance onboarding logic and update default redirect path --- frontend/src/app/onboarding/page.tsx | 10 +--- frontend/src/auth/redirects.test.ts | 4 +- frontend/src/auth/redirects.ts | 2 +- .../components/templates/DashboardShell.tsx | 33 ++++++++++++- frontend/src/lib/onboarding.test.ts | 47 +++++++++++++++++++ frontend/src/lib/onboarding.ts | 13 +++++ 6 files changed, 97 insertions(+), 12 deletions(-) create mode 100644 frontend/src/lib/onboarding.test.ts create mode 100644 frontend/src/lib/onboarding.ts diff --git a/frontend/src/app/onboarding/page.tsx b/frontend/src/app/onboarding/page.tsx index 6f17955f..643b5949 100644 --- a/frontend/src/app/onboarding/page.tsx +++ b/frontend/src/app/onboarding/page.tsx @@ -20,17 +20,11 @@ import { useGetMeApiV1UsersMeGet, useUpdateMeApiV1UsersMePatch, } from "@/api/generated/users/users"; -import type { UserRead } from "@/api/generated/model"; import { DashboardShell } from "@/components/templates/DashboardShell"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import SearchableSelect from "@/components/ui/searchable-select"; - -const isCompleteProfile = (profile: UserRead | null | undefined) => { - if (!profile) return false; - const resolvedName = profile.preferred_name?.trim() || profile.name?.trim(); - return Boolean(resolvedName) && Boolean(profile.timezone?.trim()); -}; +import { isOnboardingComplete } from "@/lib/onboarding"; export default function OnboardingPage() { const router = useRouter(); @@ -113,7 +107,7 @@ export default function OnboardingPage() { ); useEffect(() => { - if (profile && isCompleteProfile(profile)) { + if (profile && isOnboardingComplete(profile)) { router.replace("/dashboard"); } }, [profile, router]); diff --git a/frontend/src/auth/redirects.test.ts b/frontend/src/auth/redirects.test.ts index 45638c3f..5e64684b 100644 --- a/frontend/src/auth/redirects.test.ts +++ b/frontend/src/auth/redirects.test.ts @@ -13,8 +13,8 @@ describe("resolveSignInRedirectUrl", () => { expect(resolveSignInRedirectUrl(null)).toBe("/boards"); }); - it("defaults to /dashboard when no env fallback is set", () => { - expect(resolveSignInRedirectUrl(null)).toBe("/dashboard"); + it("defaults to /onboarding when no env fallback is set", () => { + expect(resolveSignInRedirectUrl(null)).toBe("/onboarding"); }); it("allows safe relative paths", () => { diff --git a/frontend/src/auth/redirects.ts b/frontend/src/auth/redirects.ts index aa27617f..90b9da0b 100644 --- a/frontend/src/auth/redirects.ts +++ b/frontend/src/auth/redirects.ts @@ -1,4 +1,4 @@ -const DEFAULT_SIGN_IN_REDIRECT = "/dashboard"; +const DEFAULT_SIGN_IN_REDIRECT = "/onboarding"; function isSafeRelativePath(value: string): boolean { return value.startsWith("/") && !value.startsWith("//"); diff --git a/frontend/src/components/templates/DashboardShell.tsx b/frontend/src/components/templates/DashboardShell.tsx index cef03809..0baa96f2 100644 --- a/frontend/src/components/templates/DashboardShell.tsx +++ b/frontend/src/components/templates/DashboardShell.tsx @@ -2,17 +2,48 @@ import { useEffect } from "react"; import type { ReactNode } from "react"; +import { usePathname, useRouter } from "next/navigation"; -import { SignedIn, useUser } from "@/auth/clerk"; +import { SignedIn, useAuth, useUser } from "@/auth/clerk"; +import { ApiError } from "@/api/mutator"; +import { + type getMeApiV1UsersMeGetResponse, + useGetMeApiV1UsersMeGet, +} from "@/api/generated/users/users"; import { BrandMark } from "@/components/atoms/BrandMark"; import { OrgSwitcher } from "@/components/organisms/OrgSwitcher"; import { UserMenu } from "@/components/organisms/UserMenu"; +import { isOnboardingComplete } from "@/lib/onboarding"; export function DashboardShell({ children }: { children: ReactNode }) { + const router = useRouter(); + const pathname = usePathname(); + const { isSignedIn } = useAuth(); const { user } = useUser(); const displayName = user?.fullName ?? user?.firstName ?? user?.username ?? "Operator"; + const isOnboardingPath = pathname === "/onboarding"; + + const meQuery = useGetMeApiV1UsersMeGet< + getMeApiV1UsersMeGetResponse, + ApiError + >({ + query: { + enabled: Boolean(isSignedIn) && !isOnboardingPath, + retry: false, + refetchOnMount: "always", + }, + }); + const profile = meQuery.data?.status === 200 ? meQuery.data.data : null; + + useEffect(() => { + if (!isSignedIn || isOnboardingPath) return; + if (!profile) return; + if (!isOnboardingComplete(profile)) { + router.replace("/onboarding"); + } + }, [isOnboardingPath, isSignedIn, profile, router]); useEffect(() => { if (typeof window === "undefined") return; diff --git a/frontend/src/lib/onboarding.test.ts b/frontend/src/lib/onboarding.test.ts new file mode 100644 index 00000000..9b151aa7 --- /dev/null +++ b/frontend/src/lib/onboarding.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, it } from "vitest"; + +import { isOnboardingComplete } from "@/lib/onboarding"; + +describe("isOnboardingComplete", () => { + it("returns false when profile is missing", () => { + expect(isOnboardingComplete(null)).toBe(false); + expect(isOnboardingComplete(undefined)).toBe(false); + }); + + it("returns false when timezone is missing", () => { + expect( + isOnboardingComplete({ + preferred_name: "Asha", + timezone: "", + }), + ).toBe(false); + }); + + it("returns false when both name fields are missing", () => { + expect( + isOnboardingComplete({ + name: " ", + preferred_name: " ", + timezone: "America/New_York", + }), + ).toBe(false); + }); + + it("accepts preferred_name + timezone", () => { + expect( + isOnboardingComplete({ + preferred_name: "Asha", + timezone: "America/New_York", + }), + ).toBe(true); + }); + + it("accepts fallback name + timezone", () => { + expect( + isOnboardingComplete({ + name: "Asha", + timezone: "America/New_York", + }), + ).toBe(true); + }); +}); diff --git a/frontend/src/lib/onboarding.ts b/frontend/src/lib/onboarding.ts new file mode 100644 index 00000000..af1a9c7d --- /dev/null +++ b/frontend/src/lib/onboarding.ts @@ -0,0 +1,13 @@ +type OnboardingProfileLike = { + name?: string | null; + preferred_name?: string | null; + timezone?: string | null; +}; + +export function isOnboardingComplete( + profile: OnboardingProfileLike | null | undefined, +): boolean { + if (!profile) return false; + const resolvedName = profile.preferred_name?.trim() || profile.name?.trim(); + return Boolean(resolvedName) && Boolean(profile.timezone?.trim()); +}