"use client"; import Link from "next/link"; import { useEffect, useState } from "react"; import { ArrowRight, CheckCircle, LoaderCircle, XCircle } from "lucide-react"; import { getAuthToken, getStoredUser, setAuthSession, startGoogleSignIn, } from "@/lib/auth/auth-client"; import type { AuthTokenResponse } from "@/lib/api/types"; import { hydratePendingProductIntentAfterAuthBounded } from "@/lib/onboarding-intent"; function readSafeNextPath(value: string | null): string { if (!value || !value.startsWith("/") || value.startsWith("//")) return "/study-chat"; return value; } function userSafeGoogleError(rawError: string | null): string { if (!rawError) return "Could not finish Google sign-in right now."; const normalized = rawError.toLowerCase(); if (normalized.includes("cancel")) return "Google sign-in was cancelled."; if (normalized.includes("not ready")) return "Google sign-in is not set up yet. Contact support."; if (normalized.includes("expired") || normalized.includes("already used")) { return "That Google sign-in attempt expired. Start a fresh sign-in to continue."; } return "Could not finish Google sign-in right now."; } type Status = | { kind: "loading" } | { kind: "redirecting" } | { kind: "error"; message: string; nextPath: string }; /** Parse the OAuth fragment after hydration so server and first-client markup match. */ export function GoogleCallbackClient() { const [status, setStatus] = useState({ kind: "loading" }); useEffect(() => { const params = new URLSearchParams(window.location.hash.replace(/^#/, "")); const requestedNextPath = readSafeNextPath(params.get("next")); window.history.replaceState(null, "", "/auth/google/callback"); let nextStatus: Status; let shouldRedirect = false; const callbackError = params.get("error"); const accessToken = params.get("access_token"); const expiresIn = Number(params.get("expires_in")); const userValue = params.get("user"); if (callbackError) { nextStatus = { kind: "error", message: userSafeGoogleError(callbackError), nextPath: requestedNextPath, }; } else if (!accessToken || !userValue || !Number.isFinite(expiresIn) || expiresIn <= 0) { // The callback fragment is intentionally removed after it is consumed. // If the student refreshes or revisits this URL after a successful // exchange, resume the already-valid account instead of showing a false // sign-in failure. if (getAuthToken() && getStoredUser()) { nextStatus = { kind: "redirecting" }; shouldRedirect = true; } else { nextStatus = { kind: "error", message: "Could not finish Google sign-in right now.", nextPath: requestedNextPath, }; } } else { try { const parsedSession: AuthTokenResponse = { access_token: accessToken, token_type: params.get("token_type") ?? "bearer", expires_in: expiresIn, user: JSON.parse(userValue) as AuthTokenResponse["user"], }; setAuthSession(parsedSession); nextStatus = { kind: "redirecting" }; shouldRedirect = true; } catch { nextStatus = { kind: "error", message: "Session data was malformed. Please try again.", nextPath: requestedNextPath, }; } } // eslint-disable-next-line react-hooks/set-state-in-effect -- URL hash parsing must happen after hydration. setStatus(nextStatus); if (shouldRedirect) { void hydratePendingProductIntentAfterAuthBounded().then((handoff) => { // RootLoadingGuard evaluates the freshly persisted account and owns // the onboarding and session decisions after the full navigation. // The backend just minted this OAuth token, so holding the callback // behind another /auth/session request only turns a cold or transient // API response into a false sign-in failure. const nextPath = handoff.redirectTo ?? requestedNextPath; // OAuth changes the authentication identity outside React context. // A full same-origin navigation guarantees every provider and route // guard boots from the newly persisted account instead of retaining a // stale guest state through a client transition. window.location.replace(nextPath); }).catch(() => { // Product-intent hydration is optional post-auth work. It must never // turn a successfully issued account session into a login-loop screen. window.location.replace(requestedNextPath); }); } }, []); if (status.kind === "error") { return (

Sign-in needs another try

{status.message}

Use another sign-in method
); } if (status.kind === "redirecting") { return (

Welcome to DocDoe

Taking you to your study workspace…

); } return (
); }