diff --git a/MIGRATION.md b/MIGRATION.md index 021ad9065..8834296b1 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -375,3 +375,120 @@ const ui = initializeUI({ **Note:** If a merge conflict occurs and the linking fails (e.g., due to account linking restrictions), Firebase Auth will throw an error that you can handle in your error handling logic. The `onUpgrade` callback will only be called if the upgrade is successful. +--- + +## Handling sign-in provider mismatch: `fetchSignInMethodsForEmail` deprecation + +### Background + +A common UX pain point occurs when a user: + +1. Signs in for the first time with an OAuth provider (e.g. Google) +2. Signs out and later returns to the app +3. Mistakenly tries to sign in with their email address and a password + +Firebase Auth returns a generic `auth/invalid-credential` error (or the legacy `auth/wrong-password`). Without additional context, the user has no idea they already have an account linked to a different provider. + +**In v6**, FirebaseUI worked around this by calling `fetchSignInMethodsForEmail()` behind the scenes. When a credential error occurred, it fetched the providers for that email and presented the user with the appropriate sign-in method. + +**In v7**, `fetchSignInMethodsForEmail()` is no longer called when [email enumeration protection](https://docs.cloud.google.com/identity-platform/docs/admin/email-enumeration-protection) is enabled. Firebase projects now have email enumeration enabled by default, which causes calls to `fetchSignInMethodsForEmail()` to fail. + +It is still possible to switch email enumeration protection off, and we are working on a feature for allowing `fetchSignInMethodsForEmail()` via `legacyFetchSignInWithEmail()` behavior which you can [track here](https://github.com/firebase/firebaseui-web/pull/1343). + +### The problem with the deprecated approach + +```ts +// ❌ Does not work when email enumeration protection is enabled +import { fetchSignInMethodsForEmail } from "firebase/auth"; + +const methods = await fetchSignInMethodsForEmail(auth, email); +// e.g. ["google.com"] — tells an attacker that this email exists in your app +``` + +This API leaks the existence of accounts to anyone who can call your Firebase project, which is a security risk. Firebase has disabled it by default in new projects and it will eventually be removed entirely. + +### The recommended approach: track providers yourself + +Because `fetchSignInMethodsForEmail()` is gone, **you are responsible for tracking which sign-in provider a user has used** and surfacing that information when a credential error occurs. + +The example screens `sign-in-with-provider-tracking` and `provider-hint` (included in both the React and Angular examples in this repository) demonstrate one way to implement this pattern. + +#### How the demo works + +1. **Track on sign-in** — When a user successfully authenticates via an OAuth button, the app stores their email and provider ID in `localStorage`: + +```ts +function storeProvider(email: string, providerId: string): void { + const existing = JSON.parse(localStorage.getItem("fui_provider_hint") ?? "{}"); + const providers: string[] = existing.email === email ? [...existing.providers] : []; + if (!providers.includes(providerId)) providers.push(providerId); + localStorage.setItem("fui_provider_hint", JSON.stringify({ email, providers })); +} +``` + +2. **Intercept credential errors** — On email + password sign-in failure, check the stored hint before showing a generic error: + +```ts +try { + await signInWithEmailAndPassword(auth, email, password); +} catch (err) { + const code = (err as AuthError).code; + const isCredentialError = + code === "auth/invalid-credential" || code === "auth/wrong-password" || code === "auth/invalid-password"; + + if (isCredentialError) { + const knownProviders = getKnownProviders(email); // reads localStorage + if (knownProviders.length > 0) { + // Navigate to a screen that shows only the correct OAuth button + navigate("/provider-hint"); + return; + } + } + // show generic error +} +``` + +3. **Show the correct provider** — The `provider-hint` screen reads the stored data and renders only the OAuth button(s) the user originally signed in with, along with a human-friendly explanation. + +#### Why localStorage for this demo? + +`localStorage` is used here purely for ease of demonstration. It requires no backend and makes the flow visible and debuggable. + +#### A more secure production approach + +`localStorage` is accessible to any JavaScript running on the page. If an XSS vulnerability exists, an attacker could read or overwrite the stored provider hint. For a production application, consider these alternatives: + +**Option 1 — HttpOnly encrypted cookie (recommended for server-rendered apps)** + +Store the provider hint in an `HttpOnly` cookie from your server after a successful sign-in. Because `HttpOnly` cookies are not accessible to JavaScript, they are immune to XSS attacks: + +``` +Set-Cookie: fui_provider_hint=; HttpOnly; Secure; SameSite=Lax; Path=/ +``` + +The encrypted payload should contain the email (or a hashed/obfuscated identifier) and the provider ID. Encrypt the value using a server-side key (e.g. AES-GCM) so that neither the email address nor the provider information is readable by the client even if the cookie value is somehow observed. + +When a credential error occurs on the client, make a server-side request to look up the provider hint. Return only enough information to drive the UI (e.g. which button to show) — never return the raw email or provider list to an unauthenticated caller. + +**Option 2 — Hashed identifier in localStorage** + +If a purely client-side solution is required, avoid storing the plain email address. Instead store a hash: + +```ts +async function hashEmail(email: string): Promise { + const encoder = new TextEncoder(); + const data = encoder.encode(email.toLowerCase().trim()); + const hashBuffer = await crypto.subtle.digest("SHA-256", data); + return Array.from(new Uint8Array(hashBuffer)) + .map((b) => b.toString(16).padStart(2, "0")) + .join(""); +} +``` + +Store `{ emailHash, providers }` instead of `{ email, providers }`. When looking up the hint on sign-in failure, hash the email the user typed and compare against the stored hash. This way the stored data does not directly reveal which email address is associated with the provider. + +**Option 3 — Derive from existing session data** + +If your application has its own session management (e.g. a JWT issued by your backend after Firebase sign-in), you can embed the provider ID in the token claims. On subsequent visits, read the provider from the token rather than from `localStorage`. + + diff --git a/examples/angular/src/app/app.routes.server.ts b/examples/angular/src/app/app.routes.server.ts index 4d2d69b8a..79de2ec1e 100644 --- a/examples/angular/src/app/app.routes.server.ts +++ b/examples/angular/src/app/app.routes.server.ts @@ -81,6 +81,15 @@ export const serverRoutes: ServerRoute[] = [ path: "screens/mfa-enrollment-screen", renderMode: RenderMode.Client, }, + /** Provider tracking screens require browser localStorage — must be client-only */ + { + path: "screens/sign-in-with-provider-tracking", + renderMode: RenderMode.Client, + }, + { + path: "screens/provider-hint", + renderMode: RenderMode.Client, + }, /** All other routes will be rendered on the server (SSR) */ { path: "**", diff --git a/examples/angular/src/app/routes.ts b/examples/angular/src/app/routes.ts index fcafffd4a..3f8ccdcd6 100644 --- a/examples/angular/src/app/routes.ts +++ b/examples/angular/src/app/routes.ts @@ -111,6 +111,20 @@ export const routes: RouteConfig[] = [ path: "/screens/phone-auth-screen-w-oauth", loadComponent: () => import("./screens/phone-auth-screen-w-oauth").then((m) => m.PhoneAuthScreenWithOAuthComponent), }, + { + name: "Sign In with provider tracking", + description: + "Demonstrates how to redirect users to their original OAuth provider when they mistakenly try to sign in with email + password.", + path: "/screens/sign-in-with-provider-tracking", + loadComponent: () => + import("./screens/sign-in-with-provider-tracking").then((m) => m.SignInWithProviderTrackingComponent), + }, + { + name: "Provider hint", + description: "Shown when a user attempts email + password sign-in but has a known OAuth provider stored locally.", + path: "/screens/provider-hint", + loadComponent: () => import("./screens/provider-hint").then((m) => m.ProviderHintComponent), + }, ] as const; export const hiddenRoutes: RouteConfig[] = [ diff --git a/examples/angular/src/app/screens/provider-hint.ts b/examples/angular/src/app/screens/provider-hint.ts new file mode 100644 index 000000000..5188c0261 --- /dev/null +++ b/examples/angular/src/app/screens/provider-hint.ts @@ -0,0 +1,141 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Component, inject, type OnInit, signal } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { Router } from "@angular/router"; +import { + AppleSignInButtonComponent, + FacebookSignInButtonComponent, + GitHubSignInButtonComponent, + GoogleSignInButtonComponent, + MicrosoftSignInButtonComponent, + TwitterSignInButtonComponent, + YahooSignInButtonComponent, +} from "@firebase-oss/ui-angular"; +import { PROVIDER_HINT_STORAGE_KEY, type StoredProviderHint } from "./sign-in-with-provider-tracking"; + +const PROVIDER_DISPLAY_NAMES: Record = { + "google.com": "Google", + "apple.com": "Apple", + "facebook.com": "Facebook", + "github.com": "GitHub", + "microsoft.com": "Microsoft", + "twitter.com": "Twitter / X", + "yahoo.com": "Yahoo", +}; + +function getStoredHint(): StoredProviderHint | null { + try { + const raw = localStorage.getItem(PROVIDER_HINT_STORAGE_KEY); + return raw ? (JSON.parse(raw) as StoredProviderHint) : null; + } catch { + return null; + } +} + +@Component({ + selector: "app-provider-hint", + standalone: true, + imports: [ + CommonModule, + GoogleSignInButtonComponent, + AppleSignInButtonComponent, + FacebookSignInButtonComponent, + GitHubSignInButtonComponent, + MicrosoftSignInButtonComponent, + TwitterSignInButtonComponent, + YahooSignInButtonComponent, + ], + template: ` + @if (hint() && hint()!.providers.length > 0) { +
+
+

+ Looks like you previously signed in with {{ providerNames() }}. +

+

+ Use the button below to sign in with the provider you used before. +

+
+ +
+ @for (providerId of hint()!.providers; track providerId) { + @switch (providerId) { + @case ("google.com") { + + } + @case ("apple.com") { + + } + @case ("facebook.com") { + + } + @case ("github.com") { + + } + @case ("microsoft.com") { + + } + @case ("twitter.com") { + + } + @case ("yahoo.com") { + + } + } + } +
+ + +
+ } @else { +
+

No provider hint found. Please sign in normally.

+ +
+ } + `, + styles: [], +}) +export class ProviderHintComponent implements OnInit { + private router = inject(Router); + + hint = signal(null); + providerNames = signal(""); + + ngOnInit(): void { + const stored = getStoredHint(); + this.hint.set(stored); + if (stored) { + this.providerNames.set(stored.providers.map((id) => PROVIDER_DISPLAY_NAMES[id] ?? id).join(" or ")); + } + } + + onSignIn(): void { + this.router.navigate(["/"]); + } + + goBack(): void { + this.router.navigate(["/screens/sign-in-with-provider-tracking"]); + } +} + +export default ProviderHintComponent; diff --git a/examples/angular/src/app/screens/sign-in-with-provider-tracking.ts b/examples/angular/src/app/screens/sign-in-with-provider-tracking.ts new file mode 100644 index 000000000..c0fc187d1 --- /dev/null +++ b/examples/angular/src/app/screens/sign-in-with-provider-tracking.ts @@ -0,0 +1,245 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * This screen demonstrates how to handle the scenario where a user previously signed in + * with an OAuth provider (e.g. Google) but later attempts to sign in with email + password. + * + * Because `fetchSignInMethodsForEmail()` is deprecated in Firebase Auth, applications must + * implement their own provider-tracking solution. This example uses localStorage to record + * which OAuth provider a user signed in with, then redirects them to the correct provider + * button when a credential error is detected. + * + * NOTE: localStorage is used here for demonstration purposes only. + * In a production application, prefer storing this information server-side or in an + * HttpOnly encrypted cookie so that provider metadata is not exposed to client-side scripts. + */ + +import { Component, inject, signal } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { ReactiveFormsModule, FormBuilder, Validators } from "@angular/forms"; +import { Router } from "@angular/router"; +import { Auth, signInWithEmailAndPassword, type AuthError, type UserCredential } from "@angular/fire/auth"; +import { + AppleSignInButtonComponent, + FacebookSignInButtonComponent, + GitHubSignInButtonComponent, + GoogleSignInButtonComponent, + MicrosoftSignInButtonComponent, + TwitterSignInButtonComponent, + YahooSignInButtonComponent, +} from "@firebase-oss/ui-angular"; + +/** localStorage key used to persist the most recent sign-in provider hint. */ +export const PROVIDER_HINT_STORAGE_KEY = "fui_provider_hint"; + +/** Shape of the data stored under PROVIDER_HINT_STORAGE_KEY. */ +export interface StoredProviderHint { + /** The email address associated with the known providers. */ + email: string; + /** Firebase provider IDs (e.g. "google.com", "github.com") the user has signed in with. */ + providers: string[]; +} + +function normalizeEmail(email: string): string { + return email.trim().toLowerCase(); +} + +function storeProvider(email: string, providerId: string): void { + try { + const normalized = normalizeEmail(email); + const raw = localStorage.getItem(PROVIDER_HINT_STORAGE_KEY); + const existing: StoredProviderHint = raw ? (JSON.parse(raw) as StoredProviderHint) : { email: "", providers: [] }; + + const providers = existing.email === normalized ? [...existing.providers] : []; + if (!providers.includes(providerId)) { + providers.push(providerId); + } + localStorage.setItem(PROVIDER_HINT_STORAGE_KEY, JSON.stringify({ email: normalized, providers })); + } catch { + // Silently ignore storage errors. + } +} + +function getKnownProviders(email: string): string[] { + try { + const normalized = normalizeEmail(email); + const raw = localStorage.getItem(PROVIDER_HINT_STORAGE_KEY); + if (!raw) return []; + const data = JSON.parse(raw) as StoredProviderHint; + return data.email === normalized ? data.providers : []; + } catch { + return []; + } +} + +function getErrorMessage(code: string): string { + switch (code) { + case "auth/user-not-found": + return "No account found with that email address."; + case "auth/too-many-requests": + return "Too many failed attempts. Please try again later."; + default: + return "Incorrect email or password."; + } +} + +@Component({ + selector: "app-sign-in-with-provider-tracking", + standalone: true, + imports: [ + CommonModule, + ReactiveFormsModule, + GoogleSignInButtonComponent, + AppleSignInButtonComponent, + FacebookSignInButtonComponent, + GitHubSignInButtonComponent, + MicrosoftSignInButtonComponent, + TwitterSignInButtonComponent, + YahooSignInButtonComponent, + ], + template: ` +
+
+

Demo

+

+ Sign in with an OAuth provider first, then sign out. Return here and try signing in with email + password to + see the provider hint flow. +

+
+ +
+
+ + +
+ +
+ + +
+ + @if (error()) { + + } + + +
+ +
+
+
+
+
+ or continue with +
+
+ +
+ + + + + + + +
+
+ `, + styles: [], +}) +export class SignInWithProviderTrackingComponent { + private router = inject(Router); + private auth = inject(Auth); + private fb = inject(FormBuilder); + + form = this.fb.group({ + email: ["", [Validators.required, Validators.email]], + password: ["", Validators.required], + }); + + error = signal(null); + loading = signal(false); + + async handleSubmit(): Promise { + if (this.form.invalid) return; + + this.error.set(null); + this.loading.set(true); + + const { email, password } = this.form.value; + + try { + await signInWithEmailAndPassword(this.auth, email!, password!); + this.router.navigate(["/"]); + } catch (err) { + const authError = err as AuthError; + + // Firebase Auth uses different error codes across SDK versions and project configurations: + // auth/invalid-login-credentials — some Identity Platform configurations + // auth/invalid-password — used in some emulator / admin SDK contexts + // All of these indicate bad credentials, so treat them the same. + const isCredentialError = + authError.code === "auth/wrong-password" || + authError.code === "auth/invalid-credential" || + authError.code === "auth/invalid-login-credentials" || + authError.code === "auth/invalid-password"; + + if (isCredentialError) { + const knownProviders = getKnownProviders(email!); + if (knownProviders.length > 0) { + this.router.navigate(["/screens/provider-hint"]); + return; + } + } + + this.error.set(getErrorMessage(authError.code)); + } finally { + this.loading.set(false); + } + } + + handleOAuthSignIn(credential: UserCredential): void { + const email = credential.user.email ?? ""; + const providerId = credential.user.providerData[0]?.providerId ?? ""; + if (email && providerId) { + storeProvider(email, providerId); + } + this.router.navigate(["/"]); + } +} + +export default SignInWithProviderTrackingComponent; diff --git a/examples/react/src/routes.ts b/examples/react/src/routes.ts index f46f72903..55f6fe785 100644 --- a/examples/react/src/routes.ts +++ b/examples/react/src/routes.ts @@ -12,6 +12,8 @@ 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 SignInWithProviderTrackingPage from "./screens/sign-in-with-provider-tracking"; +import ProviderHintPage from "./screens/provider-hint"; export const routes = [ { @@ -92,6 +94,19 @@ export const routes = [ path: "/screens/custom-auth", component: CustomAuthScreenPage, }, + { + name: "Sign In with provider tracking", + description: + "Demonstrates how to redirect users to their original OAuth provider when they mistakenly try to sign in with email + password.", + path: "/screens/sign-in-with-provider-tracking", + component: SignInWithProviderTrackingPage, + }, + { + name: "Provider hint", + description: "Shown when a user attempts email + password sign-in but has a known OAuth provider stored locally.", + path: "/screens/provider-hint", + component: ProviderHintPage, + }, ] as const; export const hiddenRoutes = [ diff --git a/examples/react/src/screens/provider-hint.tsx b/examples/react/src/screens/provider-hint.tsx new file mode 100644 index 000000000..d228d1526 --- /dev/null +++ b/examples/react/src/screens/provider-hint.tsx @@ -0,0 +1,125 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { UserCredential } from "firebase/auth"; +import { useNavigate } from "react-router"; +import { + AppleSignInButton, + FacebookSignInButton, + GitHubSignInButton, + GoogleSignInButton, + MicrosoftSignInButton, + TwitterSignInButton, + YahooSignInButton, +} from "@firebase-oss/ui-react"; +import { PROVIDER_HINT_STORAGE_KEY, type StoredProviderHint } from "./sign-in-with-provider-tracking"; + +function getStoredHint(): StoredProviderHint | null { + try { + const raw = localStorage.getItem(PROVIDER_HINT_STORAGE_KEY); + return raw ? (JSON.parse(raw) as StoredProviderHint) : null; + } catch { + return null; + } +} + +const PROVIDER_DISPLAY_NAMES: Record = { + "google.com": "Google", + "apple.com": "Apple", + "facebook.com": "Facebook", + "github.com": "GitHub", + "microsoft.com": "Microsoft", + "twitter.com": "Twitter / X", + "yahoo.com": "Yahoo", +}; + +function ProviderButton({ + providerId, + onSignIn, +}: { + providerId: string; + onSignIn: (credential: UserCredential) => void; +}) { + switch (providerId) { + case "google.com": + return ; + case "apple.com": + return ; + case "facebook.com": + return ; + case "github.com": + return ; + case "microsoft.com": + return ; + case "twitter.com": + return ; + case "yahoo.com": + return ; + default: + return null; + } +} + +export default function ProviderHintPage() { + const navigate = useNavigate(); + const hint = getStoredHint(); + + function handleSignIn() { + navigate("/"); + } + + if (!hint || hint.providers.length === 0) { + return ( +
+

No provider hint found. Please sign in normally.

+ +
+ ); + } + + const providerNames = hint.providers.map((id) => PROVIDER_DISPLAY_NAMES[id] ?? id).join(" or "); + + return ( +
+
+

+ Looks like you previously signed in with {providerNames}. +

+

+ Use the button below to sign in with the provider you used before. +

+
+ +
+ {hint.providers.map((providerId) => ( + + ))} +
+ + +
+ ); +} diff --git a/examples/react/src/screens/sign-in-with-provider-tracking.tsx b/examples/react/src/screens/sign-in-with-provider-tracking.tsx new file mode 100644 index 000000000..db7bef9ba --- /dev/null +++ b/examples/react/src/screens/sign-in-with-provider-tracking.tsx @@ -0,0 +1,230 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * This screen demonstrates how to handle the scenario where a user previously signed in + * with an OAuth provider (e.g. Google) but later attempts to sign in with email + password. + * + * Because `fetchSignInMethodsForEmail()` is deprecated in Firebase Auth, applications must + * implement their own provider-tracking solution. This example uses localStorage to record + * which OAuth provider a user signed in with, then redirects them to the correct provider + * button when a credential error is detected. + * + * NOTE: localStorage is used here for demonstration purposes only. + * In a production application, prefer storing this information server-side or in an + * HttpOnly encrypted cookie so that provider metadata is not exposed to client-side scripts. + */ + +"use client"; + +import { useState } from "react"; +import { useNavigate } from "react-router"; +import { signInWithEmailAndPassword, type AuthError, type UserCredential } from "firebase/auth"; +import { + AppleSignInButton, + FacebookSignInButton, + GitHubSignInButton, + GoogleSignInButton, + MicrosoftSignInButton, + TwitterSignInButton, + YahooSignInButton, +} from "@firebase-oss/ui-react"; +import { auth } from "../firebase/firebase"; + +/** localStorage key used to persist the most recent sign-in provider hint. */ +export const PROVIDER_HINT_STORAGE_KEY = "fui_provider_hint"; + +/** Shape of the data stored under PROVIDER_HINT_STORAGE_KEY. */ +export interface StoredProviderHint { + /** The email address associated with the known providers. */ + email: string; + /** Firebase provider IDs (e.g. "google.com", "github.com") the user has signed in with. */ + providers: string[]; +} + +function normalizeEmail(email: string): string { + return email.trim().toLowerCase(); +} + +function storeProvider(email: string, providerId: string): void { + try { + const normalized = normalizeEmail(email); + const raw = localStorage.getItem(PROVIDER_HINT_STORAGE_KEY); + const existing: StoredProviderHint = raw ? (JSON.parse(raw) as StoredProviderHint) : { email: "", providers: [] }; + + const providers = existing.email === normalized ? [...existing.providers] : []; + if (!providers.includes(providerId)) { + providers.push(providerId); + } + localStorage.setItem(PROVIDER_HINT_STORAGE_KEY, JSON.stringify({ email: normalized, providers })); + } catch { + // Silently ignore storage errors. + } +} + +function getKnownProviders(email: string): string[] { + try { + const normalized = normalizeEmail(email); + const raw = localStorage.getItem(PROVIDER_HINT_STORAGE_KEY); + if (!raw) return []; + const data = JSON.parse(raw) as StoredProviderHint; + return data.email === normalized ? data.providers : []; + } catch { + return []; + } +} + +function getErrorMessage(code: string): string { + switch (code) { + case "auth/user-not-found": + return "No account found with that email address."; + case "auth/too-many-requests": + return "Too many failed attempts. Please try again later."; + default: + return "Incorrect email or password."; + } +} + +export default function SignInWithProviderTrackingPage() { + const navigate = useNavigate(); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setError(null); + setLoading(true); + + try { + await signInWithEmailAndPassword(auth, email, password); + navigate("/"); + } catch (err) { + const authError = err as AuthError; + + // Firebase Auth uses different error codes across SDK versions and project configurations: + // auth/wrong-password — Firebase Auth v9 legacy + // auth/invalid-credential — Firebase Auth v10+ (email+password bad credentials) + // auth/invalid-login-credentials — some Identity Platform configurations + // auth/invalid-password — used in some emulator / admin SDK contexts + // All of these indicate bad credentials, so treat them the same. + const isCredentialError = + authError.code === "auth/wrong-password" || + authError.code === "auth/invalid-credential" || + authError.code === "auth/invalid-login-credentials" || + authError.code === "auth/invalid-password"; + + if (isCredentialError) { + const knownProviders = getKnownProviders(email); + if (knownProviders.length > 0) { + navigate("/screens/provider-hint"); + return; + } + } + + setError(getErrorMessage(authError.code)); + } finally { + setLoading(false); + } + } + + function handleOAuthSignIn(credential: UserCredential): void { + const userEmail = credential.user.email ?? ""; + const providerId = credential.user.providerData[0]?.providerId ?? ""; + if (userEmail && providerId) { + storeProvider(userEmail, providerId); + } + navigate("/"); + } + + return ( +
+
+

Demo

+

+ Sign in with an OAuth provider first, then sign out. Return here and try signing in with email + password to + see the provider hint flow. +

+
+ +
+
+ + setEmail(e.target.value)} + required + autoComplete="email" + className="w-full border border-gray-300 dark:border-gray-700 rounded-md px-3 py-2 text-sm bg-transparent focus:outline-none focus:ring-2 focus:ring-blue-500" + /> +
+ +
+ + setPassword(e.target.value)} + required + autoComplete="current-password" + className="w-full border border-gray-300 dark:border-gray-700 rounded-md px-3 py-2 text-sm bg-transparent focus:outline-none focus:ring-2 focus:ring-blue-500" + /> +
+ + {error && ( +

+ {error} +

+ )} + + +
+ +
+
+
+
+
+ or continue with +
+
+ +
+ + + + + + + +
+
+ ); +}