feat(auth): enhance Google sign-in flow with loading state and error handling

This commit is contained in:
Nik Afiq 2025-12-10 21:17:48 +09:00
parent 008f8a3cca
commit 0f3ee5d537
3 changed files with 29 additions and 11 deletions

View File

@ -27,9 +27,6 @@ ADDR=:8082
AUTH_ENABLED=false AUTH_ENABLED=false
FIREBASE_PROJECT_ID= FIREBASE_PROJECT_ID=
FIREBASE_CREDENTIALS_FILE=/secrets/firebase_credentials.json 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) # Frontend Firebase (for Google sign-in)
VITE_AUTH_ENABLED=true VITE_AUTH_ENABLED=true

View File

@ -14,6 +14,7 @@ type AuthContextShape = {
idToken: string | null; idToken: string | null;
backendClaims: FirebaseAuthResponse | null; backendClaims: FirebaseAuthResponse | null;
verifying: boolean; verifying: boolean;
signingIn: boolean;
error: string | null; error: string | null;
signInWithGoogle: () => Promise<void>; signInWithGoogle: () => Promise<void>;
signOut: () => Promise<void>; signOut: () => Promise<void>;
@ -29,6 +30,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
const [idToken, setIdToken] = React.useState<string | null>(null); const [idToken, setIdToken] = React.useState<string | null>(null);
const [backendClaims, setBackendClaims] = React.useState<FirebaseAuthResponse | null>(null); const [backendClaims, setBackendClaims] = React.useState<FirebaseAuthResponse | null>(null);
const [verifying, setVerifying] = React.useState(false); const [verifying, setVerifying] = React.useState(false);
const [signingIn, setSigningIn] = React.useState(false);
const [error, setError] = React.useState<string | null>(null); const [error, setError] = React.useState<string | null>(null);
// Subscribe to Firebase auth state // Subscribe to Firebase auth state
@ -67,10 +69,28 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
setError("Auth disabled"); setError("Auth disabled");
return; return;
} }
setError(null);
setSigningIn(true);
try {
const app = getFirebaseApp(); const app = getFirebaseApp();
const auth = getAuth(app); const auth = getAuth(app);
const provider = new GoogleAuthProvider(); const provider = new GoogleAuthProvider();
try {
await signInWithPopup(auth, provider); 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]); }, [enabled]);
const signOut = React.useCallback(async () => { const signOut = React.useCallback(async () => {
@ -106,11 +126,12 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
idToken, idToken,
backendClaims, backendClaims,
verifying, verifying,
signingIn,
error, error,
signInWithGoogle, signInWithGoogle,
signOut, signOut,
refreshToken, refreshToken,
}), [enabled, status, user, idToken, backendClaims, verifying, error, signInWithGoogle, signOut, refreshToken]); }), [enabled, status, user, idToken, backendClaims, verifying, signingIn, error, signInWithGoogle, signOut, refreshToken]);
return ( return (
<AuthContext.Provider value={value}> <AuthContext.Provider value={value}>

View File

@ -1,7 +1,7 @@
import { useAuth } from "../auth/AuthProvider"; import { useAuth } from "../auth/AuthProvider";
export default function AuthStatus() { 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) { if (!enabled) {
return <div className="auth-chip muted">Auth off</div>; return <div className="auth-chip muted">Auth off</div>;
@ -11,9 +11,9 @@ export default function AuthStatus() {
} }
if (!user) { if (!user) {
return ( return (
<button className="auth-btn" onClick={() => signInWithGoogle().catch(() => {})}> <button className="auth-btn" onClick={() => signInWithGoogle().catch(() => {})} disabled={signingIn}>
<span className="auth-icon">G</span> <span className="auth-icon">G</span>
<span>Google </span> <span>{signingIn ? "開いています…" : "Google でサインイン"}</span>
</button> </button>
); );
} }