Skip to content
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
0aeb8d8
feat: inital implementation for setting up fetchsigninwithemail behav…
russellwheatley Mar 23, 2026
bf8c271
test: test the new legacySignInWithEmail behaviour
russellwheatley Mar 23, 2026
8af2a9f
chore: pnpm-lock
russellwheatley Mar 23, 2026
b4be9b4
test: legacy-sign-in component
russellwheatley Mar 23, 2026
4c27682
docs: update README with legacyFetchSignInWithEmail behaviour
russellwheatley Mar 23, 2026
0e6bed5
chore: remove dev dependency from example
russellwheatley Mar 23, 2026
5977d79
feat: increase errors that trigger recovery flow
russellwheatley Mar 23, 2026
65004db
chore: update react example app with latest flow
russellwheatley Mar 23, 2026
c171e76
chore: update base css styles
russellwheatley Mar 23, 2026
b35738d
test: numerous tests for updates to recovery flow
russellwheatley Mar 23, 2026
e9d33c2
docs: recovery flow is now a modal
russellwheatley Mar 23, 2026
d64a8cd
Merge branch 'main' into fetchsigninwithemail
russellwheatley Mar 26, 2026
3d62f32
chore: format
russellwheatley Mar 26, 2026
36799f1
chore: format
russellwheatley Apr 2, 2026
b09ce8d
chore: format
russellwheatley Apr 2, 2026
13d3183
chore: update license years to 2026
russellwheatley Apr 2, 2026
0bd5bff
fix: clear legacy sign-in recovery only on success, not on attempt start
russellwheatley Jul 15, 2026
80ee026
refactor: share legacy sign-in recovery error codes between errors.ts…
russellwheatley Jul 15, 2026
f91546c
Merge remote-tracking branch 'origin/main' into fetchsigninwithemail
russellwheatley Jul 15, 2026
4206ba2
refactor(angular): remove redundant LegacySignInRecovery type + cast …
russellwheatley Jul 15, 2026
ceb9ad4
fix(auth): preserve OAuth credential for legacy recovery
russellwheatley Jul 16, 2026
49aa6b4
fix(core): stop opening legacy sign-in recovery UI with nothing actio…
russellwheatley Jul 16, 2026
a5b9c11
refactor(core): extract shared PENDING_CREDENTIAL_STORAGE_KEY constant
russellwheatley Jul 16, 2026
8b9e754
docs: document Email Enumeration Protection trade-off for legacyFetch…
russellwheatley Jul 16, 2026
8663dea
fix(react,angular): never re-offer the just-attempted provider in rec…
russellwheatley Jul 16, 2026
a693f2f
docs(core): document plaintext sessionStorage trade-off for pending c…
russellwheatley Jul 16, 2026
221e16c
test(core): add regression coverage for silent error-swallowing in au…
russellwheatley Jul 16, 2026
9ef456b
fix(react,angular): disable legacy sign-in recovery UI by default
russellwheatley Jul 17, 2026
21714c7
docs(core): expand legacyFetchSignInWithEmail JSDoc with migration gu…
russellwheatley Jul 17, 2026
76eee01
docs(examples): clarify email enumeration protection requirement in l…
russellwheatley Jul 17, 2026
5252084
docs(examples): align legacy recovery demo copy with email enumeratio…
russellwheatley Jul 17, 2026
6fba47f
Merge branch 'main' into fetchsigninwithemail
russellwheatley Jul 20, 2026
051be47
fix(core): clear pending OAuth credential from sessionStorage on reco…
russellwheatley Jul 22, 2026
3afc747
fix(core): guard against stale legacyFetchSignInWithEmail fetch races
russellwheatley Jul 22, 2026
e54e457
fix(i18n): extract hardcoded 'Account Found' recovery label into tran…
russellwheatley Jul 22, 2026
40f493b
test(e2e): assert pendingCred is cleared on custom recovery dismiss
russellwheatley Jul 22, 2026
3d60bd9
fix(core): read pending credential before clearing legacy recovery state
russellwheatley Jul 23, 2026
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
89 changes: 89 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,57 @@ const ui = initializeUI({
});
```

#### `legacyFetchSignInWithEmail`

The `legacyFetchSignInWithEmail` behavior augments OAuth `auth/account-exists-with-different-credential` flows by calling `fetchSignInMethodsForEmail(auth, email)` and storing the returned methods on the UI instance. In the packaged React and Angular screen components, this recovery state is rendered automatically as a modal on `SignInAuthScreen` and `OAuthScreen`.

The original pending credential is still preserved, so after the user signs in with the correct method, Firebase UI can continue the existing linking flow.

```ts
import { legacyFetchSignInWithEmail } from '@firebase-oss/ui-core';

const ui = initializeUI({
app,
behaviors: [legacyFetchSignInWithEmail()],
});
```

If you want full control over the UI, hide the built-in recovery component on the screen and read the recovery state directly with `useLegacySignInRecovery()`:

```tsx
import { GitHubSignInButton, GoogleSignInButton, SignInAuthScreen, useLegacySignInRecovery } from '@firebase-oss/ui-react';

function WrongProviderRecovery() {
const { recovery, clearRecovery } = useLegacySignInRecovery();

if (!recovery) {
return null;
}

return (
<div>
<p>You have previously signed in with a different method for {recovery.email}.</p>
{recovery.signInMethods.includes('google.com') && (
<GoogleSignInButton onSignIn={clearRecovery} />
)}
{recovery.signInMethods.includes('github.com') && (
<GitHubSignInButton onSignIn={clearRecovery} />
)}
</div>
);
}

export function CustomSignInScreen() {
return (
<SignInAuthScreen showLegacySignInRecovery={false}>
<WrongProviderRecovery />
</SignInAuthScreen>
);
}
```

Angular apps can hide the built-in recovery UI with `showLegacySignInRecovery="false"` and read the same state with `injectLegacySignInRecovery()` / `injectClearLegacySignInRecovery()`.

#### `oneTapSignIn`

The `oneTapSignIn` behavior triggers the [Google One Tap](https://developers.google.com/identity/gsi/web/guides/features) experience to render.
Expand Down Expand Up @@ -1061,6 +1112,7 @@ By default, any missing translations will fallback to English if not specified.
| onSignIn | `(user: User) => void?` | Callback when sign-in succeeds |
| onForgotPasswordClick | `() => void?` | Callback when forgot password link is clicked |
| onSignUpClick | `() => void?` | Callback when sign-up link is clicked |
| showLegacySignInRecovery | `boolean?` | Whether to show the built-in legacy sign-in recovery UI |

**`SignUpAuthScreen`**

Expand Down Expand Up @@ -1113,6 +1165,7 @@ By default, any missing translations will fallback to English if not specified.
|------|:----:|-------------|
| onSignIn | `(user: User) => void?` | Callback when sign-in succeeds |
| children | `React.ReactNode?` | Child components |
| showLegacySignInRecovery | `boolean?` | Whether to show the built-in legacy sign-in recovery UI |

**`OAuthButton`**

Expand Down Expand Up @@ -1189,6 +1242,10 @@ By default, any missing translations will fallback to English if not specified.
| asChild | `boolean?` | Render as child component using Slot |
| ...props | `ComponentProps<"button">` | Standard button HTML attributes |

**`LegacySignInRecovery`**

Default component for displaying suggested previous sign-in methods from `legacyFetchSignInWithEmail`.

**`Card`**

Card container component.
Expand Down Expand Up @@ -1251,6 +1308,12 @@ By default, any missing translations will fallback to English if not specified.

Returns `string | undefined`.

**`useLegacySignInRecovery`**

Gets the legacy sign-in recovery state populated by `legacyFetchSignInWithEmail`.

Returns `{ recovery: LegacySignInRecovery | undefined; clearRecovery: () => void }`.

**`useSignInAuthFormSchema`**

Creates a Zod schema for sign-in form validation.
Expand Down Expand Up @@ -1700,6 +1763,10 @@ By default, any missing translations will fallback to English if not specified.

Screen component for email/password sign-in.

| Input | Type | Description |
|-------|:----:|-------------|
| showLegacySignInRecovery | `boolean` | Whether to show the built-in legacy sign-in recovery UI |

| Output | Type | Description |
|--------|:----:|-------------|
| signIn | `EventEmitter<User>` | Emitted when sign-in succeeds |
Expand Down Expand Up @@ -1754,6 +1821,10 @@ By default, any missing translations will fallback to English if not specified.

Screen component for OAuth provider sign-in.

| Input | Type | Description |
|-------|:----:|-------------|
| showLegacySignInRecovery | `boolean` | Whether to show the built-in legacy sign-in recovery UI |

| Output | Type | Description |
|--------|:----:|-------------|
| onSignIn | `EventEmitter<User>` | Emitted when OAuth sign-in succeeds |
Expand Down Expand Up @@ -1904,6 +1975,12 @@ By default, any missing translations will fallback to English if not specified.

Component that displays redirect errors from Firebase UI authentication flow.

**`LegacySignInRecoveryComponent`**

Selector: `fui-legacy-sign-in-recovery`

Default component for displaying suggested previous sign-in methods from `legacyFetchSignInWithEmail`.

**`ContentComponent`**

Selector: `fui-content`
Expand All @@ -1922,6 +1999,18 @@ By default, any missing translations will fallback to English if not specified.

Returns `Signal<string \| undefined>`.

**`injectLegacySignInRecovery`**

Injects the legacy sign-in recovery state from the UI store as a signal.

Returns `Signal<LegacySignInRecovery \| undefined>`.

**`injectClearLegacySignInRecovery`**

Injects a callback that clears the current legacy sign-in recovery state.

Returns `() => void`.

**`injectTranslation`**

Injects a translated string for a given category and key.
Expand Down
3 changes: 2 additions & 1 deletion examples/react/src/firebase/firebase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

"use client";

import { countryCodes, initializeUI, oneTapSignIn } from "@firebase-oss/ui-core";
import { countryCodes, initializeUI, legacyFetchSignInWithEmail, oneTapSignIn } from "@firebase-oss/ui-core";
import { getApps, initializeApp } from "firebase/app";
import { connectAuthEmulator, getAuth } from "firebase/auth";

Expand All @@ -30,6 +30,7 @@ export const ui = initializeUI({
app: firebaseApp,
behaviors: [
// autoAnonymousLogin(),
legacyFetchSignInWithEmail(),
oneTapSignIn({
clientId: "616577669988-led6l3rqek9ckn9t1unj4l8l67070fhp.apps.googleusercontent.com",
}),
Expand Down
7 changes: 7 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 LegacyRecoveryDemoPage from "./screens/legacy-recovery-demo";
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 Down Expand Up @@ -32,6 +33,12 @@ export const routes = [
path: "/screens/sign-in-auth-screen-w-oauth",
component: SignInAuthScreenWithOAuthPage,
},
{
name: "Legacy Recovery Demo",
description: "Use this screen to test wrong-provider recovery for email/password and OAuth attempts.",
Comment thread
russellwheatley marked this conversation as resolved.
Outdated
path: "/screens/legacy-recovery-demo",
component: LegacyRecoveryDemoPage,
},
{
name: "Sign Up Screen",
description: "A sign up screen with email and password.",
Expand Down
60 changes: 60 additions & 0 deletions examples/react/src/screens/legacy-recovery-demo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* Copyright 2025 Google LLC
Comment thread
russellwheatley marked this conversation as resolved.
Outdated
*
* 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 {
AppleSignInButton,
FacebookSignInButton,
GitHubSignInButton,
GoogleSignInButton,
MicrosoftSignInButton,
SignInAuthScreen,
TwitterSignInButton,
YahooSignInButton,
} from "@firebase-oss/ui-react";
import { useNavigate } from "react-router";

export default function LegacyRecoveryDemoPage() {
const navigate = useNavigate();

return (
<div className="space-y-6">
<div className="max-w-sm mx-auto pt-10 text-sm text-gray-700 dark:text-gray-300 space-y-3">
<p className="font-medium text-base text-black dark:text-white">Legacy recovery demo</p>
<p>Use this screen to test wrong-provider recovery with both email/password and OAuth attempts.</p>
<p>
Suggested flow: create an account with Google first, sign out, then come back here and try the same email with
with email/password or another provider like GitHub.
</p>
</div>

<SignInAuthScreen
onSignIn={() => {
navigate("/");
}}
>
<div className="space-y-2">
<GoogleSignInButton />
<FacebookSignInButton />
<AppleSignInButton />
<GitHubSignInButton />
<MicrosoftSignInButton />
<TwitterSignInButton />
<YahooSignInButton />
</div>
</SignInAuthScreen>
</div>
);
}
Loading
Loading