From 0f3ee5d53718f682352b9f46dadc96961c5109de Mon Sep 17 00:00:00 2001 From: Nik Afiq Date: Wed, 10 Dec 2025 21:17:48 +0900 Subject: [PATCH] feat(auth): enhance Google sign-in flow with loading state and error handling --- .env.example | 3 --- frontend/src/auth/AuthProvider.tsx | 31 +++++++++++++++++++++----- frontend/src/components/AuthStatus.tsx | 6 ++--- 3 files changed, 29 insertions(+), 11 deletions(-) diff --git a/.env.example b/.env.example index 0ad2ac3..2b902bb 100644 --- a/.env.example +++ b/.env.example @@ -27,9 +27,6 @@ ADDR=:8082 AUTH_ENABLED=false FIREBASE_PROJECT_ID= FIREBASE_CREDENTIALS_FILE=/secrets/firebase_credentials.json -# Use one of the following to supply Firebase service account: -# FIREBASE_CREDENTIALS_JSON= -# FIREBASE_CREDENTIALS_FILE=/path/to/firebase.json # Frontend Firebase (for Google sign-in) VITE_AUTH_ENABLED=true diff --git a/frontend/src/auth/AuthProvider.tsx b/frontend/src/auth/AuthProvider.tsx index ba08b6f..a059203 100644 --- a/frontend/src/auth/AuthProvider.tsx +++ b/frontend/src/auth/AuthProvider.tsx @@ -14,6 +14,7 @@ type AuthContextShape = { idToken: string | null; backendClaims: FirebaseAuthResponse | null; verifying: boolean; + signingIn: boolean; error: string | null; signInWithGoogle: () => Promise; signOut: () => Promise; @@ -29,6 +30,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { const [idToken, setIdToken] = React.useState(null); const [backendClaims, setBackendClaims] = React.useState(null); const [verifying, setVerifying] = React.useState(false); + const [signingIn, setSigningIn] = React.useState(false); const [error, setError] = React.useState(null); // Subscribe to Firebase auth state @@ -67,10 +69,28 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { setError("Auth disabled"); return; } - const app = getFirebaseApp(); - const auth = getAuth(app); - const provider = new GoogleAuthProvider(); - await signInWithPopup(auth, provider); + setError(null); + setSigningIn(true); + try { + const app = getFirebaseApp(); + const auth = getAuth(app); + const provider = new GoogleAuthProvider(); + try { + await signInWithPopup(auth, provider); + } catch (err: unknown) { + // Fallback to redirect when popups are blocked. + const code = (err as { code?: string }).code || ""; + if (code === "auth/popup-blocked" || code === "auth/operation-not-supported-in-this-environment") { + await import("firebase/auth").then(({ signInWithRedirect }) => signInWithRedirect(auth, provider)); + } else { + const msg = err instanceof Error ? err.message : "Sign-in failed"; + setError(msg); + throw err; + } + } + } finally { + setSigningIn(false); + } }, [enabled]); const signOut = React.useCallback(async () => { @@ -106,11 +126,12 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { idToken, backendClaims, verifying, + signingIn, error, signInWithGoogle, signOut, refreshToken, - }), [enabled, status, user, idToken, backendClaims, verifying, error, signInWithGoogle, signOut, refreshToken]); + }), [enabled, status, user, idToken, backendClaims, verifying, signingIn, error, signInWithGoogle, signOut, refreshToken]); return ( diff --git a/frontend/src/components/AuthStatus.tsx b/frontend/src/components/AuthStatus.tsx index 12846ca..7c8212d 100644 --- a/frontend/src/components/AuthStatus.tsx +++ b/frontend/src/components/AuthStatus.tsx @@ -1,7 +1,7 @@ import { useAuth } from "../auth/AuthProvider"; export default function AuthStatus() { - const { enabled, status, user, backendClaims, verifying, signInWithGoogle, signOut, error } = useAuth(); + const { enabled, status, user, backendClaims, verifying, signingIn, signInWithGoogle, signOut, error } = useAuth(); if (!enabled) { return
Auth off
; @@ -11,9 +11,9 @@ export default function AuthStatus() { } if (!user) { return ( - ); }