feat(auth): enhance Google sign-in flow with loading state and error handling
This commit is contained in:
parent
008f8a3cca
commit
0f3ee5d537
@ -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
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
const app = getFirebaseApp();
|
setError(null);
|
||||||
const auth = getAuth(app);
|
setSigningIn(true);
|
||||||
const provider = new GoogleAuthProvider();
|
try {
|
||||||
await signInWithPopup(auth, provider);
|
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]);
|
}, [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}>
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user