Download src/app/auth/google/callback/GoogleCallbackClient.tsx from asnannp/DocDoeAI: direct link, hf CLI and curl.
- Browser
- Download file 7.72 kB
-
https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/app/auth/google/callback/GoogleCallbackClient.tsx
- Command line
-
hf download hf://spaces/asnannp/DocDoeAI/src/app/auth/google/callback/GoogleCallbackClient.tsx
-
curl -L -o GoogleCallbackClient.tsx https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/app/auth/google/callback/GoogleCallbackClient.tsx
7.72 kB
| "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<Status>({ 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 ( | |
| <section className="w-full max-w-[420px] rounded-[16px] border border-[#e3e1da] bg-[#fcfcfa] p-6 text-center text-[#171716] shadow-[0_18px_50px_rgba(23,23,22,0.08)] sm:p-8"> | |
| <div className="mx-auto grid size-12 place-items-center rounded-[12px] border border-[#e2b5b2] bg-[#fff6f5] text-[#b33a3a]"> | |
| <XCircle className="size-6" strokeWidth={1.7} aria-hidden="true" /> | |
| </div> | |
| <h1 className="mt-5 text-[24px] font-semibold tracking-[-0.035em] text-[#171716]">Sign-in needs another try</h1> | |
| <p className="mt-3 text-sm leading-6 text-[#62635e]" role="alert"> | |
| {status.message} | |
| </p> | |
| <div className="mt-6 grid gap-2"> | |
| <button | |
| type="button" | |
| onClick={() => startGoogleSignIn({ nextPath: status.nextPath })} | |
| className="inline-flex min-h-11 items-center justify-center gap-2 rounded-[10px] bg-[#171716] px-5 text-sm font-semibold text-white transition hover:bg-[#30302e] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#171716] focus-visible:ring-offset-2" | |
| > | |
| Try Google again | |
| <ArrowRight className="size-4" aria-hidden="true" /> | |
| </button> | |
| <Link | |
| href="/login" | |
| className="inline-flex min-h-11 items-center justify-center rounded-[10px] px-5 text-sm font-semibold text-[#4f504c] transition hover:bg-[#f5f4f0] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#171716] focus-visible:ring-offset-2" | |
| > | |
| Use another sign-in method | |
| </Link> | |
| </div> | |
| </section> | |
| ); | |
| } | |
| if (status.kind === "redirecting") { | |
| return ( | |
| <section className="w-full max-w-[420px] rounded-[16px] border border-[#e3e1da] bg-[#fcfcfa] p-6 text-center text-[#171716] shadow-[0_18px_50px_rgba(23,23,22,0.08)] sm:p-8"> | |
| <div className="mx-auto grid size-12 place-items-center border border-[#b7cdbd] bg-[#f3f8f4] text-[#2f6240]"> | |
| <CheckCircle className="size-6" strokeWidth={1.7} aria-hidden="true" /> | |
| </div> | |
| <h1 className="mt-5 text-[24px] font-semibold tracking-[-0.035em] text-[#171716]">Welcome to DocDoe</h1> | |
| <p className="mt-2 text-[13px] leading-6 text-[#62635e]" role="status" aria-live="polite"> | |
| Taking you to your study workspace… | |
| </p> | |
| </section> | |
| ); | |
| } | |
| return ( | |
| <section className="w-full max-w-[420px] rounded-[16px] border border-[#e3e1da] bg-[#fcfcfa] p-6 text-center text-[#171716] shadow-[0_18px_50px_rgba(23,23,22,0.08)] sm:p-8"> | |
| <LoaderCircle className="mx-auto size-7 animate-spin text-[#4c4d48]" strokeWidth={1.8} aria-hidden="true" /> | |
| <h1 className="mt-5 text-[24px] font-semibold tracking-[-0.035em] text-[#171716]">Completing Google sign-in</h1> | |
| <p className="mt-2 text-[13px] leading-6 text-[#62635e]" role="status" aria-live="polite"> | |
| Connecting your Google account securely. | |
| </p> | |
| </section> | |
| ); | |
| } | |