File size: 6,468 Bytes
65d68f5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
/**
 * 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];
}