Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions examples/react/src/routes.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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 = [
{
Expand All @@ -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.",
Expand Down
163 changes: 163 additions & 0 deletions examples/react/src/screens/sign-in-auth-screen-provider-guidance.tsx
Original file line number Diff line number Diff line change
@@ -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<string | null>(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 (
<div className="space-y-6">
<div className="fui-screen">
<Card>
<CardHeader>
<CardTitle>{signInLabel}</CardTitle>
<CardSubtitle>{signInSubtitle}</CardSubtitle>
</CardHeader>

<CardContent>
<form className="fui-form" onSubmit={handleSubmit}>
<fieldset>
<label className="fui-field__label" htmlFor="provider-guidance-email">
{emailLabel}
</label>
<input
id="provider-guidance-email"
type="email"
className="fui-input"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
autoComplete="email"
/>
</fieldset>

<fieldset>
<label className="fui-field__label" htmlFor="provider-guidance-password">
{passwordLabel}
</label>
<input
id="provider-guidance-password"
type="password"
className="fui-input"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
autoComplete="current-password"
/>
</fieldset>

{error && (
<div role="alert" className="fui-form__error">
{error}
</div>
)}

<fieldset>
<button type="submit" className="fui-button fui-button--primary" disabled={submitting}>
{submitting ? "..." : signInLabel}
</button>
</fieldset>
</form>

<Divider>{dividerLabel}</Divider>

<div className="fui-screen__children space-y-2">
<GoogleSignInButton />
<FacebookSignInButton />
<AppleSignInButton />
<GitHubSignInButton />
<MicrosoftSignInButton />
<TwitterSignInButton />
<YahooSignInButton />
<RedirectError />
</div>
</CardContent>
</Card>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -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 (
<SignUpAuthScreen
onSignUp={async (user) => {
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("/");
}}
>
<GoogleSignInButton />
<FacebookSignInButton />
<AppleSignInButton />
<GitHubSignInButton />
<MicrosoftSignInButton />
<TwitterSignInButton />
<YahooSignInButton />
</SignUpAuthScreen>
);
}