diff --git a/examples/react/src/routes.ts b/examples/react/src/routes.ts index f46f7290..7d296ee5 100644 --- a/examples/react/src/routes.ts +++ b/examples/react/src/routes.ts @@ -1,6 +1,7 @@ import SignInAuthScreenPage from "./screens/sign-in-auth-screen"; import SignInAuthScreenWithHandlersPage from "./screens/sign-in-auth-screen-w-handlers"; import SignInAuthScreenWithOAuthPage from "./screens/sign-in-auth-screen-w-oauth"; +import SignInAuthScreenProviderGuidancePage from "./screens/sign-in-auth-screen-provider-guidance"; import SignUpAuthScreenPage from "./screens/sign-up-auth-screen"; import SignUpAuthScreenWithHandlersPage from "./screens/sign-up-auth-screen-w-handlers"; import SignUpAuthScreenWithOAuthPage from "./screens/sign-up-auth-screen-w-oauth"; @@ -12,6 +13,7 @@ import CustomAuthScreenPage from "./screens/custom-auth-screen"; import PhoneAuthScreenPage from "./screens/phone-auth-screen"; import PhoneAuthScreenWithOAuthPage from "./screens/phone-auth-screen-w-oauth"; import MultiFactorAuthEnrollmentScreenPage from "./screens/mfa-enrollment-screen"; +import SignUpAuthScreenProviderGuidancePage from "./screens/sign-up-auth-screen-provider-guidance"; export const routes = [ { @@ -32,6 +34,19 @@ export const routes = [ path: "/screens/sign-in-auth-screen-w-oauth", component: SignInAuthScreenWithOAuthPage, }, + { + name: "Sign In Screen (provider guidance)", + description: + "Sign-in with OAuth; shows a custom message when email/password is used for an OAuth-only account. Requires email enumeration protection disabled.", + path: "/screens/sign-in-auth-screen-provider-guidance", + component: SignInAuthScreenProviderGuidancePage, + }, + { + name: "Sign Up Screen (provider guidance)", + description: "Sign up with provider then log to database for provider guidance error messaging", + path: "/screens/sign-up-auth-screen-provider-guidance", + component: SignUpAuthScreenProviderGuidancePage, + }, { name: "Sign Up Screen", description: "A sign up screen with email and password.", diff --git a/examples/react/src/screens/sign-in-auth-screen-provider-guidance.tsx b/examples/react/src/screens/sign-in-auth-screen-provider-guidance.tsx new file mode 100644 index 00000000..a6d162a9 --- /dev/null +++ b/examples/react/src/screens/sign-in-auth-screen-provider-guidance.tsx @@ -0,0 +1,163 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + */ + +import { useState } from "react"; +import { + AppleSignInButton, + Card, + CardContent, + CardHeader, + CardSubtitle, + CardTitle, + Divider, + GoogleSignInButton, + FacebookSignInButton, + GitHubSignInButton, + MicrosoftSignInButton, + TwitterSignInButton, + YahooSignInButton, + useUI, + RedirectError, +} from "@firebase-oss/ui-react"; + +import { signInWithEmailAndPassword, getTranslation, FirebaseUIError } from "@firebase-oss/ui-core"; + +import { getDatabase, ref, get } from "firebase/database"; +import { useNavigate } from "react-router"; + +const PROVIDER_MISMATCH_MESSAGE = + "This account may have been created using a different sign-in method. Try signing in with another method or reset your password."; + +export default function SignInAuthScreenProviderGuidancePage() { + const navigate = useNavigate(); + const ui = useUI(); + const db = getDatabase(); + + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(null); + const [submitting, setSubmitting] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(null); + setSubmitting(true); + + try { + // Attempt login first + await signInWithEmailAndPassword(ui, email, password); + navigate("/"); // success + } catch (err) { + if (err instanceof FirebaseUIError) { + // Only show provider guidance if password is wrong + // This is the error you will need to catch and handle in your app + if (err.code === "auth/wrong-password") { + try { + // Normalize email for DB lookup + const safeEmail = email.trim().toLowerCase().replace(/\./g, ","); + const snapshot = await get(ref(db, `usersByEmail/${safeEmail}`)); + + if (snapshot.exists()) { + const provider = snapshot.val().provider; + if (provider !== "password") { + setError(PROVIDER_MISMATCH_MESSAGE); + return; // stop here + } + } + } catch (dbErr) { + setError("Error getting user by email: " + String(dbErr)); + } + } + + // If not wrong-password or no provider guidance, show original error + setError( + err && typeof err === "object" && typeof (err as { message?: unknown }).message === "string" + ? (err as { message: string }).message + : "An unknown error occurred." + ); + } + setSubmitting(false); + } + }; + + const signInLabel = getTranslation(ui, "labels", "signIn"); + const signInSubtitle = getTranslation(ui, "prompts", "signInToAccount"); + const emailLabel = getTranslation(ui, "labels", "emailAddress"); + const passwordLabel = getTranslation(ui, "labels", "password"); + const dividerLabel = getTranslation(ui, "messages", "dividerOr"); + + return ( +
+
+ + + {signInLabel} + {signInSubtitle} + + + +
+
+ + setEmail(e.target.value)} + required + autoComplete="email" + /> +
+ +
+ + setPassword(e.target.value)} + required + autoComplete="current-password" + /> +
+ + {error && ( +
+ {error} +
+ )} + +
+ +
+
+ + {dividerLabel} + +
+ + + + + + + + +
+
+
+
+
+ ); +} diff --git a/examples/react/src/screens/sign-up-auth-screen-provider-guidance.tsx b/examples/react/src/screens/sign-up-auth-screen-provider-guidance.tsx new file mode 100644 index 00000000..ac213a4d --- /dev/null +++ b/examples/react/src/screens/sign-up-auth-screen-provider-guidance.tsx @@ -0,0 +1,52 @@ +import { useNavigate } from "react-router"; +import { + SignUpAuthScreen, + GoogleSignInButton, + FacebookSignInButton, + AppleSignInButton, + GitHubSignInButton, + MicrosoftSignInButton, + TwitterSignInButton, + YahooSignInButton, +} from "@firebase-oss/ui-react"; + +import { getDatabase, ref, set } from "firebase/database"; + +export default function SignUpAuthScreenProviderGuidancePage() { + const navigate = useNavigate(); + const db = getDatabase(); + + return ( + { + if (!user?.email) return; + const safeEmail = user.email.replace(/\./g, ","); + await set(ref(db, `usersByEmail/${safeEmail}`), { + provider: user.providerData[0]?.providerId, + email: user.email, + uid: user.uid, + }); + // RTDB rules for usersByEmail + // { + // "rules": { + // "usersByEmail": { + // "$email": { + // ".read": true, // anyone can check provider + // ".write": "auth != null" // only logged-in users can write + // } + // } + // } + // } + navigate("/"); + }} + > + + + + + + + + + ); +}