/** * Pure helpers for app boot / onboarding loader visibility. * Kept free of React so unit tests can assert real decision logic * without mounting the full app tree. */ export const PUBLIC_AUTH_PATHS = new Set([ "/", "/login", "/signup", "/forgot-password", "/reset-password", "/privacy", "/terms", "/refund", "/refunds", "/contact", "/feedback", "/report-bug", "/logout", "/pricing", "/preview/tuition", "/preview/chapter/sound-waves", ]); const PUBLIC_AUTH_PREFIXES = [ "/auth/", "/compare/", "/flashcards/", "/kerala-plus-one", "/kerala-plus-two", "/learn/", "/quiz/", "/revision/", ] as const; /** * Known signed-in app surfaces. Auth gates must use this allowlist so * unknown URLs fall through to Next.js `not-found` instead of login?next=. */ const PROTECTED_APP_PATHS = new Set([ "/onboarding", "/dashboard", "/home", "/home-next", "/tuition", "/study-chat", "/learn-anything", "/bookmarks", "/classes", "/notifications", "/pyq-analyzer", "/analytics", "/documents", "/exam-coach", "/first-hour", "/generate", "/last-night", "/library", "/missions", "/mode-selection", "/notes", "/previous-papers", "/profile", "/progress", "/quizzes", "/settings", "/sources", "/syllabus", "/teach", "/upload", "/video", "/videos", "/video-generator", ]); const PROTECTED_APP_PREFIXES = [ "/admin", "/onboarding", "/dashboard", "/home", "/home-next", "/tuition", "/study-chat", "/learn-anything", "/bookmarks", "/classes", "/notifications", "/pyq-analyzer", "/analytics", "/documents", "/exam-coach", "/first-hour", "/generate", "/last-night", "/library", "/missions", "/mode-selection", "/notes", "/previous-papers", "/profile", "/progress", "/quizzes", "/settings", "/sources", "/syllabus", "/teach", "/upload", "/video", "/videos", "/video-generator", ] as const; export function isPublicAuthPath(pathname: string | null | undefined): boolean { if (!pathname) return true; if (PUBLIC_AUTH_PATHS.has(pathname)) return true; if (pathname === "/learn") return true; return PUBLIC_AUTH_PREFIXES.some((prefix) => pathname.startsWith(prefix)); } function matchesPathOrPrefix(pathname: string, prefix: string): boolean { return pathname === prefix || pathname.startsWith(`${prefix}/`); } /** * True only for known workspace / admin app routes. * Unknown paths are intentionally false so RootLoadingGuard does not * intercept them into login — Next.js not-found handles those. */ export function isProtectedAppPath(pathname: string | null | undefined): boolean { if (!pathname) return false; if (isPublicAuthPath(pathname)) return false; if (PROTECTED_APP_PATHS.has(pathname)) return true; return PROTECTED_APP_PREFIXES.some((prefix) => matchesPathOrPrefix(pathname, prefix), ); } export type AppLoadingInput = { hasAuthContext: boolean; isAuthLoading: boolean; isRedirecting: boolean; pathname: string | null | undefined; }; /** * Full-screen branded boot loader. * - Always when auth context is missing (provider not ready). * - Always while an onboarding redirect is in flight. * - Never over marketing/auth pages during a routine session probe * (those pages should paint immediately). * - Never over unknown URLs (real 404 must paint, not workspace loader). * - On known protected student routes while auth is still resolving. */ export function shouldShowAppLoadingScreen(input: AppLoadingInput): boolean { if (!input.hasAuthContext) return true; if (input.isRedirecting) return true; if (!isProtectedAppPath(input.pathname)) return false; return input.isAuthLoading; } /** * Whether the real route tree may render under the boot guard. * Public pages and unknown URLs render even while auth is probing; * known protected routes wait. */ export function shouldRenderAppChildren(input: AppLoadingInput): boolean { if (!input.hasAuthContext) return false; if (input.isRedirecting) return false; if (!isProtectedAppPath(input.pathname)) return true; return !input.isAuthLoading; } export type OnboardingRedirectTarget = "/onboarding" | "/home" | null; export function resolveProtectedRouteRedirect(input: { hasUser: boolean; isAuthLoading: boolean; pathname: string | null | undefined; }): string | null { const path = input.pathname ?? "/"; if (input.isAuthLoading || input.hasUser) return null; if (!isProtectedAppPath(path)) return null; const candidate = path.startsWith("/") && !path.startsWith("//") ? path : "/home"; const authLoop = candidate === "/login" || candidate === "/signup" || candidate === "/logout" || candidate.startsWith("/login?") || candidate.startsWith("/signup?") || candidate.startsWith("/logout?") || candidate.startsWith("/login/") || candidate.startsWith("/signup/") || candidate.startsWith("/logout/"); const safeNext = authLoop ? "/home" : candidate; return `/login?next=${encodeURIComponent(safeNext)}`; } /** * Resolve post-auth onboarding redirects. * `onboardingComplete === null` means the async check is still pending. */ export function resolveOnboardingRedirect(input: { hasUser: boolean; isAuthLoading: boolean; pathname: string | null | undefined; onboardingComplete: boolean | null; }): OnboardingRedirectTarget { if (input.isAuthLoading || !input.hasUser) return null; if (input.onboardingComplete === null) return null; const path = input.pathname ?? "/"; if (!input.onboardingComplete && path !== "/onboarding" && isProtectedAppPath(path)) { return "/onboarding"; } if (input.onboardingComplete && path === "/onboarding") { return "/home"; } return null; } /** Ease fake progress toward a soft cap — never claims 100% before unmount. */ export function nextBootProgress(current: number, cap = 94): number { if (current >= cap) return cap; const step = Math.max(1, Math.round((cap - current) * 0.12)); return Math.min(cap, current + step); } export const BOOT_STATUS_MESSAGES = [ "Starting DocDoe…", "Checking your session…", "Opening your workspace…", "Almost ready…", ] as const; export function bootStatusForProgress(progress: number): string { if (progress < 28) return BOOT_STATUS_MESSAGES[0]; if (progress < 55) return BOOT_STATUS_MESSAGES[1]; if (progress < 82) return BOOT_STATUS_MESSAGES[2]; return BOOT_STATUS_MESSAGES[3]; }