diff --git a/packages/angular/src/lib/auth/screens/email-link-auth-screen.spec.ts b/packages/angular/src/lib/auth/screens/email-link-auth-screen.spec.ts
index 03355da3..b9e4328f 100644
--- a/packages/angular/src/lib/auth/screens/email-link-auth-screen.spec.ts
+++ b/packages/angular/src/lib/auth/screens/email-link-auth-screen.spec.ts
@@ -14,8 +14,8 @@
* limitations under the License.
*/
-import { render, screen } from "@testing-library/angular";
-import { Component } from "@angular/core";
+import { render, screen, fireEvent } from "@testing-library/angular";
+import { Component, EventEmitter } from "@angular/core";
import { EmailLinkAuthScreenComponent } from "./email-link-auth-screen";
import {
@@ -40,6 +40,21 @@ class MockEmailLinkAuthFormComponent {}
})
class MockRedirectErrorComponent {}
+@Component({
+ selector: "fui-multi-factor-auth-assertion-form",
+ template: `
+
MFA Assertion Form
+
+ `,
+ standalone: true,
+ outputs: ["onSuccess"],
+})
+class MockMultiFactorAuthAssertionFormComponent {
+ onSuccess = new EventEmitter();
+}
+
@Component({
template: `
@@ -60,7 +75,7 @@ class TestHostWithoutContentComponent {}
describe("", () => {
beforeEach(() => {
- const { injectTranslation } = require("../../../provider");
+ const { injectTranslation, injectUI } = require("../../../provider");
injectTranslation.mockImplementation((category: string, key: string) => {
const mockTranslations: Record> = {
labels: {
@@ -72,6 +87,10 @@ describe("", () => {
};
return () => mockTranslations[category]?.[key] || `${category}.${key}`;
});
+
+ injectUI.mockImplementation(() => () => ({
+ multiFactorResolver: null,
+ }));
});
it("renders with correct title and subtitle", async () => {
@@ -188,4 +207,87 @@ describe("", () => {
expect(injectTranslation).toHaveBeenCalledWith("labels", "signIn");
expect(injectTranslation).toHaveBeenCalledWith("prompts", "signInToAccount");
});
+
+ it("renders MFA assertion form when MFA resolver is present", async () => {
+ const { injectUI } = require("../../../provider");
+ injectUI.mockImplementation(() => () => ({
+ multiFactorResolver: { auth: {}, session: null, hints: [] },
+ }));
+
+ const { container } = await render(TestHostWithoutContentComponent, {
+ imports: [
+ EmailLinkAuthScreenComponent,
+ MockEmailLinkAuthFormComponent,
+ MockMultiFactorAuthAssertionFormComponent,
+ MockRedirectErrorComponent,
+ CardComponent,
+ CardHeaderComponent,
+ CardTitleComponent,
+ CardSubtitleComponent,
+ CardContentComponent,
+ ],
+ });
+
+ // Check for the MFA form element by its selector
+ expect(container.querySelector("fui-multi-factor-auth-assertion-form")).toBeInTheDocument();
+ });
+
+ it("does not render RedirectError when MFA resolver is present", async () => {
+ const { injectUI } = require("../../../provider");
+ injectUI.mockImplementation(() => () => ({
+ multiFactorResolver: { auth: {}, session: null, hints: [] },
+ }));
+
+ const { container } = await render(TestHostWithContentComponent, {
+ imports: [
+ EmailLinkAuthScreenComponent,
+ MockEmailLinkAuthFormComponent,
+ MockMultiFactorAuthAssertionFormComponent,
+ MockRedirectErrorComponent,
+ CardComponent,
+ CardHeaderComponent,
+ CardTitleComponent,
+ CardSubtitleComponent,
+ CardContentComponent,
+ ],
+ });
+
+ expect(container.querySelector("fui-redirect-error")).toBeNull();
+ expect(container.querySelector("fui-multi-factor-auth-assertion-form")).toBeInTheDocument();
+ });
+
+ it("calls signIn output when MFA flow succeeds", async () => {
+ const { injectUI } = require("../../../provider");
+ injectUI.mockImplementation(() => () => ({
+ multiFactorResolver: { auth: {}, session: null, hints: [] },
+ }));
+
+ const { fixture } = await render(TestHostWithoutContentComponent, {
+ imports: [
+ EmailLinkAuthScreenComponent,
+ MockEmailLinkAuthFormComponent,
+ MockMultiFactorAuthAssertionFormComponent,
+ MockRedirectErrorComponent,
+ CardComponent,
+ CardHeaderComponent,
+ CardTitleComponent,
+ CardSubtitleComponent,
+ CardContentComponent,
+ ],
+ });
+
+ const component = fixture.debugElement.query((el) => el.name === "fui-email-link-auth-screen").componentInstance;
+ const signInSpy = jest.spyOn(component.signIn, "emit");
+
+ // Simulate MFA success by directly calling the onSuccess handler
+ const mfaComponent = fixture.debugElement.query(
+ (el) => el.name === "fui-multi-factor-auth-assertion-form"
+ ).componentInstance;
+ mfaComponent.onSuccess.emit({ user: { uid: "mfa-user" } });
+
+ expect(signInSpy).toHaveBeenCalledTimes(1);
+ expect(signInSpy).toHaveBeenCalledWith(
+ expect.objectContaining({ user: expect.objectContaining({ uid: "mfa-user" }) })
+ );
+ });
});
diff --git a/packages/angular/src/lib/auth/screens/email-link-auth-screen.ts b/packages/angular/src/lib/auth/screens/email-link-auth-screen.ts
index 20c4ab63..87106720 100644
--- a/packages/angular/src/lib/auth/screens/email-link-auth-screen.ts
+++ b/packages/angular/src/lib/auth/screens/email-link-auth-screen.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { Component, output } from "@angular/core";
+import { Component, output, computed } from "@angular/core";
import { CommonModule } from "@angular/common";
import {
CardComponent,
@@ -23,8 +23,9 @@ import {
CardSubtitleComponent,
CardContentComponent,
} from "../../components/card";
-import { injectTranslation } from "../../provider";
+import { injectTranslation, injectUI } from "../../provider";
import { EmailLinkAuthFormComponent } from "../forms/email-link-auth-form";
+import { MultiFactorAuthAssertionFormComponent } from "../forms/multi-factor-auth-assertion-form";
import { RedirectErrorComponent } from "../../components/redirect-error";
import { UserCredential } from "@angular/fire/auth";
@@ -39,6 +40,7 @@ import { UserCredential } from "@angular/fire/auth";
CardSubtitleComponent,
CardContentComponent,
EmailLinkAuthFormComponent,
+ MultiFactorAuthAssertionFormComponent,
RedirectErrorComponent,
],
template: `
@@ -49,15 +51,23 @@ import { UserCredential } from "@angular/fire/auth";
{{ subtitleText() }}
-
-
-
+ @if (mfaResolver()) {
+
+ } @else {
+
+
+
+ }
`,
})
export class EmailLinkAuthScreenComponent {
+ private ui = injectUI();
+
+ mfaResolver = computed(() => this.ui().multiFactorResolver);
+
titleText = injectTranslation("labels", "signIn");
subtitleText = injectTranslation("prompts", "signInToAccount");
diff --git a/packages/react/src/auth/screens/email-link-auth-screen.test.tsx b/packages/react/src/auth/screens/email-link-auth-screen.test.tsx
index fa42dde9..af370c38 100644
--- a/packages/react/src/auth/screens/email-link-auth-screen.test.tsx
+++ b/packages/react/src/auth/screens/email-link-auth-screen.test.tsx
@@ -15,10 +15,11 @@
*/
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
-import { render, screen, cleanup } from "@testing-library/react";
+import { render, screen, cleanup, fireEvent } from "@testing-library/react";
import { EmailLinkAuthScreen } from "~/auth/screens/email-link-auth-screen";
import { CreateFirebaseUIProvider, createMockUI } from "~/tests/utils";
import { registerLocale } from "@firebase-ui/translations";
+import type { MultiFactorResolver } from "firebase/auth";
vi.mock("~/auth/forms/email-link-auth-form", () => ({
EmailLinkAuthForm: () => Email Link Form
,
@@ -32,6 +33,17 @@ vi.mock("~/components/redirect-error", () => ({
RedirectError: () => Redirect Error
,
}));
+vi.mock("~/auth/forms/multi-factor-auth-assertion-form", () => ({
+ MultiFactorAuthAssertionForm: ({ onSuccess }: { onSuccess?: (credential: any) => void }) => (
+
+
MFA Assertion Form
+
+
+ ),
+}));
+
describe("", () => {
beforeEach(() => {
vi.clearAllMocks();
@@ -122,4 +134,89 @@ describe("", () => {
expect(screen.queryByTestId("redirect-error")).toBeNull();
});
+
+ it("renders MFA assertion form when MFA resolver is present", () => {
+ const mockResolver = {
+ auth: {} as any,
+ session: null,
+ hints: [],
+ };
+ const ui = createMockUI();
+ ui.get().setMultiFactorResolver(mockResolver as unknown as MultiFactorResolver);
+
+ render(
+
+
+
+ );
+
+ expect(screen.getByTestId("mfa-assertion-form")).toBeDefined();
+ });
+
+ it("renders RedirectError component in children section when no MFA resolver", () => {
+ const ui = createMockUI({
+ locale: registerLocale("test", {
+ messages: {
+ dividerOr: "dividerOr",
+ },
+ }),
+ });
+
+ render(
+
+
+ Test Child
+
+
+ );
+
+ expect(screen.getByTestId("redirect-error")).toBeDefined();
+ expect(screen.getByTestId("test-child")).toBeDefined();
+ });
+
+ it("does not render RedirectError when MFA resolver is present", () => {
+ const mockResolver = {
+ auth: {} as any,
+ session: null,
+ hints: [],
+ };
+ const ui = createMockUI();
+ ui.get().setMultiFactorResolver(mockResolver as unknown as MultiFactorResolver);
+
+ render(
+
+
+ Test Child
+
+
+ );
+
+ expect(screen.queryByTestId("redirect-error")).toBeNull();
+ expect(screen.getByTestId("mfa-assertion-form")).toBeDefined();
+ });
+
+ it("calls onSignIn with credential when MFA flow succeeds", () => {
+ const mockResolver = {
+ auth: {} as any,
+ session: null,
+ hints: [],
+ };
+ const ui = createMockUI();
+ ui.get().setMultiFactorResolver(mockResolver as unknown as MultiFactorResolver);
+
+ const onSignIn = vi.fn();
+
+ render(
+
+
+
+ );
+
+ fireEvent.click(screen.getByTestId("mfa-on-success"));
+
+ expect(onSignIn).toHaveBeenCalledTimes(1);
+ expect(onSignIn).toHaveBeenCalledWith(
+ expect.objectContaining({ user: expect.objectContaining({ uid: "mfa-user" }) })
+ );
+ });
});
diff --git a/packages/react/src/auth/screens/email-link-auth-screen.tsx b/packages/react/src/auth/screens/email-link-auth-screen.tsx
index 368f6eef..2e0b3569 100644
--- a/packages/react/src/auth/screens/email-link-auth-screen.tsx
+++ b/packages/react/src/auth/screens/email-link-auth-screen.tsx
@@ -20,16 +20,19 @@ import { Divider } from "~/components/divider";
import { useUI } from "~/hooks";
import { Card, CardContent, CardHeader, CardSubtitle, CardTitle } from "~/components/card";
import { EmailLinkAuthForm, type EmailLinkAuthFormProps } from "../forms/email-link-auth-form";
+import { MultiFactorAuthAssertionForm } from "../forms/multi-factor-auth-assertion-form";
import { RedirectError } from "~/components/redirect-error";
export type EmailLinkAuthScreenProps = PropsWithChildren;
-export function EmailLinkAuthScreen({ children, onEmailSent }: EmailLinkAuthScreenProps) {
+export function EmailLinkAuthScreen({ children, onEmailSent, onSignIn }: EmailLinkAuthScreenProps) {
const ui = useUI();
const titleText = getTranslation(ui, "labels", "signIn");
const subtitleText = getTranslation(ui, "prompts", "signInToAccount");
+ const mfaResolver = ui.multiFactorResolver;
+
return (
@@ -38,16 +41,26 @@ export function EmailLinkAuthScreen({ children, onEmailSent }: EmailLinkAuthScre
{subtitleText}
-
- {children ? (
+ {mfaResolver ? (
+ {
+ onSignIn?.(credential);
+ }}
+ />
+ ) : (
<>
- {getTranslation(ui, "messages", "dividerOr")}
-
- {children}
-
-
+
+ {children ? (
+ <>
+ {getTranslation(ui, "messages", "dividerOr")}
+
+ {children}
+
+
+ >
+ ) : null}
>
- ) : null}
+ )}