DocDoeAI / src /lib /ui /loading-visibility.ts
asnannp's picture
deploy: sync backend to Space root (learn-lesson HF cache fix)
65d68f5
Raw History Blame Contribute Delete
6.47 kB
/**
* 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];
}