From c780bde561000aabac556492c58192f17e3cbfe4 Mon Sep 17 00:00:00 2001 From: Elliot Hesp Date: Sat, 8 Nov 2025 14:38:14 +0000 Subject: [PATCH 01/32] feat(angular): Add MFA assertion screen --- .../screens/email-link-auth-screen.spec.ts | 40 +-- .../auth/screens/email-link-auth-screen.ts | 34 +-- ...multi-factor-auth-assertion-screen.spec.ts | 229 ++++++++++++++++++ .../multi-factor-auth-assertion-screen.ts | 66 +++++ .../src/lib/auth/screens/oauth-screen.spec.ts | 79 +++--- .../src/lib/auth/screens/oauth-screen.ts | 32 +-- .../auth/screens/phone-auth-screen.spec.ts | 70 +++--- .../src/lib/auth/screens/phone-auth-screen.ts | 32 +-- .../auth/screens/sign-in-auth-screen.spec.ts | 70 +++--- .../lib/auth/screens/sign-in-auth-screen.ts | 32 +-- .../auth/screens/sign-up-auth-screen.spec.ts | 70 +++--- .../lib/auth/screens/sign-up-auth-screen.ts | 32 +-- packages/angular/src/public-api.ts | 1 + 13 files changed, 518 insertions(+), 269 deletions(-) create mode 100644 packages/angular/src/lib/auth/screens/multi-factor-auth-assertion-screen.spec.ts create mode 100644 packages/angular/src/lib/auth/screens/multi-factor-auth-assertion-screen.ts 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 b9e4328f4..4067b69cf 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 @@ -41,9 +41,9 @@ class MockEmailLinkAuthFormComponent {} class MockRedirectErrorComponent {} @Component({ - selector: "fui-multi-factor-auth-assertion-form", + selector: "fui-multi-factor-auth-assertion-screen", template: ` -
MFA Assertion Form
+
MFA Assertion Screen
@@ -51,7 +51,7 @@ class MockRedirectErrorComponent {} standalone: true, outputs: ["onSuccess"], }) -class MockMultiFactorAuthAssertionFormComponent { +class MockMultiFactorAuthAssertionScreenComponent { onSuccess = new EventEmitter(); } @@ -218,7 +218,7 @@ describe("", () => { imports: [ EmailLinkAuthScreenComponent, MockEmailLinkAuthFormComponent, - MockMultiFactorAuthAssertionFormComponent, + MockMultiFactorAuthAssertionScreenComponent, MockRedirectErrorComponent, CardComponent, CardHeaderComponent, @@ -228,32 +228,8 @@ describe("", () => { ], }); - // 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(); + // Check for the MFA screen element by its selector + expect(container.querySelector("fui-multi-factor-auth-assertion-screen")).toBeInTheDocument(); }); it("calls signIn output when MFA flow succeeds", async () => { @@ -266,7 +242,7 @@ describe("", () => { imports: [ EmailLinkAuthScreenComponent, MockEmailLinkAuthFormComponent, - MockMultiFactorAuthAssertionFormComponent, + MockMultiFactorAuthAssertionScreenComponent, MockRedirectErrorComponent, CardComponent, CardHeaderComponent, @@ -281,7 +257,7 @@ describe("", () => { // Simulate MFA success by directly calling the onSuccess handler const mfaComponent = fixture.debugElement.query( - (el) => el.name === "fui-multi-factor-auth-assertion-form" + (el) => el.name === "fui-multi-factor-auth-assertion-screen" ).componentInstance; mfaComponent.onSuccess.emit({ user: { 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 87106720c..e4492fa43 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 @@ -25,7 +25,7 @@ import { } from "../../components/card"; import { injectTranslation, injectUI } from "../../provider"; import { EmailLinkAuthFormComponent } from "../forms/email-link-auth-form"; -import { MultiFactorAuthAssertionFormComponent } from "../forms/multi-factor-auth-assertion-form"; +import { MultiFactorAuthAssertionScreenComponent } from "../screens/multi-factor-auth-assertion-screen"; import { RedirectErrorComponent } from "../../components/redirect-error"; import { UserCredential } from "@angular/fire/auth"; @@ -40,27 +40,27 @@ import { UserCredential } from "@angular/fire/auth"; CardSubtitleComponent, CardContentComponent, EmailLinkAuthFormComponent, - MultiFactorAuthAssertionFormComponent, + MultiFactorAuthAssertionScreenComponent, RedirectErrorComponent, ], template: ` -
- - - {{ titleText() }} - {{ subtitleText() }} - - - @if (mfaResolver()) { - - } @else { + @if (mfaResolver()) { + + } @else { +
+ + + {{ titleText() }} + {{ subtitleText() }} + + - - } - - -
+ +
+
+
+ } `, }) export class EmailLinkAuthScreenComponent { diff --git a/packages/angular/src/lib/auth/screens/multi-factor-auth-assertion-screen.spec.ts b/packages/angular/src/lib/auth/screens/multi-factor-auth-assertion-screen.spec.ts new file mode 100644 index 000000000..dfa4f5f62 --- /dev/null +++ b/packages/angular/src/lib/auth/screens/multi-factor-auth-assertion-screen.spec.ts @@ -0,0 +1,229 @@ +/** + * 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 { render, screen } from "@testing-library/angular"; +import { Component } from "@angular/core"; +import { TestBed } from "@angular/core/testing"; +import { MultiFactorAuthAssertionScreenComponent } from "./multi-factor-auth-assertion-screen"; +import { MultiFactorAuthAssertionFormComponent } from "../forms/multi-factor-auth-assertion-form"; +import { + CardComponent, + CardHeaderComponent, + CardTitleComponent, + CardSubtitleComponent, + CardContentComponent, +} from "../../components/card"; + +@Component({ + selector: "fui-redirect-error", + template: '
Redirect Error
', + standalone: true, +}) +class MockRedirectErrorComponent {} + +@Component({ + template: ` + +
Test Content
+
+ `, + standalone: true, + imports: [MultiFactorAuthAssertionScreenComponent], +}) +class TestHostWithContentComponent {} + +@Component({ + template: ``, + standalone: true, + imports: [MultiFactorAuthAssertionScreenComponent], +}) +class TestHostWithoutContentComponent {} + +describe("", () => { + beforeEach(() => { + const { injectTranslation, injectUI } = require("../../../provider"); + injectTranslation.mockImplementation((category: string, key: string) => { + const mockTranslations: Record> = { + labels: { + multiFactorAssertion: "Multi-Factor Assertion", + }, + prompts: { + mfaAssertionPrompt: "Verify your multi-factor authentication", + }, + }; + return () => mockTranslations[category]?.[key] || `${category}.${key}`; + }); + + injectUI.mockImplementation(() => () => ({ + multiFactorResolver: { + auth: {}, + session: null, + hints: [], + }, + })); + }); + + it("renders with correct title and subtitle", async () => { + await render(TestHostWithoutContentComponent, { + imports: [ + MultiFactorAuthAssertionScreenComponent, + MockRedirectErrorComponent, + CardComponent, + CardHeaderComponent, + CardTitleComponent, + CardSubtitleComponent, + CardContentComponent, + ], + }); + + expect(screen.getByRole("heading", { name: "Multi-Factor Assertion" })).toBeInTheDocument(); + expect(screen.getByText("Verify your multi-factor authentication")).toBeInTheDocument(); + }); + + it("includes the MultiFactorAuthAssertionForm component", async () => { + TestBed.overrideComponent(MultiFactorAuthAssertionFormComponent, { + set: { + template: '
MFA Assertion Form
', + }, + }); + + await render(TestHostWithoutContentComponent, { + imports: [ + MultiFactorAuthAssertionScreenComponent, + MockRedirectErrorComponent, + CardComponent, + CardHeaderComponent, + CardTitleComponent, + CardSubtitleComponent, + CardContentComponent, + ], + }); + + const form = screen.getByTestId("mfa-assertion-form"); + expect(form).toBeInTheDocument(); + expect(form).toHaveTextContent("MFA Assertion Form"); + }); + + it("renders projected content when provided", async () => { + await render(TestHostWithContentComponent, { + imports: [ + MultiFactorAuthAssertionScreenComponent, + MockRedirectErrorComponent, + CardComponent, + CardHeaderComponent, + CardTitleComponent, + CardSubtitleComponent, + CardContentComponent, + ], + }); + + const projectedContent = screen.getByTestId("projected-content"); + expect(projectedContent).toBeInTheDocument(); + expect(projectedContent).toHaveTextContent("Test Content"); + }); + + it("renders RedirectError component", async () => { + const { container } = await render(TestHostWithContentComponent, { + imports: [ + MultiFactorAuthAssertionScreenComponent, + MockRedirectErrorComponent, + CardComponent, + CardHeaderComponent, + CardTitleComponent, + CardSubtitleComponent, + CardContentComponent, + ], + }); + + const redirectErrorElement = container.querySelector("fui-redirect-error"); + expect(redirectErrorElement).toBeInTheDocument(); + }); + + it("has correct CSS classes", async () => { + const { container } = await render(TestHostWithoutContentComponent, { + imports: [ + MultiFactorAuthAssertionScreenComponent, + MockRedirectErrorComponent, + CardComponent, + CardHeaderComponent, + CardTitleComponent, + CardSubtitleComponent, + CardContentComponent, + ], + }); + + expect(container.querySelector(".fui-screen")).toBeInTheDocument(); + expect(container.querySelector(".fui-card")).toBeInTheDocument(); + expect(container.querySelector(".fui-card__header")).toBeInTheDocument(); + expect(container.querySelector(".fui-card__title")).toBeInTheDocument(); + expect(container.querySelector(".fui-card__subtitle")).toBeInTheDocument(); + expect(container.querySelector(".fui-card__content")).toBeInTheDocument(); + }); + + it("calls injectTranslation with correct parameters", async () => { + const { injectTranslation } = require("../../../provider"); + await render(TestHostWithoutContentComponent, { + imports: [ + MultiFactorAuthAssertionScreenComponent, + MockRedirectErrorComponent, + CardComponent, + CardHeaderComponent, + CardTitleComponent, + CardSubtitleComponent, + CardContentComponent, + ], + }); + + expect(injectTranslation).toHaveBeenCalledWith("labels", "multiFactorAssertion"); + expect(injectTranslation).toHaveBeenCalledWith("prompts", "mfaAssertionPrompt"); + }); + + it("emits onSuccess event when form emits onSuccess", async () => { + TestBed.overrideComponent(MultiFactorAuthAssertionFormComponent, { + set: { + template: '
MFA Assertion Form
', + }, + }); + + const { fixture } = await render(TestHostWithoutContentComponent, { + imports: [ + MultiFactorAuthAssertionScreenComponent, + MockRedirectErrorComponent, + CardComponent, + CardHeaderComponent, + CardTitleComponent, + CardSubtitleComponent, + CardContentComponent, + ], + }); + + const component = fixture.debugElement.query( + (el) => el.name === "fui-multi-factor-auth-assertion-screen" + ).componentInstance; + const onSuccessSpy = jest.spyOn(component.onSuccess, "emit"); + + const formComponent = fixture.debugElement.query( + (el) => el.name === "fui-multi-factor-auth-assertion-form" + ).componentInstance; + formComponent.onSuccess.emit({ user: { uid: "mfa-user" } }); + + expect(onSuccessSpy).toHaveBeenCalledTimes(1); + expect(onSuccessSpy).toHaveBeenCalledWith( + expect.objectContaining({ user: expect.objectContaining({ uid: "mfa-user" }) }) + ); + }); +}); + diff --git a/packages/angular/src/lib/auth/screens/multi-factor-auth-assertion-screen.ts b/packages/angular/src/lib/auth/screens/multi-factor-auth-assertion-screen.ts new file mode 100644 index 000000000..a41da9afd --- /dev/null +++ b/packages/angular/src/lib/auth/screens/multi-factor-auth-assertion-screen.ts @@ -0,0 +1,66 @@ +/** + * 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, output } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { UserCredential } from "@angular/fire/auth"; +import { injectTranslation } from "../../provider"; +import { MultiFactorAuthAssertionFormComponent } from "../forms/multi-factor-auth-assertion-form"; +import { RedirectErrorComponent } from "../../components/redirect-error"; +import { + CardComponent, + CardHeaderComponent, + CardTitleComponent, + CardSubtitleComponent, + CardContentComponent, +} from "../../components/card"; + +@Component({ + selector: "fui-multi-factor-auth-assertion-screen", + standalone: true, + imports: [ + CommonModule, + CardComponent, + CardHeaderComponent, + CardTitleComponent, + CardSubtitleComponent, + CardContentComponent, + MultiFactorAuthAssertionFormComponent, + RedirectErrorComponent, + ], + template: ` +
+ + + {{ titleText() }} + {{ subtitleText() }} + + + + + + + +
+ `, +}) +export class MultiFactorAuthAssertionScreenComponent { + onSuccess = output(); + + titleText = injectTranslation("labels", "multiFactorAssertion"); + subtitleText = injectTranslation("prompts", "mfaAssertionPrompt"); +} + diff --git a/packages/angular/src/lib/auth/screens/oauth-screen.spec.ts b/packages/angular/src/lib/auth/screens/oauth-screen.spec.ts index 49a57e09b..1b87973fa 100644 --- a/packages/angular/src/lib/auth/screens/oauth-screen.spec.ts +++ b/packages/angular/src/lib/auth/screens/oauth-screen.spec.ts @@ -26,6 +26,7 @@ import { CardSubtitleComponent, CardContentComponent, } from "../../components/card"; +import { MultiFactorAuthAssertionScreenComponent } from "../screens/multi-factor-auth-assertion-screen"; import { MultiFactorAuthAssertionFormComponent } from "../forms/multi-factor-auth-assertion-form"; import { ContentComponent } from "../../components/content"; @@ -81,11 +82,12 @@ class TestHostWithMultipleProvidersComponent {} class TestHostWithoutContentComponent {} @Component({ - selector: "fui-multi-factor-auth-assertion-form", - template: '
MFA Assertion Form
', + selector: "fui-multi-factor-auth-assertion-screen", + template: '
MFA Assertion Screen
', standalone: true, + outputs: ["onSuccess"], }) -class MockMultiFactorAuthAssertionFormComponent { +class MockMultiFactorAuthAssertionScreenComponent { onSuccess = new EventEmitter(); } @@ -126,7 +128,7 @@ describe("", () => { OAuthScreenComponent, MockPoliciesComponent, MockRedirectErrorComponent, - MultiFactorAuthAssertionFormComponent, + MultiFactorAuthAssertionScreenComponent, CardComponent, CardHeaderComponent, CardTitleComponent, @@ -146,7 +148,7 @@ describe("", () => { OAuthScreenComponent, MockPoliciesComponent, MockRedirectErrorComponent, - MultiFactorAuthAssertionFormComponent, + MultiFactorAuthAssertionScreenComponent, CardComponent, CardHeaderComponent, CardTitleComponent, @@ -166,7 +168,7 @@ describe("", () => { OAuthScreenComponent, MockPoliciesComponent, MockRedirectErrorComponent, - MultiFactorAuthAssertionFormComponent, + MultiFactorAuthAssertionScreenComponent, CardComponent, CardHeaderComponent, CardTitleComponent, @@ -187,7 +189,7 @@ describe("", () => { OAuthScreenComponent, MockPoliciesComponent, MockRedirectErrorComponent, - MultiFactorAuthAssertionFormComponent, + MultiFactorAuthAssertionScreenComponent, CardComponent, CardHeaderComponent, CardTitleComponent, @@ -212,7 +214,7 @@ describe("", () => { OAuthScreenComponent, MockPoliciesComponent, MockRedirectErrorComponent, - MultiFactorAuthAssertionFormComponent, + MultiFactorAuthAssertionScreenComponent, CardComponent, CardHeaderComponent, CardTitleComponent, @@ -232,7 +234,7 @@ describe("", () => { OAuthScreenComponent, MockPoliciesComponent, MockRedirectErrorComponent, - MultiFactorAuthAssertionFormComponent, + MultiFactorAuthAssertionScreenComponent, CardComponent, CardHeaderComponent, CardTitleComponent, @@ -257,7 +259,7 @@ describe("", () => { OAuthScreenComponent, MockPoliciesComponent, MockRedirectErrorComponent, - MultiFactorAuthAssertionFormComponent, + MultiFactorAuthAssertionScreenComponent, CardComponent, CardHeaderComponent, CardTitleComponent, @@ -271,17 +273,17 @@ describe("", () => { expect(injectTranslation).toHaveBeenCalledWith("prompts", "signInToAccount"); }); - it("renders MFA assertion form when multiFactorResolver is present", async () => { + it("renders MFA assertion screen when multiFactorResolver is present", async () => { const { injectUI } = require("../../../provider"); injectUI.mockImplementation(() => { return () => ({ - multiFactorResolver: { hints: [] }, + multiFactorResolver: { auth: {}, session: null, hints: [] }, }); }); - TestBed.overrideComponent(MultiFactorAuthAssertionFormComponent, { + TestBed.overrideComponent(MultiFactorAuthAssertionScreenComponent, { set: { - template: '
MFA Assertion Form
', + template: '
MFA Assertion Screen
', }, }); @@ -290,7 +292,7 @@ describe("", () => { OAuthScreenComponent, MockPoliciesComponent, MockRedirectErrorComponent, - MultiFactorAuthAssertionFormComponent, + MultiFactorAuthAssertionScreenComponent, CardComponent, CardHeaderComponent, CardTitleComponent, @@ -300,7 +302,7 @@ describe("", () => { ], }); - expect(screen.getByTestId("mfa-assertion-form")).toBeInTheDocument(); + expect(screen.getByTestId("mfa-assertion-screen")).toBeInTheDocument(); expect(screen.queryByTestId("policies")).not.toBeInTheDocument(); }); @@ -308,13 +310,13 @@ describe("", () => { const { injectUI } = require("../../../provider"); injectUI.mockImplementation(() => { return () => ({ - multiFactorResolver: { hints: [] }, + multiFactorResolver: { auth: {}, session: null, hints: [] }, }); }); - TestBed.overrideComponent(MultiFactorAuthAssertionFormComponent, { + TestBed.overrideComponent(MultiFactorAuthAssertionScreenComponent, { set: { - template: '
MFA Assertion Form
', + template: '
MFA Assertion Screen
', }, }); @@ -323,7 +325,7 @@ describe("", () => { OAuthScreenComponent, MockPoliciesComponent, MockRedirectErrorComponent, - MultiFactorAuthAssertionFormComponent, + MultiFactorAuthAssertionScreenComponent, CardComponent, CardHeaderComponent, CardTitleComponent, @@ -334,41 +336,29 @@ describe("", () => { }); expect(screen.queryByTestId("policies")).not.toBeInTheDocument(); - expect(screen.getByTestId("mfa-assertion-form")).toBeInTheDocument(); + expect(screen.getByTestId("mfa-assertion-screen")).toBeInTheDocument(); }); it("emits onSignIn with credential when MFA flow succeeds", async () => { const { injectUI } = require("../../../provider"); injectUI.mockImplementation(() => { return () => ({ - multiFactorResolver: { hints: [{ factorId: "totp", uid: "test" }] }, + multiFactorResolver: { auth: {}, session: null, hints: [{ factorId: "totp", uid: "test" }] }, }); }); - TestBed.overrideComponent(MultiFactorAuthAssertionFormComponent, { + TestBed.overrideComponent(MultiFactorAuthAssertionScreenComponent, { set: { - template: - '
MFA Assertion Form
', + template: '
MFA Assertion Screen
', }, }); - const onSignInHandler = jest.fn(); - - @Component({ - template: ``, - standalone: true, - imports: [OAuthScreenComponent], - }) - class HostCaptureComponent { - onSignIn = onSignInHandler; - } - - await render(HostCaptureComponent, { + const { fixture } = await render(TestHostWithoutContentComponent, { imports: [ OAuthScreenComponent, MockPoliciesComponent, MockRedirectErrorComponent, - MultiFactorAuthAssertionFormComponent, + MultiFactorAuthAssertionScreenComponent, CardComponent, CardHeaderComponent, CardTitleComponent, @@ -378,11 +368,16 @@ describe("", () => { ], }); - const trigger = screen.getByTestId("mfa-on-success"); - trigger.dispatchEvent(new MouseEvent("click", { bubbles: true })); + const component = fixture.debugElement.query((el) => el.name === "fui-oauth-screen").componentInstance; + const onSignInSpy = jest.spyOn(component.onSignIn, "emit"); + + const mfaScreenComponent = fixture.debugElement.query( + (el) => el.name === "fui-multi-factor-auth-assertion-screen" + ).componentInstance; + mfaScreenComponent.onSuccess.emit({ user: { uid: "angular-oauth-mfa-user" } }); - expect(onSignInHandler).toHaveBeenCalled(); - expect(onSignInHandler).toHaveBeenCalledWith( + expect(onSignInSpy).toHaveBeenCalledTimes(1); + expect(onSignInSpy).toHaveBeenCalledWith( expect.objectContaining({ user: expect.objectContaining({ uid: "angular-oauth-mfa-user" }) }) ); }); diff --git a/packages/angular/src/lib/auth/screens/oauth-screen.ts b/packages/angular/src/lib/auth/screens/oauth-screen.ts index 04b4e2794..916bb7736 100644 --- a/packages/angular/src/lib/auth/screens/oauth-screen.ts +++ b/packages/angular/src/lib/auth/screens/oauth-screen.ts @@ -26,7 +26,7 @@ import { import { injectTranslation, injectUI } from "../../provider"; import { PoliciesComponent } from "../../components/policies"; import { ContentComponent } from "../../components/content"; -import { MultiFactorAuthAssertionFormComponent } from "../forms/multi-factor-auth-assertion-form"; +import { MultiFactorAuthAssertionScreenComponent } from "../screens/multi-factor-auth-assertion-screen"; import { RedirectErrorComponent } from "../../components/redirect-error"; import { type UserCredential } from "firebase/auth"; @@ -42,29 +42,29 @@ import { type UserCredential } from "firebase/auth"; CardContentComponent, PoliciesComponent, ContentComponent, - MultiFactorAuthAssertionFormComponent, + MultiFactorAuthAssertionScreenComponent, RedirectErrorComponent, ], template: ` -
- - - {{ titleText() }} - {{ subtitleText() }} - - - @if (mfaResolver()) { - - } @else { + @if (mfaResolver()) { + + } @else { +
+ + + {{ titleText() }} + {{ subtitleText() }} + + - } - - -
+
+
+
+ } `, }) export class OAuthScreenComponent { diff --git a/packages/angular/src/lib/auth/screens/phone-auth-screen.spec.ts b/packages/angular/src/lib/auth/screens/phone-auth-screen.spec.ts index 45b937727..284d045ca 100644 --- a/packages/angular/src/lib/auth/screens/phone-auth-screen.spec.ts +++ b/packages/angular/src/lib/auth/screens/phone-auth-screen.spec.ts @@ -26,6 +26,7 @@ import { CardSubtitleComponent, CardContentComponent, } from "../../components/card"; +import { MultiFactorAuthAssertionScreenComponent } from "../screens/multi-factor-auth-assertion-screen"; import { MultiFactorAuthAssertionFormComponent } from "../forms/multi-factor-auth-assertion-form"; import { TotpMultiFactorAssertionFormComponent } from "../forms/mfa/totp-multi-factor-assertion-form"; import { TotpMultiFactorGenerator } from "firebase/auth"; @@ -90,7 +91,7 @@ describe("", () => { PhoneAuthScreenComponent, MockPhoneAuthFormComponent, MockRedirectErrorComponent, - MultiFactorAuthAssertionFormComponent, + MultiFactorAuthAssertionScreenComponent, CardComponent, CardHeaderComponent, CardTitleComponent, @@ -109,7 +110,7 @@ describe("", () => { PhoneAuthScreenComponent, MockPhoneAuthFormComponent, MockRedirectErrorComponent, - MultiFactorAuthAssertionFormComponent, + MultiFactorAuthAssertionScreenComponent, CardComponent, CardHeaderComponent, CardTitleComponent, @@ -129,7 +130,7 @@ describe("", () => { PhoneAuthScreenComponent, MockPhoneAuthFormComponent, MockRedirectErrorComponent, - MultiFactorAuthAssertionFormComponent, + MultiFactorAuthAssertionScreenComponent, CardComponent, CardHeaderComponent, CardTitleComponent, @@ -149,7 +150,7 @@ describe("", () => { PhoneAuthScreenComponent, MockPhoneAuthFormComponent, MockRedirectErrorComponent, - MultiFactorAuthAssertionFormComponent, + MultiFactorAuthAssertionScreenComponent, CardComponent, CardHeaderComponent, CardTitleComponent, @@ -168,7 +169,7 @@ describe("", () => { PhoneAuthScreenComponent, MockPhoneAuthFormComponent, MockRedirectErrorComponent, - MultiFactorAuthAssertionFormComponent, + MultiFactorAuthAssertionScreenComponent, CardComponent, CardHeaderComponent, CardTitleComponent, @@ -192,7 +193,7 @@ describe("", () => { PhoneAuthScreenComponent, MockPhoneAuthFormComponent, MockRedirectErrorComponent, - MultiFactorAuthAssertionFormComponent, + MultiFactorAuthAssertionScreenComponent, CardComponent, CardHeaderComponent, CardTitleComponent, @@ -205,17 +206,17 @@ describe("", () => { expect(injectTranslation).toHaveBeenCalledWith("prompts", "signInToAccount"); }); - it("renders MFA assertion form when multiFactorResolver is present", async () => { + it("renders MFA assertion screen when multiFactorResolver is present", async () => { const { injectUI } = require("../../../provider"); injectUI.mockImplementation(() => { return () => ({ - multiFactorResolver: { hints: [] }, + multiFactorResolver: { auth: {}, session: null, hints: [] }, }); }); - TestBed.overrideComponent(MultiFactorAuthAssertionFormComponent, { + TestBed.overrideComponent(MultiFactorAuthAssertionScreenComponent, { set: { - template: '
MFA Assertion Form
', + template: '
MFA Assertion Screen
', }, }); @@ -224,7 +225,7 @@ describe("", () => { PhoneAuthScreenComponent, MockPhoneAuthFormComponent, MockRedirectErrorComponent, - MultiFactorAuthAssertionFormComponent, + MultiFactorAuthAssertionScreenComponent, CardComponent, CardHeaderComponent, CardTitleComponent, @@ -233,7 +234,7 @@ describe("", () => { ], }); - expect(screen.getByTestId("mfa-assertion-form")).toBeInTheDocument(); + expect(screen.getByTestId("mfa-assertion-screen")).toBeInTheDocument(); expect(screen.queryByText("Phone Auth Form")).not.toBeInTheDocument(); }); @@ -241,13 +242,13 @@ describe("", () => { const { injectUI } = require("../../../provider"); injectUI.mockImplementation(() => { return () => ({ - multiFactorResolver: { hints: [] }, + multiFactorResolver: { auth: {}, session: null, hints: [] }, }); }); - TestBed.overrideComponent(MultiFactorAuthAssertionFormComponent, { + TestBed.overrideComponent(MultiFactorAuthAssertionScreenComponent, { set: { - template: '
MFA Assertion Form
', + template: '
MFA Assertion Screen
', }, }); @@ -256,7 +257,7 @@ describe("", () => { PhoneAuthScreenComponent, MockPhoneAuthFormComponent, MockRedirectErrorComponent, - MultiFactorAuthAssertionFormComponent, + MultiFactorAuthAssertionScreenComponent, CardComponent, CardHeaderComponent, CardTitleComponent, @@ -266,41 +267,29 @@ describe("", () => { }); expect(screen.queryByText("Phone Auth Form")).not.toBeInTheDocument(); - expect(screen.getByTestId("mfa-assertion-form")).toBeInTheDocument(); + expect(screen.getByTestId("mfa-assertion-screen")).toBeInTheDocument(); }); it("emits signIn with credential when MFA flow succeeds", async () => { const { injectUI } = require("../../../provider"); injectUI.mockImplementation(() => { return () => ({ - multiFactorResolver: { hints: [{ factorId: TotpMultiFactorGenerator.FACTOR_ID, uid: "test" }] }, + multiFactorResolver: { auth: {}, session: null, hints: [{ factorId: TotpMultiFactorGenerator.FACTOR_ID, uid: "test" }] }, }); }); - TestBed.overrideComponent(TotpMultiFactorAssertionFormComponent, { + TestBed.overrideComponent(MultiFactorAuthAssertionScreenComponent, { set: { - template: - '
TOTP
', + template: '
MFA Assertion Screen
', }, }); - const signInHandler = jest.fn(); - - @Component({ - template: ``, - standalone: true, - imports: [PhoneAuthScreenComponent], - }) - class HostCaptureComponent { - onSignIn = signInHandler; - } - - await render(HostCaptureComponent, { + const { fixture } = await render(TestHostWithoutContentComponent, { imports: [ PhoneAuthScreenComponent, MockPhoneAuthFormComponent, MockRedirectErrorComponent, - MultiFactorAuthAssertionFormComponent, // Using real component + MultiFactorAuthAssertionScreenComponent, CardComponent, CardHeaderComponent, CardTitleComponent, @@ -309,11 +298,16 @@ describe("", () => { ], }); - const trigger = screen.getByTestId("mfa-on-success"); - trigger.dispatchEvent(new MouseEvent("click", { bubbles: true })); + const component = fixture.debugElement.query((el) => el.name === "fui-phone-auth-screen").componentInstance; + const signInSpy = jest.spyOn(component.signIn, "emit"); + + const mfaScreenComponent = fixture.debugElement.query( + (el) => el.name === "fui-multi-factor-auth-assertion-screen" + ).componentInstance; + mfaScreenComponent.onSuccess.emit({ user: { uid: "angular-phone-mfa-user" } }); - expect(signInHandler).toHaveBeenCalled(); - expect(signInHandler).toHaveBeenCalledWith( + expect(signInSpy).toHaveBeenCalledTimes(1); + expect(signInSpy).toHaveBeenCalledWith( expect.objectContaining({ user: expect.objectContaining({ uid: "angular-phone-mfa-user" }) }) ); }); diff --git a/packages/angular/src/lib/auth/screens/phone-auth-screen.ts b/packages/angular/src/lib/auth/screens/phone-auth-screen.ts index fb12e057e..e7dfacf5e 100644 --- a/packages/angular/src/lib/auth/screens/phone-auth-screen.ts +++ b/packages/angular/src/lib/auth/screens/phone-auth-screen.ts @@ -25,7 +25,7 @@ import { } from "../../components/card"; import { injectTranslation, injectUI } from "../../provider"; import { PhoneAuthFormComponent } from "../forms/phone-auth-form"; -import { MultiFactorAuthAssertionFormComponent } from "../forms/multi-factor-auth-assertion-form"; +import { MultiFactorAuthAssertionScreenComponent } from "../screens/multi-factor-auth-assertion-screen"; import { RedirectErrorComponent } from "../../components/redirect-error"; import { UserCredential } from "@angular/fire/auth"; @@ -40,27 +40,27 @@ import { UserCredential } from "@angular/fire/auth"; CardSubtitleComponent, CardContentComponent, PhoneAuthFormComponent, - MultiFactorAuthAssertionFormComponent, + MultiFactorAuthAssertionScreenComponent, RedirectErrorComponent, ], template: ` -
- - - {{ titleText() }} - {{ subtitleText() }} - - - @if (mfaResolver()) { - - } @else { + @if (mfaResolver()) { + + } @else { +
+ + + {{ titleText() }} + {{ subtitleText() }} + + - } - - -
+
+
+
+ } `, }) export class PhoneAuthScreenComponent { diff --git a/packages/angular/src/lib/auth/screens/sign-in-auth-screen.spec.ts b/packages/angular/src/lib/auth/screens/sign-in-auth-screen.spec.ts index d87a65e61..ed9e18b57 100644 --- a/packages/angular/src/lib/auth/screens/sign-in-auth-screen.spec.ts +++ b/packages/angular/src/lib/auth/screens/sign-in-auth-screen.spec.ts @@ -26,6 +26,7 @@ import { CardSubtitleComponent, CardContentComponent, } from "../../components/card"; +import { MultiFactorAuthAssertionScreenComponent } from "../screens/multi-factor-auth-assertion-screen"; import { MultiFactorAuthAssertionFormComponent } from "../forms/multi-factor-auth-assertion-form"; import { TotpMultiFactorAssertionFormComponent } from "../forms/mfa/totp-multi-factor-assertion-form"; import { TotpMultiFactorGenerator } from "firebase/auth"; @@ -90,7 +91,7 @@ describe("", () => { SignInAuthScreenComponent, MockSignInAuthFormComponent, MockRedirectErrorComponent, - MultiFactorAuthAssertionFormComponent, + MultiFactorAuthAssertionScreenComponent, CardComponent, CardHeaderComponent, CardTitleComponent, @@ -109,7 +110,7 @@ describe("", () => { SignInAuthScreenComponent, MockSignInAuthFormComponent, MockRedirectErrorComponent, - MultiFactorAuthAssertionFormComponent, + MultiFactorAuthAssertionScreenComponent, CardComponent, CardHeaderComponent, CardTitleComponent, @@ -129,7 +130,7 @@ describe("", () => { SignInAuthScreenComponent, MockSignInAuthFormComponent, MockRedirectErrorComponent, - MultiFactorAuthAssertionFormComponent, + MultiFactorAuthAssertionScreenComponent, CardComponent, CardHeaderComponent, CardTitleComponent, @@ -149,7 +150,7 @@ describe("", () => { SignInAuthScreenComponent, MockSignInAuthFormComponent, MockRedirectErrorComponent, - MultiFactorAuthAssertionFormComponent, + MultiFactorAuthAssertionScreenComponent, CardComponent, CardHeaderComponent, CardTitleComponent, @@ -168,7 +169,7 @@ describe("", () => { SignInAuthScreenComponent, MockSignInAuthFormComponent, MockRedirectErrorComponent, - MultiFactorAuthAssertionFormComponent, + MultiFactorAuthAssertionScreenComponent, CardComponent, CardHeaderComponent, CardTitleComponent, @@ -192,7 +193,7 @@ describe("", () => { SignInAuthScreenComponent, MockSignInAuthFormComponent, MockRedirectErrorComponent, - MultiFactorAuthAssertionFormComponent, + MultiFactorAuthAssertionScreenComponent, CardComponent, CardHeaderComponent, CardTitleComponent, @@ -205,17 +206,17 @@ describe("", () => { expect(injectTranslation).toHaveBeenCalledWith("prompts", "signInToAccount"); }); - it("renders MFA assertion form when multiFactorResolver is present", async () => { + it("renders MFA assertion screen when multiFactorResolver is present", async () => { const { injectUI } = require("../../../provider"); injectUI.mockImplementation(() => { return () => ({ - multiFactorResolver: { hints: [] }, + multiFactorResolver: { auth: {}, session: null, hints: [] }, }); }); - TestBed.overrideComponent(MultiFactorAuthAssertionFormComponent, { + TestBed.overrideComponent(MultiFactorAuthAssertionScreenComponent, { set: { - template: '
MFA Assertion Form
', + template: '
MFA Assertion Screen
', }, }); @@ -224,7 +225,7 @@ describe("", () => { SignInAuthScreenComponent, MockSignInAuthFormComponent, MockRedirectErrorComponent, - MultiFactorAuthAssertionFormComponent, + MultiFactorAuthAssertionScreenComponent, CardComponent, CardHeaderComponent, CardTitleComponent, @@ -233,7 +234,7 @@ describe("", () => { ], }); - expect(screen.getByTestId("mfa-assertion-form")).toBeInTheDocument(); + expect(screen.getByTestId("mfa-assertion-screen")).toBeInTheDocument(); expect(screen.queryByRole("button", { name: "Sign in" })).not.toBeInTheDocument(); }); @@ -241,13 +242,13 @@ describe("", () => { const { injectUI } = require("../../../provider"); injectUI.mockImplementation(() => { return () => ({ - multiFactorResolver: { hints: [] }, + multiFactorResolver: { auth: {}, session: null, hints: [] }, }); }); - TestBed.overrideComponent(MultiFactorAuthAssertionFormComponent, { + TestBed.overrideComponent(MultiFactorAuthAssertionScreenComponent, { set: { - template: '
MFA Assertion Form
', + template: '
MFA Assertion Screen
', }, }); @@ -256,7 +257,7 @@ describe("", () => { SignInAuthScreenComponent, MockSignInAuthFormComponent, MockRedirectErrorComponent, - MultiFactorAuthAssertionFormComponent, + MultiFactorAuthAssertionScreenComponent, CardComponent, CardHeaderComponent, CardTitleComponent, @@ -266,41 +267,29 @@ describe("", () => { }); expect(screen.queryByRole("button", { name: "Sign in" })).not.toBeInTheDocument(); - expect(screen.getByTestId("mfa-assertion-form")).toBeInTheDocument(); + expect(screen.getByTestId("mfa-assertion-screen")).toBeInTheDocument(); }); it("emits signIn with credential when MFA flow succeeds", async () => { const { injectUI } = require("../../../provider"); injectUI.mockImplementation(() => { return () => ({ - multiFactorResolver: { hints: [{ factorId: TotpMultiFactorGenerator.FACTOR_ID, uid: "test" }] }, + multiFactorResolver: { auth: {}, session: null, hints: [{ factorId: TotpMultiFactorGenerator.FACTOR_ID, uid: "test" }] }, }); }); - TestBed.overrideComponent(TotpMultiFactorAssertionFormComponent, { + TestBed.overrideComponent(MultiFactorAuthAssertionScreenComponent, { set: { - template: - '
TOTP
', + template: '
MFA Assertion Screen
', }, }); - const signInHandler = jest.fn(); - - @Component({ - template: ``, - standalone: true, - imports: [SignInAuthScreenComponent], - }) - class HostCaptureComponent { - onSignIn = signInHandler; - } - - await render(HostCaptureComponent, { + const { fixture } = await render(TestHostWithoutContentComponent, { imports: [ SignInAuthScreenComponent, MockSignInAuthFormComponent, MockRedirectErrorComponent, - MultiFactorAuthAssertionFormComponent, + MultiFactorAuthAssertionScreenComponent, CardComponent, CardHeaderComponent, CardTitleComponent, @@ -309,11 +298,16 @@ describe("", () => { ], }); - const trigger = screen.getByTestId("mfa-on-success"); - trigger.dispatchEvent(new MouseEvent("click", { bubbles: true })); + const component = fixture.debugElement.query((el) => el.name === "fui-sign-in-auth-screen").componentInstance; + const signInSpy = jest.spyOn(component.signIn, "emit"); + + const mfaScreenComponent = fixture.debugElement.query( + (el) => el.name === "fui-multi-factor-auth-assertion-screen" + ).componentInstance; + mfaScreenComponent.onSuccess.emit({ user: { uid: "angular-mfa-user" } }); - expect(signInHandler).toHaveBeenCalled(); - expect(signInHandler).toHaveBeenCalledWith( + expect(signInSpy).toHaveBeenCalledTimes(1); + expect(signInSpy).toHaveBeenCalledWith( expect.objectContaining({ user: expect.objectContaining({ uid: "angular-mfa-user" }) }) ); }); diff --git a/packages/angular/src/lib/auth/screens/sign-in-auth-screen.ts b/packages/angular/src/lib/auth/screens/sign-in-auth-screen.ts index abf254812..2cd61d88a 100644 --- a/packages/angular/src/lib/auth/screens/sign-in-auth-screen.ts +++ b/packages/angular/src/lib/auth/screens/sign-in-auth-screen.ts @@ -19,7 +19,7 @@ import { CommonModule } from "@angular/common"; import { injectTranslation, injectUI } from "../../provider"; import { SignInAuthFormComponent } from "../forms/sign-in-auth-form"; -import { MultiFactorAuthAssertionFormComponent } from "../forms/multi-factor-auth-assertion-form"; +import { MultiFactorAuthAssertionScreenComponent } from "../screens/multi-factor-auth-assertion-screen"; import { RedirectErrorComponent } from "../../components/redirect-error"; import { CardComponent, @@ -40,20 +40,20 @@ import { UserCredential } from "@angular/fire/auth"; CardSubtitleComponent, CardContentComponent, SignInAuthFormComponent, - MultiFactorAuthAssertionFormComponent, + MultiFactorAuthAssertionScreenComponent, RedirectErrorComponent, ], template: ` -
- - - {{ titleText() }} - {{ subtitleText() }} - - - @if (mfaResolver()) { - - } @else { + @if (mfaResolver()) { + + } @else { +
+ + + {{ titleText() }} + {{ subtitleText() }} + + - } - - -
+
+
+
+ } `, }) export class SignInAuthScreenComponent { diff --git a/packages/angular/src/lib/auth/screens/sign-up-auth-screen.spec.ts b/packages/angular/src/lib/auth/screens/sign-up-auth-screen.spec.ts index f7879f315..b6ab33330 100644 --- a/packages/angular/src/lib/auth/screens/sign-up-auth-screen.spec.ts +++ b/packages/angular/src/lib/auth/screens/sign-up-auth-screen.spec.ts @@ -26,6 +26,7 @@ import { CardSubtitleComponent, CardContentComponent, } from "../../components/card"; +import { MultiFactorAuthAssertionScreenComponent } from "../screens/multi-factor-auth-assertion-screen"; import { MultiFactorAuthAssertionFormComponent } from "../forms/multi-factor-auth-assertion-form"; import { TotpMultiFactorAssertionFormComponent } from "../forms/mfa/totp-multi-factor-assertion-form"; import { TotpMultiFactorGenerator } from "firebase/auth"; @@ -90,7 +91,7 @@ describe("", () => { SignUpAuthScreenComponent, MockSignUpAuthFormComponent, MockRedirectErrorComponent, - MultiFactorAuthAssertionFormComponent, + MultiFactorAuthAssertionScreenComponent, CardComponent, CardHeaderComponent, CardTitleComponent, @@ -109,7 +110,7 @@ describe("", () => { SignUpAuthScreenComponent, MockSignUpAuthFormComponent, MockRedirectErrorComponent, - MultiFactorAuthAssertionFormComponent, + MultiFactorAuthAssertionScreenComponent, CardComponent, CardHeaderComponent, CardTitleComponent, @@ -128,7 +129,7 @@ describe("", () => { SignUpAuthScreenComponent, MockSignUpAuthFormComponent, MockRedirectErrorComponent, - MultiFactorAuthAssertionFormComponent, + MultiFactorAuthAssertionScreenComponent, CardComponent, CardHeaderComponent, CardTitleComponent, @@ -148,7 +149,7 @@ describe("", () => { SignUpAuthScreenComponent, MockSignUpAuthFormComponent, MockRedirectErrorComponent, - MultiFactorAuthAssertionFormComponent, + MultiFactorAuthAssertionScreenComponent, CardComponent, CardHeaderComponent, CardTitleComponent, @@ -167,7 +168,7 @@ describe("", () => { SignUpAuthScreenComponent, MockSignUpAuthFormComponent, MockRedirectErrorComponent, - MultiFactorAuthAssertionFormComponent, + MultiFactorAuthAssertionScreenComponent, CardComponent, CardHeaderComponent, CardTitleComponent, @@ -191,7 +192,7 @@ describe("", () => { SignUpAuthScreenComponent, MockSignUpAuthFormComponent, MockRedirectErrorComponent, - MultiFactorAuthAssertionFormComponent, + MultiFactorAuthAssertionScreenComponent, CardComponent, CardHeaderComponent, CardTitleComponent, @@ -204,17 +205,17 @@ describe("", () => { expect(injectTranslation).toHaveBeenCalledWith("prompts", "enterDetailsToCreate"); }); - it("renders MFA assertion form when multiFactorResolver is present", async () => { + it("renders MFA assertion screen when multiFactorResolver is present", async () => { const { injectUI } = require("../../../provider"); injectUI.mockImplementation(() => { return () => ({ - multiFactorResolver: { hints: [] }, + multiFactorResolver: { auth: {}, session: null, hints: [] }, }); }); - TestBed.overrideComponent(MultiFactorAuthAssertionFormComponent, { + TestBed.overrideComponent(MultiFactorAuthAssertionScreenComponent, { set: { - template: '
MFA Assertion Form
', + template: '
MFA Assertion Screen
', }, }); @@ -223,7 +224,7 @@ describe("", () => { SignUpAuthScreenComponent, MockSignUpAuthFormComponent, MockRedirectErrorComponent, - MultiFactorAuthAssertionFormComponent, + MultiFactorAuthAssertionScreenComponent, CardComponent, CardHeaderComponent, CardTitleComponent, @@ -232,7 +233,7 @@ describe("", () => { ], }); - expect(screen.getByTestId("mfa-assertion-form")).toBeInTheDocument(); + expect(screen.getByTestId("mfa-assertion-screen")).toBeInTheDocument(); expect(screen.queryByText("Sign Up Form")).not.toBeInTheDocument(); }); @@ -240,13 +241,13 @@ describe("", () => { const { injectUI } = require("../../../provider"); injectUI.mockImplementation(() => { return () => ({ - multiFactorResolver: { hints: [] }, + multiFactorResolver: { auth: {}, session: null, hints: [] }, }); }); - TestBed.overrideComponent(MultiFactorAuthAssertionFormComponent, { + TestBed.overrideComponent(MultiFactorAuthAssertionScreenComponent, { set: { - template: '
MFA Assertion Form
', + template: '
MFA Assertion Screen
', }, }); @@ -255,7 +256,7 @@ describe("", () => { SignUpAuthScreenComponent, MockSignUpAuthFormComponent, MockRedirectErrorComponent, - MultiFactorAuthAssertionFormComponent, + MultiFactorAuthAssertionScreenComponent, CardComponent, CardHeaderComponent, CardTitleComponent, @@ -265,41 +266,29 @@ describe("", () => { }); expect(screen.queryByText("Sign Up Form")).not.toBeInTheDocument(); - expect(screen.getByTestId("mfa-assertion-form")).toBeInTheDocument(); + expect(screen.getByTestId("mfa-assertion-screen")).toBeInTheDocument(); }); it("emits signUp with credential when MFA flow succeeds", async () => { const { injectUI } = require("../../../provider"); injectUI.mockImplementation(() => { return () => ({ - multiFactorResolver: { hints: [{ factorId: TotpMultiFactorGenerator.FACTOR_ID, uid: "test" }] }, + multiFactorResolver: { auth: {}, session: null, hints: [{ factorId: TotpMultiFactorGenerator.FACTOR_ID, uid: "test" }] }, }); }); - TestBed.overrideComponent(TotpMultiFactorAssertionFormComponent, { + TestBed.overrideComponent(MultiFactorAuthAssertionScreenComponent, { set: { - template: - '
TOTP
', + template: '
MFA Assertion Screen
', }, }); - const signUpHandler = jest.fn(); - - @Component({ - template: ``, - standalone: true, - imports: [SignUpAuthScreenComponent], - }) - class HostCaptureComponent { - onSignUp = signUpHandler; - } - - await render(HostCaptureComponent, { + const { fixture } = await render(TestHostWithoutContentComponent, { imports: [ SignUpAuthScreenComponent, MockSignUpAuthFormComponent, MockRedirectErrorComponent, - MultiFactorAuthAssertionFormComponent, + MultiFactorAuthAssertionScreenComponent, CardComponent, CardHeaderComponent, CardTitleComponent, @@ -308,11 +297,16 @@ describe("", () => { ], }); - const trigger = screen.getByTestId("mfa-on-success"); - trigger.dispatchEvent(new MouseEvent("click", { bubbles: true })); + const component = fixture.debugElement.query((el) => el.name === "fui-sign-up-auth-screen").componentInstance; + const signUpSpy = jest.spyOn(component.signUp, "emit"); + + const mfaScreenComponent = fixture.debugElement.query( + (el) => el.name === "fui-multi-factor-auth-assertion-screen" + ).componentInstance; + mfaScreenComponent.onSuccess.emit({ user: { uid: "angular-signup-mfa-user" } }); - expect(signUpHandler).toHaveBeenCalled(); - expect(signUpHandler).toHaveBeenCalledWith( + expect(signUpSpy).toHaveBeenCalledTimes(1); + expect(signUpSpy).toHaveBeenCalledWith( expect.objectContaining({ user: expect.objectContaining({ uid: "angular-signup-mfa-user" }) }) ); }); diff --git a/packages/angular/src/lib/auth/screens/sign-up-auth-screen.ts b/packages/angular/src/lib/auth/screens/sign-up-auth-screen.ts index ce206f4a1..ef103a313 100644 --- a/packages/angular/src/lib/auth/screens/sign-up-auth-screen.ts +++ b/packages/angular/src/lib/auth/screens/sign-up-auth-screen.ts @@ -20,7 +20,7 @@ import { UserCredential } from "@angular/fire/auth"; import { injectTranslation, injectUI } from "../../provider"; import { SignUpAuthFormComponent } from "../forms/sign-up-auth-form"; -import { MultiFactorAuthAssertionFormComponent } from "../forms/multi-factor-auth-assertion-form"; +import { MultiFactorAuthAssertionScreenComponent } from "../screens/multi-factor-auth-assertion-screen"; import { RedirectErrorComponent } from "../../components/redirect-error"; import { CardComponent, @@ -41,27 +41,27 @@ import { CardSubtitleComponent, CardContentComponent, SignUpAuthFormComponent, - MultiFactorAuthAssertionFormComponent, + MultiFactorAuthAssertionScreenComponent, RedirectErrorComponent, ], template: ` -
- - - {{ titleText() }} - {{ subtitleText() }} - - - @if (mfaResolver()) { - - } @else { + @if (mfaResolver()) { + + } @else { +
+ + + {{ titleText() }} + {{ subtitleText() }} + + - } - - -
+
+
+
+ } `, }) export class SignUpAuthScreenComponent { diff --git a/packages/angular/src/public-api.ts b/packages/angular/src/public-api.ts index 22d9199bc..d5cd736b9 100644 --- a/packages/angular/src/public-api.ts +++ b/packages/angular/src/public-api.ts @@ -41,6 +41,7 @@ export { OAuthButtonComponent } from "./lib/auth/oauth/oauth-button"; export { EmailLinkAuthScreenComponent } from "./lib/auth/screens/email-link-auth-screen"; export { ForgotPasswordAuthScreenComponent } from "./lib/auth/screens/forgot-password-auth-screen"; +export { MultiFactorAuthAssertionScreenComponent } from "./lib/auth/screens/multi-factor-auth-assertion-screen"; export { OAuthScreenComponent } from "./lib/auth/screens/oauth-screen"; export { PhoneAuthScreenComponent } from "./lib/auth/screens/phone-auth-screen"; export { SignInAuthScreenComponent } from "./lib/auth/screens/sign-in-auth-screen"; From b545c11e39d32fc44a602ecc26ba4a62ac6b0dfa Mon Sep 17 00:00:00 2001 From: Elliot Hesp Date: Sat, 8 Nov 2025 14:40:32 +0000 Subject: [PATCH 02/32] fix(angular): Remove redirect error from forgot password --- .../forgot-password-auth-screen.spec.ts | 29 ------------------- .../screens/forgot-password-auth-screen.ts | 3 -- 2 files changed, 32 deletions(-) diff --git a/packages/angular/src/lib/auth/screens/forgot-password-auth-screen.spec.ts b/packages/angular/src/lib/auth/screens/forgot-password-auth-screen.spec.ts index 6c5fa7352..fbff9cdab 100644 --- a/packages/angular/src/lib/auth/screens/forgot-password-auth-screen.spec.ts +++ b/packages/angular/src/lib/auth/screens/forgot-password-auth-screen.spec.ts @@ -33,13 +33,6 @@ import { }) class MockForgotPasswordAuthFormComponent {} -@Component({ - selector: "fui-redirect-error", - template: '
Redirect Error
', - standalone: true, -}) -class MockRedirectErrorComponent {} - describe("", () => { beforeEach(() => { const { injectTranslation } = require("../../../provider"); @@ -61,7 +54,6 @@ describe("", () => { imports: [ ForgotPasswordAuthScreenComponent, MockForgotPasswordAuthFormComponent, - MockRedirectErrorComponent, CardComponent, CardHeaderComponent, CardTitleComponent, @@ -79,7 +71,6 @@ describe("", () => { imports: [ ForgotPasswordAuthScreenComponent, MockForgotPasswordAuthFormComponent, - MockRedirectErrorComponent, CardComponent, CardHeaderComponent, CardTitleComponent, @@ -92,30 +83,11 @@ describe("", () => { expect(form).toBeInTheDocument(); }); - it("renders RedirectError component in children section when no MFA resolver", async () => { - const { container } = await render(ForgotPasswordAuthScreenComponent, { - imports: [ - ForgotPasswordAuthScreenComponent, - MockForgotPasswordAuthFormComponent, - MockRedirectErrorComponent, - CardComponent, - CardHeaderComponent, - CardTitleComponent, - CardSubtitleComponent, - CardContentComponent, - ], - }); - - const redirectErrorElement = container.querySelector("fui-redirect-error"); - expect(redirectErrorElement).toBeInTheDocument(); - }); - it("has correct CSS classes", async () => { const { container } = await render(ForgotPasswordAuthScreenComponent, { imports: [ ForgotPasswordAuthScreenComponent, MockForgotPasswordAuthFormComponent, - MockRedirectErrorComponent, CardComponent, CardHeaderComponent, CardTitleComponent, @@ -138,7 +110,6 @@ describe("", () => { imports: [ ForgotPasswordAuthScreenComponent, MockForgotPasswordAuthFormComponent, - MockRedirectErrorComponent, CardComponent, CardHeaderComponent, CardTitleComponent, diff --git a/packages/angular/src/lib/auth/screens/forgot-password-auth-screen.ts b/packages/angular/src/lib/auth/screens/forgot-password-auth-screen.ts index d3a0bd650..d8c8f4d23 100644 --- a/packages/angular/src/lib/auth/screens/forgot-password-auth-screen.ts +++ b/packages/angular/src/lib/auth/screens/forgot-password-auth-screen.ts @@ -25,7 +25,6 @@ import { } from "../../components/card"; import { injectTranslation } from "../../provider"; import { ForgotPasswordAuthFormComponent } from "../forms/forgot-password-auth-form"; -import { RedirectErrorComponent } from "../../components/redirect-error"; @Component({ selector: "fui-forgot-password-auth-screen", @@ -38,7 +37,6 @@ import { RedirectErrorComponent } from "../../components/redirect-error"; CardSubtitleComponent, CardContentComponent, ForgotPasswordAuthFormComponent, - RedirectErrorComponent, ], template: `
@@ -49,7 +47,6 @@ import { RedirectErrorComponent } from "../../components/redirect-error"; -
From 8ad393e6fee8bc4ffde1c3f271794f294a628ca6 Mon Sep 17 00:00:00 2001 From: Elliot Hesp Date: Sat, 8 Nov 2025 14:42:03 +0000 Subject: [PATCH 03/32] fix(angular): Align MFA Assertion screen --- ...multi-factor-auth-assertion-screen.spec.ts | 58 ------------------- .../multi-factor-auth-assertion-screen.ts | 5 -- 2 files changed, 63 deletions(-) diff --git a/packages/angular/src/lib/auth/screens/multi-factor-auth-assertion-screen.spec.ts b/packages/angular/src/lib/auth/screens/multi-factor-auth-assertion-screen.spec.ts index dfa4f5f62..83b3bd1df 100644 --- a/packages/angular/src/lib/auth/screens/multi-factor-auth-assertion-screen.spec.ts +++ b/packages/angular/src/lib/auth/screens/multi-factor-auth-assertion-screen.spec.ts @@ -27,24 +27,6 @@ import { CardContentComponent, } from "../../components/card"; -@Component({ - selector: "fui-redirect-error", - template: '
Redirect Error
', - standalone: true, -}) -class MockRedirectErrorComponent {} - -@Component({ - template: ` - -
Test Content
-
- `, - standalone: true, - imports: [MultiFactorAuthAssertionScreenComponent], -}) -class TestHostWithContentComponent {} - @Component({ template: ``, standalone: true, @@ -80,7 +62,6 @@ describe("", () => { await render(TestHostWithoutContentComponent, { imports: [ MultiFactorAuthAssertionScreenComponent, - MockRedirectErrorComponent, CardComponent, CardHeaderComponent, CardTitleComponent, @@ -103,7 +84,6 @@ describe("", () => { await render(TestHostWithoutContentComponent, { imports: [ MultiFactorAuthAssertionScreenComponent, - MockRedirectErrorComponent, CardComponent, CardHeaderComponent, CardTitleComponent, @@ -117,46 +97,10 @@ describe("", () => { expect(form).toHaveTextContent("MFA Assertion Form"); }); - it("renders projected content when provided", async () => { - await render(TestHostWithContentComponent, { - imports: [ - MultiFactorAuthAssertionScreenComponent, - MockRedirectErrorComponent, - CardComponent, - CardHeaderComponent, - CardTitleComponent, - CardSubtitleComponent, - CardContentComponent, - ], - }); - - const projectedContent = screen.getByTestId("projected-content"); - expect(projectedContent).toBeInTheDocument(); - expect(projectedContent).toHaveTextContent("Test Content"); - }); - - it("renders RedirectError component", async () => { - const { container } = await render(TestHostWithContentComponent, { - imports: [ - MultiFactorAuthAssertionScreenComponent, - MockRedirectErrorComponent, - CardComponent, - CardHeaderComponent, - CardTitleComponent, - CardSubtitleComponent, - CardContentComponent, - ], - }); - - const redirectErrorElement = container.querySelector("fui-redirect-error"); - expect(redirectErrorElement).toBeInTheDocument(); - }); - it("has correct CSS classes", async () => { const { container } = await render(TestHostWithoutContentComponent, { imports: [ MultiFactorAuthAssertionScreenComponent, - MockRedirectErrorComponent, CardComponent, CardHeaderComponent, CardTitleComponent, @@ -178,7 +122,6 @@ describe("", () => { await render(TestHostWithoutContentComponent, { imports: [ MultiFactorAuthAssertionScreenComponent, - MockRedirectErrorComponent, CardComponent, CardHeaderComponent, CardTitleComponent, @@ -201,7 +144,6 @@ describe("", () => { const { fixture } = await render(TestHostWithoutContentComponent, { imports: [ MultiFactorAuthAssertionScreenComponent, - MockRedirectErrorComponent, CardComponent, CardHeaderComponent, CardTitleComponent, diff --git a/packages/angular/src/lib/auth/screens/multi-factor-auth-assertion-screen.ts b/packages/angular/src/lib/auth/screens/multi-factor-auth-assertion-screen.ts index a41da9afd..0aec82ebf 100644 --- a/packages/angular/src/lib/auth/screens/multi-factor-auth-assertion-screen.ts +++ b/packages/angular/src/lib/auth/screens/multi-factor-auth-assertion-screen.ts @@ -19,7 +19,6 @@ import { CommonModule } from "@angular/common"; import { UserCredential } from "@angular/fire/auth"; import { injectTranslation } from "../../provider"; import { MultiFactorAuthAssertionFormComponent } from "../forms/multi-factor-auth-assertion-form"; -import { RedirectErrorComponent } from "../../components/redirect-error"; import { CardComponent, CardHeaderComponent, @@ -39,7 +38,6 @@ import { CardSubtitleComponent, CardContentComponent, MultiFactorAuthAssertionFormComponent, - RedirectErrorComponent, ], template: `
@@ -50,8 +48,6 @@ import { - -
@@ -63,4 +59,3 @@ export class MultiFactorAuthAssertionScreenComponent { titleText = injectTranslation("labels", "multiFactorAssertion"); subtitleText = injectTranslation("prompts", "mfaAssertionPrompt"); } - From 2490dfec2b2ca379c0a816d1d24092234dc85743 Mon Sep 17 00:00:00 2001 From: Elliot Hesp Date: Sat, 8 Nov 2025 14:43:21 +0000 Subject: [PATCH 04/32] fix(angular): Align MFA enrollment screen --- ...ulti-factor-auth-enrollment-screen.spec.ts | 61 ------------------- .../multi-factor-auth-enrollment-screen.ts | 4 -- 2 files changed, 65 deletions(-) diff --git a/packages/angular/src/lib/auth/screens/multi-factor-auth-enrollment-screen.spec.ts b/packages/angular/src/lib/auth/screens/multi-factor-auth-enrollment-screen.spec.ts index abf321446..abe68f671 100644 --- a/packages/angular/src/lib/auth/screens/multi-factor-auth-enrollment-screen.spec.ts +++ b/packages/angular/src/lib/auth/screens/multi-factor-auth-enrollment-screen.spec.ts @@ -33,24 +33,6 @@ import { FactorId } from "firebase/auth"; }) class MockMultiFactorAuthEnrollmentFormComponent {} -@Component({ - selector: "fui-redirect-error", - template: '
Redirect Error
', - standalone: true, -}) -class MockRedirectErrorComponent {} - -@Component({ - template: ` - -
Test Content
-
- `, - standalone: true, - imports: [MultiFactorAuthEnrollmentScreenComponent], -}) -class TestHostWithContentComponent {} - @Component({ template: ``, standalone: true, @@ -79,7 +61,6 @@ describe("", () => { imports: [ MultiFactorAuthEnrollmentScreenComponent, MockMultiFactorAuthEnrollmentFormComponent, - MockRedirectErrorComponent, CardComponent, CardHeaderComponent, CardTitleComponent, @@ -97,7 +78,6 @@ describe("", () => { imports: [ MultiFactorAuthEnrollmentScreenComponent, MockMultiFactorAuthEnrollmentFormComponent, - MockRedirectErrorComponent, CardComponent, CardHeaderComponent, CardTitleComponent, @@ -111,49 +91,11 @@ describe("", () => { expect(form.parentElement).toHaveTextContent("labels.mfaTotpVerification labels.mfaSmsVerification"); }); - it("renders projected content when provided", async () => { - await render(TestHostWithContentComponent, { - imports: [ - MultiFactorAuthEnrollmentScreenComponent, - MockMultiFactorAuthEnrollmentFormComponent, - MockRedirectErrorComponent, - CardComponent, - CardHeaderComponent, - CardTitleComponent, - CardSubtitleComponent, - CardContentComponent, - ], - }); - - const projectedContent = screen.getByTestId("projected-content"); - expect(projectedContent).toBeInTheDocument(); - expect(projectedContent).toHaveTextContent("Test Content"); - }); - - it("renders RedirectError component", async () => { - const { container } = await render(TestHostWithContentComponent, { - imports: [ - MultiFactorAuthEnrollmentScreenComponent, - MockMultiFactorAuthEnrollmentFormComponent, - MockRedirectErrorComponent, - CardComponent, - CardHeaderComponent, - CardTitleComponent, - CardSubtitleComponent, - CardContentComponent, - ], - }); - - const redirectErrorElement = container.querySelector("fui-redirect-error"); - expect(redirectErrorElement).toBeInTheDocument(); - }); - it("has correct CSS classes", async () => { const { container } = await render(TestHostWithoutContentComponent, { imports: [ MultiFactorAuthEnrollmentScreenComponent, MockMultiFactorAuthEnrollmentFormComponent, - MockRedirectErrorComponent, CardComponent, CardHeaderComponent, CardTitleComponent, @@ -176,7 +118,6 @@ describe("", () => { imports: [ MultiFactorAuthEnrollmentScreenComponent, MockMultiFactorAuthEnrollmentFormComponent, - MockRedirectErrorComponent, CardComponent, CardHeaderComponent, CardTitleComponent, @@ -194,7 +135,6 @@ describe("", () => { imports: [ MultiFactorAuthEnrollmentScreenComponent, MockMultiFactorAuthEnrollmentFormComponent, - MockRedirectErrorComponent, CardComponent, CardHeaderComponent, CardTitleComponent, @@ -215,7 +155,6 @@ describe("", () => { imports: [ MultiFactorAuthEnrollmentScreenComponent, MockMultiFactorAuthEnrollmentFormComponent, - MockRedirectErrorComponent, CardComponent, CardHeaderComponent, CardTitleComponent, diff --git a/packages/angular/src/lib/auth/screens/multi-factor-auth-enrollment-screen.ts b/packages/angular/src/lib/auth/screens/multi-factor-auth-enrollment-screen.ts index 8d5c9eb77..ed82f15c6 100644 --- a/packages/angular/src/lib/auth/screens/multi-factor-auth-enrollment-screen.ts +++ b/packages/angular/src/lib/auth/screens/multi-factor-auth-enrollment-screen.ts @@ -19,7 +19,6 @@ import { CommonModule } from "@angular/common"; import { FactorId } from "firebase/auth"; import { injectTranslation } from "../../provider"; import { MultiFactorAuthEnrollmentFormComponent } from "../forms/multi-factor-auth-enrollment-form"; -import { RedirectErrorComponent } from "../../components/redirect-error"; import { CardComponent, CardHeaderComponent, @@ -41,7 +40,6 @@ type Hint = (typeof FactorId)[keyof typeof FactorId]; CardSubtitleComponent, CardContentComponent, MultiFactorAuthEnrollmentFormComponent, - RedirectErrorComponent, ], template: `
@@ -52,8 +50,6 @@ type Hint = (typeof FactorId)[keyof typeof FactorId]; - -
From 109ec38df31db8561065764f54bdcec5d64c2031 Mon Sep 17 00:00:00 2001 From: Elliot Hesp Date: Sat, 8 Nov 2025 14:45:07 +0000 Subject: [PATCH 05/32] fix(angular): Sync phone auth screen --- packages/angular/src/lib/auth/screens/phone-auth-screen.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/angular/src/lib/auth/screens/phone-auth-screen.ts b/packages/angular/src/lib/auth/screens/phone-auth-screen.ts index e7dfacf5e..ae523e4bc 100644 --- a/packages/angular/src/lib/auth/screens/phone-auth-screen.ts +++ b/packages/angular/src/lib/auth/screens/phone-auth-screen.ts @@ -55,8 +55,8 @@ import { UserCredential } from "@angular/fire/auth"; - + @@ -71,6 +71,5 @@ export class PhoneAuthScreenComponent { titleText = injectTranslation("labels", "signIn"); subtitleText = injectTranslation("prompts", "signInToAccount"); - resendDelay = input(30); signIn = output(); } From e6d443ec0b05f9c8fbbdfa24d96cc33c1cea8576 Mon Sep 17 00:00:00 2001 From: Elliot Hesp Date: Sat, 8 Nov 2025 14:45:53 +0000 Subject: [PATCH 06/32] fix(angular): Align sign in screen --- packages/angular/src/lib/auth/screens/sign-in-auth-screen.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/angular/src/lib/auth/screens/sign-in-auth-screen.ts b/packages/angular/src/lib/auth/screens/sign-in-auth-screen.ts index 2cd61d88a..9b1ef4b9b 100644 --- a/packages/angular/src/lib/auth/screens/sign-in-auth-screen.ts +++ b/packages/angular/src/lib/auth/screens/sign-in-auth-screen.ts @@ -59,8 +59,8 @@ import { UserCredential } from "@angular/fire/auth"; (signUp)="signUp.emit()" (signIn)="signIn.emit($event)" /> - + From 9972d686ef15529247d79578351fe29ec8048fa2 Mon Sep 17 00:00:00 2001 From: Elliot Hesp Date: Sat, 8 Nov 2025 14:46:39 +0000 Subject: [PATCH 07/32] fix(angular): Align sign up screen --- packages/angular/src/lib/auth/screens/sign-up-auth-screen.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/angular/src/lib/auth/screens/sign-up-auth-screen.ts b/packages/angular/src/lib/auth/screens/sign-up-auth-screen.ts index ef103a313..260f2f447 100644 --- a/packages/angular/src/lib/auth/screens/sign-up-auth-screen.ts +++ b/packages/angular/src/lib/auth/screens/sign-up-auth-screen.ts @@ -56,8 +56,8 @@ import { - + From 7b4ffee1bbb4e90c6bc1f81973d5e12eae783e25 Mon Sep 17 00:00:00 2001 From: Elliot Hesp Date: Sat, 8 Nov 2025 14:51:10 +0000 Subject: [PATCH 08/32] feat(angular): Add input form description --- .../angular/src/lib/components/form.spec.ts | 61 +++++++++++++++++++ packages/angular/src/lib/components/form.ts | 4 ++ 2 files changed, 65 insertions(+) diff --git a/packages/angular/src/lib/components/form.spec.ts b/packages/angular/src/lib/components/form.spec.ts index b6845b2db..34372808e 100644 --- a/packages/angular/src/lib/components/form.spec.ts +++ b/packages/angular/src/lib/components/form.spec.ts @@ -189,6 +189,28 @@ describe("Form Components", () => { }); } + @Component({ + template: ` + + `, + standalone: true, + imports: [FormInputComponent, TanStackAppField], + }) + class TestFormInputWithDescriptionHostComponent { + form = injectForm({ + defaultValues: { + test: "", + }, + }); + description = signal(undefined); + } + it("renders action content when provided", async () => { await render(TestFormInputHostComponent, { imports: [TestFormInputHostComponent], @@ -198,6 +220,45 @@ describe("Form Components", () => { expect(actionButton).toBeTruthy(); expect(actionButton).toHaveTextContent("Action"); }); + + it("renders description when provided", async () => { + const component = await render(TestFormInputWithDescriptionHostComponent, { + imports: [TestFormInputWithDescriptionHostComponent], + }); + + component.fixture.componentInstance.description.set("Test description text"); + component.fixture.detectChanges(); + + const descriptionElement = screen.getByText("Test description text"); + expect(descriptionElement).toBeTruthy(); + expect(descriptionElement).toHaveAttribute("data-input-description"); + }); + + it("does not render description when not provided", async () => { + const { container } = await render(TestFormInputWithDescriptionHostComponent, { + imports: [TestFormInputWithDescriptionHostComponent], + }); + + const descriptionElement = container.querySelector("[data-input-description]"); + expect(descriptionElement).toBeFalsy(); + }); + + it("updates description when input changes", async () => { + const component = await render(TestFormInputWithDescriptionHostComponent, { + imports: [TestFormInputWithDescriptionHostComponent], + }); + + component.fixture.componentInstance.description.set("Initial description"); + component.fixture.detectChanges(); + + expect(screen.getByText("Initial description")).toBeTruthy(); + + component.fixture.componentInstance.description.set("Updated description"); + component.fixture.detectChanges(); + + expect(screen.queryByText("Initial description")).toBeFalsy(); + expect(screen.getByText("Updated description")).toBeTruthy(); + }); }); describe("", () => { diff --git a/packages/angular/src/lib/components/form.ts b/packages/angular/src/lib/components/form.ts index dbf9fc638..77a68a9d7 100644 --- a/packages/angular/src/lib/components/form.ts +++ b/packages/angular/src/lib/components/form.ts @@ -34,6 +34,9 @@ export class FormMetadataComponent {
{{ label() }}
+ @if (description()) { +
{{ description() }}
+ }
(); label = input.required(); type = input("text"); + description = input(); } @Component({ From 14efac1b68ed10074f9f94d7be47886e61ae256f Mon Sep 17 00:00:00 2001 From: Elliot Hesp Date: Sat, 8 Nov 2025 14:53:49 +0000 Subject: [PATCH 09/32] fix(angular): Align email link form --- .../angular/src/lib/auth/forms/email-link-auth-form.spec.ts | 4 +++- packages/angular/src/lib/auth/forms/email-link-auth-form.ts | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/angular/src/lib/auth/forms/email-link-auth-form.spec.ts b/packages/angular/src/lib/auth/forms/email-link-auth-form.spec.ts index 57673edaa..a1c9a5edc 100644 --- a/packages/angular/src/lib/auth/forms/email-link-auth-form.spec.ts +++ b/packages/angular/src/lib/auth/forms/email-link-auth-form.spec.ts @@ -224,7 +224,9 @@ describe("", () => { component.emailSentState.set(true); fixture.detectChanges(); - expect(screen.getByText("Check your email for a sign in link")).toBeInTheDocument(); + const successMessage = screen.getByText("Check your email for a sign in link"); + expect(successMessage).toBeInTheDocument(); + expect(successMessage).toHaveClass("fui-success"); }); it("should handle FirebaseUIError and display error message", async () => { diff --git a/packages/angular/src/lib/auth/forms/email-link-auth-form.ts b/packages/angular/src/lib/auth/forms/email-link-auth-form.ts index 320e7b028..13ff011c2 100644 --- a/packages/angular/src/lib/auth/forms/email-link-auth-form.ts +++ b/packages/angular/src/lib/auth/forms/email-link-auth-form.ts @@ -38,7 +38,7 @@ import { injectEmailLinkAuthFormSchema, injectTranslation, injectUI } from "../. ], template: ` @if (emailSentState()) { -
+
{{ emailSentMessage() }}
} @@ -107,6 +107,7 @@ export class EmailLinkAuthFormComponent { return error.message; } + console.error(error); return this.unknownErrorLabel(); } }, From df5a5d6a410c94b2334c97efe8195e30e4009d6f Mon Sep 17 00:00:00 2001 From: Elliot Hesp Date: Sat, 8 Nov 2025 14:55:14 +0000 Subject: [PATCH 10/32] fix(angular): Align forgot password form --- .../src/lib/auth/forms/forgot-password-auth-form.spec.ts | 4 +++- .../angular/src/lib/auth/forms/forgot-password-auth-form.ts | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/angular/src/lib/auth/forms/forgot-password-auth-form.spec.ts b/packages/angular/src/lib/auth/forms/forgot-password-auth-form.spec.ts index 753a83651..048b0860d 100644 --- a/packages/angular/src/lib/auth/forms/forgot-password-auth-form.spec.ts +++ b/packages/angular/src/lib/auth/forms/forgot-password-auth-form.spec.ts @@ -266,7 +266,9 @@ describe("", () => { component.emailSent.set(true); fixture.detectChanges(); - expect(screen.getByText("Check your email for a password reset link")).toBeInTheDocument(); + const successMessage = screen.getByText("Check your email for a password reset link"); + expect(successMessage).toBeInTheDocument(); + expect(successMessage).toHaveClass("fui-success"); }); it("should handle FirebaseUIError and display error message", async () => { diff --git a/packages/angular/src/lib/auth/forms/forgot-password-auth-form.ts b/packages/angular/src/lib/auth/forms/forgot-password-auth-form.ts index d91f13e37..af79f4cf0 100644 --- a/packages/angular/src/lib/auth/forms/forgot-password-auth-form.ts +++ b/packages/angular/src/lib/auth/forms/forgot-password-auth-form.ts @@ -43,7 +43,7 @@ import { injectForgotPasswordAuthFormSchema, injectTranslation, injectUI } from ], template: ` @if (emailSent()) { -
+
{{ checkEmailForResetMessage() }}
} @@ -115,6 +115,7 @@ export class ForgotPasswordAuthFormComponent { return error.message; } + console.error(error); return this.unknownErrorLabel(); } }, From b27a0fac71f69080fd1c6c75462cd6d505ccdde8 Mon Sep 17 00:00:00 2001 From: Elliot Hesp Date: Sat, 8 Nov 2025 15:00:01 +0000 Subject: [PATCH 11/32] fix(angular): Align MFA assertion form --- .../multi-factor-auth-assertion-form.spec.ts | 78 +++++++++++++++++++ .../forms/multi-factor-auth-assertion-form.ts | 23 ++++-- 2 files changed, 94 insertions(+), 7 deletions(-) diff --git a/packages/angular/src/lib/auth/forms/multi-factor-auth-assertion-form.spec.ts b/packages/angular/src/lib/auth/forms/multi-factor-auth-assertion-form.spec.ts index d37d323f5..246e0170d 100644 --- a/packages/angular/src/lib/auth/forms/multi-factor-auth-assertion-form.spec.ts +++ b/packages/angular/src/lib/auth/forms/multi-factor-auth-assertion-form.spec.ts @@ -51,6 +51,7 @@ describe("", () => { }, ], }, + setMultiFactorResolver: jest.fn(), }); }); }); @@ -91,6 +92,7 @@ describe("", () => { }, ], }, + setMultiFactorResolver: jest.fn(), }); }); @@ -145,6 +147,7 @@ describe("", () => { injectUI.mockImplementation(() => { return () => ({ multiFactorResolver: null, + setMultiFactorResolver: jest.fn(), }); }); @@ -154,4 +157,79 @@ describe("", () => { }) ).rejects.toThrow("MultiFactorAuthAssertionForm requires a multi-factor resolver"); }); + + it("calls setMultiFactorResolver on component destruction", async () => { + const { injectUI } = require("../../../provider"); + const setMultiFactorResolverSpy = jest.fn(); + injectUI.mockImplementation(() => { + return () => ({ + multiFactorResolver: { + hints: [ + { + factorId: PhoneMultiFactorGenerator.FACTOR_ID, + displayName: "Phone", + }, + ], + }, + setMultiFactorResolver: setMultiFactorResolverSpy, + }); + }); + + TestBed.overrideComponent(SmsMultiFactorAssertionFormComponent, { + set: { + template: '
SMS Assertion Form
', + }, + }); + + const { fixture } = await render(MultiFactorAuthAssertionFormComponent, { + imports: [MultiFactorAuthAssertionFormComponent], + }); + + expect(setMultiFactorResolverSpy).not.toHaveBeenCalled(); + + fixture.destroy(); + + expect(setMultiFactorResolverSpy).toHaveBeenCalledTimes(1); + }); + + it("clears multiFactorResolver when component is destroyed", async () => { + const { injectUI } = require("../../../provider"); + const mockResolver = { + hints: [ + { + factorId: PhoneMultiFactorGenerator.FACTOR_ID, + displayName: "Phone", + }, + ], + }; + let currentResolver: any = mockResolver; + const setMultiFactorResolverSpy = jest.fn((value?: any) => { + currentResolver = value; + }); + const uiMock = () => ({ + get multiFactorResolver() { + return currentResolver; + }, + setMultiFactorResolver: setMultiFactorResolverSpy, + }); + + injectUI.mockImplementation(() => uiMock); + + TestBed.overrideComponent(SmsMultiFactorAssertionFormComponent, { + set: { + template: '
SMS Assertion Form
', + }, + }); + + const { fixture } = await render(MultiFactorAuthAssertionFormComponent, { + imports: [MultiFactorAuthAssertionFormComponent], + }); + + expect(uiMock().multiFactorResolver).toBe(mockResolver); + + fixture.destroy(); + + expect(setMultiFactorResolverSpy).toHaveBeenCalledTimes(1); + expect(uiMock().multiFactorResolver).toBeUndefined(); + }); }); diff --git a/packages/angular/src/lib/auth/forms/multi-factor-auth-assertion-form.ts b/packages/angular/src/lib/auth/forms/multi-factor-auth-assertion-form.ts index ac59f54cb..656ed76f2 100644 --- a/packages/angular/src/lib/auth/forms/multi-factor-auth-assertion-form.ts +++ b/packages/angular/src/lib/auth/forms/multi-factor-auth-assertion-form.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Component, computed, output, signal } from "@angular/core"; +import { Component, computed, effect, output, signal } from "@angular/core"; import { CommonModule } from "@angular/common"; import { injectUI, injectTranslation } from "../../provider"; import { @@ -34,19 +34,19 @@ import { ButtonComponent } from "../../components/button"; template: `
@if (selectedHint()) { - @if (selectedHint()!.factorId === phoneFactorId()) { + @if (selectedHint()!.factorId === phoneFactorId) { - } @else if (selectedHint()!.factorId === totpFactorId()) { + } @else if (selectedHint()!.factorId === totpFactorId) { } } @else {

{{ mfaAssertionFactorPrompt() }}

@for (hint of resolver().hints; track hint.factorId) { - @if (hint.factorId === totpFactorId()) { + @if (hint.factorId === totpFactorId) { - } @else if (hint.factorId === phoneFactorId()) { + } @else if (hint.factorId === phoneFactorId) { @@ -59,6 +59,15 @@ import { ButtonComponent } from "../../components/button"; export class MultiFactorAuthAssertionFormComponent { private ui = injectUI(); + constructor() { + effect((onCleanup) => { + // Cleanup the multi-factor resolver when the component unmounts. + onCleanup(() => { + this.ui().setMultiFactorResolver(); + }); + }); + } + onSuccess = output(); resolver = computed(() => { @@ -73,8 +82,8 @@ export class MultiFactorAuthAssertionFormComponent { this.resolver().hints.length === 1 ? this.resolver().hints[0] : undefined ); - phoneFactorId = computed(() => PhoneMultiFactorGenerator.FACTOR_ID); - totpFactorId = computed(() => TotpMultiFactorGenerator.FACTOR_ID); + phoneFactorId = PhoneMultiFactorGenerator.FACTOR_ID; + totpFactorId = TotpMultiFactorGenerator.FACTOR_ID; smsVerificationLabel = injectTranslation("labels", "mfaSmsVerification"); totpVerificationLabel = injectTranslation("labels", "mfaTotpVerification"); From 0fdfab9371dfb3f232862025cfc7f3aaf9154c4a Mon Sep 17 00:00:00 2001 From: Elliot Hesp Date: Sat, 8 Nov 2025 15:02:26 +0000 Subject: [PATCH 12/32] fix(angular): Align MFA enrollment form --- .../multi-factor-auth-enrollment-form.spec.ts | 36 ++++++++++++++++++ .../multi-factor-auth-enrollment-form.ts | 38 ++++++++++++------- 2 files changed, 61 insertions(+), 13 deletions(-) diff --git a/packages/angular/src/lib/auth/forms/multi-factor-auth-enrollment-form.spec.ts b/packages/angular/src/lib/auth/forms/multi-factor-auth-enrollment-form.spec.ts index 8e5c6b05f..511458b85 100644 --- a/packages/angular/src/lib/auth/forms/multi-factor-auth-enrollment-form.spec.ts +++ b/packages/angular/src/lib/auth/forms/multi-factor-auth-enrollment-form.spec.ts @@ -190,4 +190,40 @@ describe("", () => { expect(container.querySelector(".fui-content")).toBeInTheDocument(); }); + + it("should throw error when hints array is empty", async () => { + await expect( + render(MultiFactorAuthEnrollmentFormComponent, { + imports: [ + CommonModule, + MultiFactorAuthEnrollmentFormComponent, + SmsMultiFactorEnrollmentFormComponent, + TotpMultiFactorEnrollmentFormComponent, + ButtonComponent, + ], + componentInputs: { + hints: [], + }, + }) + ).rejects.toThrow("MultiFactorAuthEnrollmentForm must have at least one hint"); + }); + + it("should throw error for unknown hint type", async () => { + const unknownHint = "unknown" as any; + + await expect( + render(MultiFactorAuthEnrollmentFormComponent, { + imports: [ + CommonModule, + MultiFactorAuthEnrollmentFormComponent, + SmsMultiFactorEnrollmentFormComponent, + TotpMultiFactorEnrollmentFormComponent, + ButtonComponent, + ], + componentInputs: { + hints: [unknownHint], + }, + }) + ).rejects.toThrow("Unknown multi-factor enrollment type: unknown"); + }); }); diff --git a/packages/angular/src/lib/auth/forms/multi-factor-auth-enrollment-form.ts b/packages/angular/src/lib/auth/forms/multi-factor-auth-enrollment-form.ts index 527116b21..dcee1bb0f 100644 --- a/packages/angular/src/lib/auth/forms/multi-factor-auth-enrollment-form.ts +++ b/packages/angular/src/lib/auth/forms/multi-factor-auth-enrollment-form.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Component, signal, input, output, OnInit } from "@angular/core"; +import { Component, signal, input, output, OnInit, computed } from "@angular/core"; import { CommonModule } from "@angular/common"; import { FactorId } from "firebase/auth"; import { injectTranslation } from "../../provider"; @@ -35,22 +35,22 @@ type Hint = (typeof FactorId)[keyof typeof FactorId]; ], template: `
- @if (selectedHint()) { - @if (selectedHint() === "phone") { + @if (validatedHint()) { + @if (validatedHint() === phoneFactorId) { - } @else if (selectedHint() === "totp") { + } @else if (validatedHint() === totpFactorId) { } } @else { @for (hint of hints(); track hint) { - @if (hint === "phone") { - - } @else if (hint === "totp") { - + } @else if (hint === phoneFactorId) { + } } } @@ -63,16 +63,28 @@ export class MultiFactorAuthEnrollmentFormComponent implements OnInit { selectedHint = signal(undefined); + phoneFactorId = FactorId.PHONE; + totpFactorId = FactorId.TOTP; + smsVerificationLabel = injectTranslation("labels", "mfaSmsVerification"); totpVerificationLabel = injectTranslation("labels", "mfaTotpVerification"); + validatedHint = computed(() => { + const hint = this.selectedHint(); + if (hint && hint !== this.phoneFactorId && hint !== this.totpFactorId) { + throw new Error(`Unknown multi-factor enrollment type: ${hint}`); + } + return hint; + }); + ngOnInit() { - // Auto-select single hint after component initialization const hints = this.hints(); + if (hints.length === 0) { + throw new Error("MultiFactorAuthEnrollmentForm must have at least one hint"); + } + // Auto-select single hint after component initialization if (hints.length === 1) { this.selectedHint.set(hints[0]); - } else if (hints.length === 0) { - throw new Error("MultiFactorAuthEnrollmentForm must have at least one hint"); } } From dbdbd2358e71b58e3300b8a122aa58fb81a82c94 Mon Sep 17 00:00:00 2001 From: Elliot Hesp Date: Sat, 8 Nov 2025 15:05:21 +0000 Subject: [PATCH 13/32] fix(angular): Align phone auth form --- .../angular/src/lib/auth/forms/phone-auth-form.spec.ts | 4 +++- packages/angular/src/lib/auth/forms/phone-auth-form.ts | 8 ++++++-- packages/angular/src/lib/tests/test-helpers.ts | 1 + 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/angular/src/lib/auth/forms/phone-auth-form.spec.ts b/packages/angular/src/lib/auth/forms/phone-auth-form.spec.ts index 88d984bdf..98e85ab14 100644 --- a/packages/angular/src/lib/auth/forms/phone-auth-form.spec.ts +++ b/packages/angular/src/lib/auth/forms/phone-auth-form.spec.ts @@ -126,7 +126,9 @@ describe("", () => { component.verificationId.set("test-verification-id"); fixture.detectChanges(); - expect(screen.getByLabelText("Verification Code")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByLabelText("Verification Code")).toBeInTheDocument(); + }); expect(screen.getByRole("button", { name: "Verify Code" })).toBeInTheDocument(); }); diff --git a/packages/angular/src/lib/auth/forms/phone-auth-form.ts b/packages/angular/src/lib/auth/forms/phone-auth-form.ts index 916590896..e7cdb938f 100644 --- a/packages/angular/src/lib/auth/forms/phone-auth-form.ts +++ b/packages/angular/src/lib/auth/forms/phone-auth-form.ts @@ -58,7 +58,8 @@ import { name="phoneNumber" tanstack-app-field [tanstackField]="form" - label="{{ phoneNumberLabel() }}" + [label]="phoneNumberLabel()" + type="tel" >
@@ -162,7 +163,9 @@ export class PhoneNumberFormComponent { name="verificationCode" tanstack-app-field [tanstackField]="form" - label="{{ verificationCodeLabel() }}" + [label]="verificationCodeLabel()" + [description]="smsVerificationPrompt()" + type="text" >
@@ -186,6 +189,7 @@ export class VerificationFormComponent { verificationCodeLabel = injectTranslation("labels", "verificationCode"); verifyCodeLabel = injectTranslation("labels", "verifyCode"); + smsVerificationPrompt = injectTranslation("prompts", "smsVerificationPrompt"); unknownErrorLabel = injectTranslation("errors", "unknownError"); form = injectForm({ diff --git a/packages/angular/src/lib/tests/test-helpers.ts b/packages/angular/src/lib/tests/test-helpers.ts index 8784ecb8b..4d6a977c0 100644 --- a/packages/angular/src/lib/tests/test-helpers.ts +++ b/packages/angular/src/lib/tests/test-helpers.ts @@ -129,6 +129,7 @@ export const injectTranslation = jest.fn().mockImplementation((category: string, noAccount: "Don't have an account?", signInToAccount: "Sign in to your account", haveAccount: "Already have an account?", + smsVerificationPrompt: "Enter the verification code sent to your phone number", }, errors: { unknownError: "An unknown error occurred", From 4ca47b577fa7a446328265b95cbcf4d0d5c190d7 Mon Sep 17 00:00:00 2001 From: Elliot Hesp Date: Sat, 8 Nov 2025 15:07:24 +0000 Subject: [PATCH 14/32] fix(angular): Align sign in form --- packages/angular/src/lib/auth/forms/sign-in-auth-form.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/angular/src/lib/auth/forms/sign-in-auth-form.ts b/packages/angular/src/lib/auth/forms/sign-in-auth-form.ts index 2760c6b86..820c06d69 100644 --- a/packages/angular/src/lib/auth/forms/sign-in-auth-form.ts +++ b/packages/angular/src/lib/auth/forms/sign-in-auth-form.ts @@ -49,7 +49,8 @@ import { name="email" tanstack-app-field [tanstackField]="form" - label="{{ emailLabel() }}" + [label]="emailLabel()" + type="email" >
@@ -57,7 +58,7 @@ import { name="password" tanstack-app-field [tanstackField]="form" - label="{{ passwordLabel() }}" + [label]="passwordLabel()" type="password" > @if (forgotPassword) { @@ -122,13 +123,14 @@ export class SignInAuthFormComponent { onSubmitAsync: async ({ value }) => { try { const credential = await signInWithEmailAndPassword(this.ui(), value.email, value.password); - this.signIn?.emit(credential); + this.signIn.emit(credential); return; } catch (error) { if (error instanceof FirebaseUIError) { return error.message; } + console.error(error); return this.unknownErrorLabel(); } }, From 9483de6ea9c3dd3a4ae5fbb8924391e4cfbe8bdc Mon Sep 17 00:00:00 2001 From: Elliot Hesp Date: Sat, 8 Nov 2025 15:09:36 +0000 Subject: [PATCH 15/32] fix(angular): Align sign up form --- .../src/lib/auth/forms/sign-up-auth-form.spec.ts | 4 ++-- .../angular/src/lib/auth/forms/sign-up-auth-form.ts | 13 +++++++------ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/angular/src/lib/auth/forms/sign-up-auth-form.spec.ts b/packages/angular/src/lib/auth/forms/sign-up-auth-form.spec.ts index b6c1b840f..2e6ccec6e 100644 --- a/packages/angular/src/lib/auth/forms/sign-up-auth-form.spec.ts +++ b/packages/angular/src/lib/auth/forms/sign-up-auth-form.spec.ts @@ -83,7 +83,7 @@ describe("", () => { expect(screen.queryByLabelText("Display Name")).toBeNull(); expect(screen.getByRole("button", { name: "Create Account" })).toBeInTheDocument(); expect(screen.getByText("By continuing, you agree to our")).toBeInTheDocument(); - expect(screen.getByRole("button", { name: "Already have an account? Sign In →" })).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Already have an account? Sign In" })).toBeInTheDocument(); }); it("should render display name field when hasBehavior returns true", async () => { @@ -171,7 +171,7 @@ describe("", () => { const component = fixture.componentInstance; const signInSpy = jest.spyOn(component.signIn, "emit"); - const signInButton = screen.getByRole("button", { name: "Already have an account? Sign In →" }); + const signInButton = screen.getByRole("button", { name: "Already have an account? Sign In" }); fireEvent.click(signInButton); expect(signInSpy).toHaveBeenCalled(); }); diff --git a/packages/angular/src/lib/auth/forms/sign-up-auth-form.ts b/packages/angular/src/lib/auth/forms/sign-up-auth-form.ts index 7e2f884c2..acbbb34f9 100644 --- a/packages/angular/src/lib/auth/forms/sign-up-auth-form.ts +++ b/packages/angular/src/lib/auth/forms/sign-up-auth-form.ts @@ -31,6 +31,7 @@ import { @Component({ selector: "fui-sign-up-auth-form", + standalone: true, imports: [ CommonModule, TanStackField, @@ -49,19 +50,19 @@ import { name="displayName" tanstack-app-field [tanstackField]="form" - label="{{ displayNameLabel() }}" + [label]="displayNameLabel()" />
}
- +
@@ -74,11 +75,10 @@ import { @if (signIn) { - + } `, - standalone: true, }) export class SignUpAuthFormComponent { private ui = injectUI(); @@ -128,13 +128,14 @@ export class SignUpAuthFormComponent { value.password, value.displayName ); - this.signUp?.emit(credential); + this.signUp.emit(credential); return; } catch (error) { if (error instanceof FirebaseUIError) { return error.message; } + console.error(error); return this.unknownErrorLabel(); } }, From cf9e39b6823291f74b679bf02e10af77b725d79e Mon Sep 17 00:00:00 2001 From: Elliot Hesp Date: Sat, 8 Nov 2025 15:14:10 +0000 Subject: [PATCH 16/32] fix(angular): Align SMS MFA assertion form --- .../sms-multi-factor-assertion-form.spec.ts | 80 ++++++++++++++++--- .../mfa/sms-multi-factor-assertion-form.ts | 57 +++++-------- 2 files changed, 90 insertions(+), 47 deletions(-) diff --git a/packages/angular/src/lib/auth/forms/mfa/sms-multi-factor-assertion-form.spec.ts b/packages/angular/src/lib/auth/forms/mfa/sms-multi-factor-assertion-form.spec.ts index 5a7915d52..e78cfc360 100644 --- a/packages/angular/src/lib/auth/forms/mfa/sms-multi-factor-assertion-form.spec.ts +++ b/packages/angular/src/lib/auth/forms/mfa/sms-multi-factor-assertion-form.spec.ts @@ -37,6 +37,31 @@ describe("", () => { injectRecaptchaVerifier, } = require("../../../tests/test-helpers"); + const { getTranslation } = require("@invertase/firebaseui-core"); + getTranslation.mockImplementation((ui: any, category: string, key: string, params?: any) => { + if (category === "messages" && key === "mfaSmsAssertionPrompt" && params) { + return `A verification code will be sent to ${params.phoneNumber} to complete the authentication process.`; + } + const mockTranslations: Record> = { + labels: { + phoneNumber: "Phone Number", + sendCode: "Send Code", + verificationCode: "Verification Code", + verifyCode: "Verify Code", + }, + messages: { + mfaSmsAssertionPrompt: "A verification code will be sent to {phoneNumber} to complete the authentication process.", + }, + prompts: { + smsVerificationPrompt: "Enter the verification code sent to your phone number", + }, + errors: { + unknownError: "An unknown error occurred", + }, + }; + return mockTranslations[category]?.[key] || `${category}.${key}`; + }); + injectTranslation.mockImplementation((category: string, key: string) => { const mockTranslations: Record> = { labels: { @@ -45,6 +70,12 @@ describe("", () => { verificationCode: "Verification Code", verifyCode: "Verify Code", }, + messages: { + mfaSmsAssertionPrompt: "A verification code will be sent to {phoneNumber} to complete the authentication process.", + }, + prompts: { + smsVerificationPrompt: "Enter the verification code sent to your phone number", + }, errors: { unknownError: "An unknown error occurred", }, @@ -102,8 +133,7 @@ describe("", () => { imports: [SmsMultiFactorAssertionFormComponent], }); - expect(screen.getByLabelText("Phone Number")).toBeInTheDocument(); - expect(screen.getByDisplayValue("+1234567890")).toBeInTheDocument(); + expect(screen.getByText(/A verification code will be sent to \+1234567890/)).toBeInTheDocument(); expect(screen.getByRole("button", { name: "Send Code" })).toBeInTheDocument(); }); @@ -121,16 +151,16 @@ describe("", () => { imports: [SmsMultiFactorAssertionFormComponent], }); - expect(screen.getByLabelText("Phone Number")).toBeInTheDocument(); + expect(screen.getByText(/A verification code will be sent to \+1234567890/)).toBeInTheDocument(); fireEvent.click(screen.getByRole("button", { name: "Send Code" })); await waitFor(() => { - expect(screen.getByLabelText("Verification Code")).toBeInTheDocument(); + expect(screen.getByRole("textbox", { name: /Verification Code/i })).toBeInTheDocument(); }); expect(screen.getByRole("button", { name: "Verify Code" })).toBeInTheDocument(); - expect(screen.queryByLabelText("Phone Number")).not.toBeInTheDocument(); + expect(screen.queryByText(/A verification code will be sent to/)).not.toBeInTheDocument(); }); it("emits onSuccess when verification is successful", async () => { @@ -155,12 +185,11 @@ describe("", () => { )?.componentInstance; if (phoneFormComponent) { - phoneFormComponent.form.setFieldValue("phoneNumber", "+1234567890"); await phoneFormComponent.form.handleSubmit(); } await waitFor(() => { - expect(screen.getByLabelText("Verification Code")).toBeInTheDocument(); + expect(screen.getByRole("textbox", { name: /Verification Code/i })).toBeInTheDocument(); }); const verifyFormComponent = fixture.debugElement.query( @@ -189,12 +218,35 @@ describe("", () => { injectMultiFactorPhoneAuthAssertionFormSchema, } = require("../../../tests/test-helpers"); + const { getTranslation } = require("@invertase/firebaseui-core"); + getTranslation.mockImplementation((ui: any, category: string, key: string, params?: any) => { + if (category === "messages" && key === "mfaSmsAssertionPrompt" && params) { + return `A verification code will be sent to ${params.phoneNumber} to complete the authentication process.`; + } + const mockTranslations: Record> = { + labels: { + phoneNumber: "Phone Number", + sendCode: "Send Code", + }, + messages: { + mfaSmsAssertionPrompt: "A verification code will be sent to {phoneNumber} to complete the authentication process.", + }, + errors: { + unknownError: "An unknown error occurred", + }, + }; + return mockTranslations[category]?.[key] || `${category}.${key}`; + }); + injectTranslation.mockImplementation((category: string, key: string) => { const mockTranslations: Record> = { labels: { phoneNumber: "Phone Number", sendCode: "Send Code", }, + messages: { + mfaSmsAssertionPrompt: "A verification code will be sent to {phoneNumber} to complete the authentication process.", + }, errors: { unknownError: "An unknown error occurred", }, @@ -218,7 +270,7 @@ describe("", () => { verifyPhoneNumber.mockResolvedValue("test-verification-id"); }); - it("renders phone form with phone number from hint", async () => { + it("renders phone form with message showing phone number from hint", async () => { const mockHint = { factorId: PhoneMultiFactorGenerator.FACTOR_ID, displayName: "Phone", @@ -232,9 +284,8 @@ describe("", () => { imports: [SmsMultiFactorAssertionPhoneFormComponent], }); - const phoneInput = screen.getByLabelText("Phone Number"); - expect(phoneInput).toBeInTheDocument(); - expect(phoneInput).toHaveValue("+1234567890"); + expect(screen.getByText(/A verification code will be sent to \+1234567890/)).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Send Code" })).toBeInTheDocument(); }); it("emits onSubmit when form is submitted", async () => { @@ -276,6 +327,9 @@ describe("", () => { verificationCode: "Verification Code", verifyCode: "Verify Code", }, + prompts: { + smsVerificationPrompt: "Enter the verification code sent to your phone number", + }, errors: { unknownError: "An unknown error occurred", }, @@ -311,7 +365,9 @@ describe("", () => { imports: [SmsMultiFactorAssertionVerifyFormComponent], }); - expect(screen.getByLabelText("Verification Code")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByRole("textbox", { name: /Verification Code/i })).toBeInTheDocument(); + }); expect(screen.getByRole("button", { name: "Verify Code" })).toBeInTheDocument(); }); diff --git a/packages/angular/src/lib/auth/forms/mfa/sms-multi-factor-assertion-form.ts b/packages/angular/src/lib/auth/forms/mfa/sms-multi-factor-assertion-form.ts index cb90db425..b09a263d0 100644 --- a/packages/angular/src/lib/auth/forms/mfa/sms-multi-factor-assertion-form.ts +++ b/packages/angular/src/lib/auth/forms/mfa/sms-multi-factor-assertion-form.ts @@ -17,14 +17,13 @@ import { Component, ElementRef, effect, input, signal, output, computed, viewChi import { CommonModule } from "@angular/common"; import { injectForm, injectStore, TanStackAppField, TanStackField } from "@tanstack/angular-form"; import { - injectMultiFactorPhoneAuthAssertionFormSchema, injectMultiFactorPhoneAuthVerifyFormSchema, injectRecaptchaVerifier, injectTranslation, injectUI, } from "../../../provider"; import { FormInputComponent, FormSubmitComponent, FormErrorMessageComponent } from "../../../components/form"; -import { FirebaseUIError, verifyPhoneNumber, signInWithMultiFactorAssertion } from "@invertase/firebaseui-core"; +import { FirebaseUIError, verifyPhoneNumber, signInWithMultiFactorAssertion, getTranslation } from "@invertase/firebaseui-core"; import { PhoneAuthProvider, PhoneMultiFactorGenerator, type MultiFactorInfo, type UserCredential } from "firebase/auth"; type PhoneMultiFactorInfo = MultiFactorInfo & { @@ -36,22 +35,17 @@ type PhoneMultiFactorInfo = MultiFactorInfo & { standalone: true, imports: [ CommonModule, - TanStackField, - TanStackAppField, - FormInputComponent, FormSubmitComponent, FormErrorMessageComponent, ], template: `
- +
@@ -67,14 +61,11 @@ type PhoneMultiFactorInfo = MultiFactorInfo & { }) export class SmsMultiFactorAssertionPhoneFormComponent { private ui = injectUI(); - private formSchema = injectMultiFactorPhoneAuthAssertionFormSchema(); hint = input.required(); onSubmit = output(); - phoneNumberLabel = injectTranslation("labels", "phoneNumber"); sendCodeLabel = injectTranslation("labels", "sendCode"); - unknownErrorLabel = injectTranslation("errors", "unknownError"); recaptchaContainer = viewChild.required>("recaptchaContainer"); @@ -83,42 +74,39 @@ export class SmsMultiFactorAssertionPhoneFormComponent { return hint.phoneNumber || ""; }); + mfaSmsAssertionPrompt = computed(() => { + return getTranslation( + this.ui(), + "messages", + "mfaSmsAssertionPrompt", + { phoneNumber: this.phoneNumber() } + ); + }); + recaptchaVerifier = injectRecaptchaVerifier(() => this.recaptchaContainer()); form = injectForm({ - defaultValues: { - phoneNumber: "", - }, + defaultValues: {}, }); state = injectStore(this.form, (state) => state); constructor() { - effect(() => { - // Set the phone number value from the hint - this.form.setFieldValue("phoneNumber", this.phoneNumber()); - }); - effect(() => { this.form.update({ validators: { - onBlur: this.formSchema(), - onSubmit: this.formSchema(), onSubmitAsync: async () => { try { const verifier = this.recaptchaVerifier(); if (!verifier) { - return this.unknownErrorLabel(); + return "Recaptcha verifier not available"; } const verificationId = await verifyPhoneNumber(this.ui(), "", verifier, undefined, this.hint()); this.onSubmit.emit(verificationId); return; } catch (error) { - if (error instanceof FirebaseUIError) { - return error.message; - } - return this.unknownErrorLabel(); + return error instanceof FirebaseUIError ? error.message : String(error); } }, }, @@ -160,7 +148,8 @@ export class SmsMultiFactorAssertionPhoneFormComponent { name="verificationCode" tanstack-app-field [tanstackField]="form" - label="{{ verificationCodeLabel() }}" + [label]="verificationCodeLabel()" + [description]="smsVerificationPrompt()" type="text" >
@@ -182,6 +171,7 @@ export class SmsMultiFactorAssertionVerifyFormComponent { verificationCodeLabel = injectTranslation("labels", "verificationCode"); verifyCodeLabel = injectTranslation("labels", "verifyCode"); + smsVerificationPrompt = injectTranslation("prompts", "smsVerificationPrompt"); unknownErrorLabel = injectTranslation("errors", "unknownError"); form = injectForm({ @@ -211,10 +201,7 @@ export class SmsMultiFactorAssertionVerifyFormComponent { this.onSuccess.emit(result); return; } catch (error) { - if (error instanceof FirebaseUIError) { - return error.message; - } - return this.unknownErrorLabel(); + return error instanceof FirebaseUIError ? error.message : String(error); } }, }, From 70e9f4c04974f55232e98e01035706af5da416a8 Mon Sep 17 00:00:00 2001 From: Elliot Hesp Date: Sat, 8 Nov 2025 15:17:25 +0000 Subject: [PATCH 17/32] fix(angular): Align SMS MFA Enrollment form --- .../sms-multi-factor-enrollment-form.spec.ts | 56 +++++++------------ .../mfa/sms-multi-factor-enrollment-form.ts | 42 ++++++-------- 2 files changed, 36 insertions(+), 62 deletions(-) diff --git a/packages/angular/src/lib/auth/forms/mfa/sms-multi-factor-enrollment-form.spec.ts b/packages/angular/src/lib/auth/forms/mfa/sms-multi-factor-enrollment-form.spec.ts index e192cfba9..c710bf450 100644 --- a/packages/angular/src/lib/auth/forms/mfa/sms-multi-factor-enrollment-form.spec.ts +++ b/packages/angular/src/lib/auth/forms/mfa/sms-multi-factor-enrollment-form.spec.ts @@ -20,7 +20,6 @@ import { TanStackField, TanStackAppField } from "@tanstack/angular-form"; import { SmsMultiFactorEnrollmentFormComponent } from "./sms-multi-factor-enrollment-form"; import { FormInputComponent, FormSubmitComponent, FormErrorMessageComponent } from "../../../components/form"; import { CountrySelectorComponent } from "../../../components/country-selector"; -import { PoliciesComponent } from "../../../components/policies"; jest.mock("@invertase/firebaseui-core", () => { const originalModule = jest.requireActual("@invertase/firebaseui-core"); @@ -94,6 +93,9 @@ describe("", () => { verificationCode: "Verification Code", verifyCode: "Verify Code", }, + prompts: { + smsVerificationPrompt: "Enter the verification code sent to your phone number", + }, errors: { unknownError: "An unknown error occurred", }, @@ -145,7 +147,6 @@ describe("", () => { FormSubmitComponent, FormErrorMessageComponent, CountrySelectorComponent, - PoliciesComponent, ], }); expect(fixture.componentInstance).toBeTruthy(); @@ -162,7 +163,6 @@ describe("", () => { FormSubmitComponent, FormErrorMessageComponent, CountrySelectorComponent, - PoliciesComponent, ], }); @@ -185,7 +185,6 @@ describe("", () => { FormSubmitComponent, FormErrorMessageComponent, CountrySelectorComponent, - PoliciesComponent, ], }); @@ -193,7 +192,9 @@ describe("", () => { component.verificationId.set(mockVerificationId); fixture.detectChanges(); - expect(screen.getByLabelText("Verification Code")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByRole("textbox", { name: /Verification Code/i })).toBeInTheDocument(); + }); expect(screen.getByRole("button", { name: "Verify Code" })).toBeInTheDocument(); }); @@ -211,7 +212,6 @@ describe("", () => { FormSubmitComponent, FormErrorMessageComponent, CountrySelectorComponent, - PoliciesComponent, ], }); @@ -243,7 +243,6 @@ describe("", () => { FormSubmitComponent, FormErrorMessageComponent, CountrySelectorComponent, - PoliciesComponent, ], }); @@ -277,7 +276,6 @@ describe("", () => { FormSubmitComponent, FormErrorMessageComponent, CountrySelectorComponent, - PoliciesComponent, ], }); @@ -309,7 +307,6 @@ describe("", () => { FormSubmitComponent, FormErrorMessageComponent, CountrySelectorComponent, - PoliciesComponent, ], }); @@ -342,7 +339,6 @@ describe("", () => { FormSubmitComponent, FormErrorMessageComponent, CountrySelectorComponent, - PoliciesComponent, ], }); @@ -375,31 +371,20 @@ describe("", () => { }); }); - const { fixture } = await render(SmsMultiFactorEnrollmentFormComponent, { - imports: [ - CommonModule, - SmsMultiFactorEnrollmentFormComponent, - TanStackField, - TanStackAppField, - FormInputComponent, - FormSubmitComponent, - FormErrorMessageComponent, - CountrySelectorComponent, - PoliciesComponent, - ], - }); - - const component = fixture.componentInstance; - - component.phoneForm.setFieldValue("displayName", "Test User"); - component.phoneForm.setFieldValue("phoneNumber", "1234567890"); - fixture.detectChanges(); - - await component.phoneForm.handleSubmit(); - await fixture.whenStable(); - fixture.detectChanges(); - - expect(screen.getByText("An unknown error occurred")).toBeInTheDocument(); + await expect( + render(SmsMultiFactorEnrollmentFormComponent, { + imports: [ + CommonModule, + SmsMultiFactorEnrollmentFormComponent, + TanStackField, + TanStackAppField, + FormInputComponent, + FormSubmitComponent, + FormErrorMessageComponent, + CountrySelectorComponent, + ], + }) + ).rejects.toThrow("User must be authenticated to enroll with multi-factor authentication"); }); it("should have correct CSS classes", async () => { @@ -413,7 +398,6 @@ describe("", () => { FormSubmitComponent, FormErrorMessageComponent, CountrySelectorComponent, - PoliciesComponent, ], }); diff --git a/packages/angular/src/lib/auth/forms/mfa/sms-multi-factor-enrollment-form.ts b/packages/angular/src/lib/auth/forms/mfa/sms-multi-factor-enrollment-form.ts index e0d2de369..784b61558 100644 --- a/packages/angular/src/lib/auth/forms/mfa/sms-multi-factor-enrollment-form.ts +++ b/packages/angular/src/lib/auth/forms/mfa/sms-multi-factor-enrollment-form.ts @@ -50,7 +50,6 @@ import { FormSubmitComponent, FormErrorMessageComponent, CountrySelectorComponent, - PoliciesComponent, ], template: `
@@ -61,7 +60,8 @@ import { name="displayName" tanstack-app-field [tanstackField]="phoneForm" - label="{{ displayNameLabel() }}" + [label]="displayNameLabel()" + type="text" >
@@ -70,13 +70,13 @@ import { name="phoneNumber" tanstack-app-field [tanstackField]="phoneForm" - label="{{ phoneNumberLabel() }}" + [label]="phoneNumberLabel()" + type="tel" >
-
{{ sendCodeLabel() }} @@ -91,10 +91,11 @@ import { name="verificationCode" tanstack-app-field [tanstackField]="verificationForm" - label="{{ verificationCodeLabel() }}" + [label]="verificationCodeLabel()" + [description]="smsVerificationPrompt()" + type="text" >
-
{{ verifyCodeLabel() }} @@ -121,7 +122,7 @@ export class SmsMultiFactorEnrollmentFormComponent { sendCodeLabel = injectTranslation("labels", "sendCode"); verificationCodeLabel = injectTranslation("labels", "verificationCode"); verifyCodeLabel = injectTranslation("labels", "verifyCode"); - unknownErrorLabel = injectTranslation("errors", "unknownError"); + smsVerificationPrompt = injectTranslation("prompts", "smsVerificationPrompt"); onEnrollment = output(); @@ -146,6 +147,10 @@ export class SmsMultiFactorEnrollmentFormComponent { verificationState = injectStore(this.verificationForm, (state) => state); constructor() { + if (!this.ui().auth.currentUser) { + throw new Error("User must be authenticated to enroll with multi-factor authentication"); + } + effect(() => { this.phoneForm.update({ validators: { @@ -153,16 +158,12 @@ export class SmsMultiFactorEnrollmentFormComponent { onSubmit: this.phoneFormSchema(), onSubmitAsync: async ({ value }) => { try { - const currentUser = this.ui().auth.currentUser; - if (!currentUser) { - throw new Error("User must be authenticated to enroll with multi-factor authentication"); - } - const verifier = this.recaptchaVerifier(); if (!verifier) { - return this.unknownErrorLabel(); + return "Recaptcha verifier not available"; } + const currentUser = this.ui().auth.currentUser!; const mfaUser = multiFactor(currentUser); const formattedPhoneNumber = formatPhoneNumber(value.phoneNumber, this.defaultCountry()); const verificationId = await verifyPhoneNumber(this.ui(), formattedPhoneNumber, verifier, mfaUser); @@ -171,12 +172,7 @@ export class SmsMultiFactorEnrollmentFormComponent { this.verificationId.set(verificationId); return; } catch (error) { - if (error instanceof FirebaseUIError) { - return error.message; - } - - console.error(error); - return this.unknownErrorLabel(); + return error instanceof FirebaseUIError ? error.message : String(error); } }, }, @@ -196,13 +192,7 @@ export class SmsMultiFactorEnrollmentFormComponent { this.onEnrollment.emit(); return; } catch (error) { - if (error instanceof FirebaseUIError) { - return error.message; - } - if (error instanceof Error) { - return error.message; - } - return this.unknownErrorLabel(); + return error instanceof FirebaseUIError ? error.message : String(error); } }, }, From 46359509f398db3bbe3a8bc663a46a8f8cb2dfad Mon Sep 17 00:00:00 2001 From: Elliot Hesp Date: Sat, 8 Nov 2025 15:21:22 +0000 Subject: [PATCH 18/32] fix(angular): Align TOTP MFA assertion form --- .../mfa/totp-multi-factor-assertion-form.spec.ts | 12 +++++++++--- .../forms/mfa/totp-multi-factor-assertion-form.ts | 10 ++++------ .../forms/mfa/totp-multi-factor-assertion-form.tsx | 1 + 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/packages/angular/src/lib/auth/forms/mfa/totp-multi-factor-assertion-form.spec.ts b/packages/angular/src/lib/auth/forms/mfa/totp-multi-factor-assertion-form.spec.ts index 71750a236..a1df6dc81 100644 --- a/packages/angular/src/lib/auth/forms/mfa/totp-multi-factor-assertion-form.spec.ts +++ b/packages/angular/src/lib/auth/forms/mfa/totp-multi-factor-assertion-form.spec.ts @@ -50,6 +50,9 @@ describe("", () => { verificationCode: "Verification Code", verifyCode: "Verify Code", }, + prompts: { + enterVerificationCode: "Enter the verification code", + }, errors: { unknownError: "An unknown error occurred", }, @@ -94,7 +97,9 @@ describe("", () => { imports: [TotpMultiFactorAssertionFormComponent], }); - expect(screen.getByLabelText("Verification Code")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByRole("textbox", { name: /Verification Code/i })).toBeInTheDocument(); + }); expect(screen.getByPlaceholderText("123456")).toBeInTheDocument(); expect(screen.getByRole("button", { name: "Verify Code" })).toBeInTheDocument(); }); @@ -268,7 +273,8 @@ describe("", () => { uid: "test-uid", }; - signInWithMultiFactorAssertion.mockRejectedValue(new Error("Network error")); + const errorMessage = "Network error"; + signInWithMultiFactorAssertion.mockRejectedValue(new Error(errorMessage)); const { fixture } = await render(TotpMultiFactorAssertionFormComponent, { componentInputs: { @@ -284,7 +290,7 @@ describe("", () => { await component.form.handleSubmit(); await waitFor(() => { - expect(screen.getByText("An unknown error occurred")).toBeInTheDocument(); + expect(screen.getByText(new RegExp(errorMessage))).toBeInTheDocument(); }); }); }); diff --git a/packages/angular/src/lib/auth/forms/mfa/totp-multi-factor-assertion-form.ts b/packages/angular/src/lib/auth/forms/mfa/totp-multi-factor-assertion-form.ts index 4045a849b..fea336044 100644 --- a/packages/angular/src/lib/auth/forms/mfa/totp-multi-factor-assertion-form.ts +++ b/packages/angular/src/lib/auth/forms/mfa/totp-multi-factor-assertion-form.ts @@ -39,7 +39,8 @@ import { TotpMultiFactorGenerator, type MultiFactorInfo, type UserCredential } f name="verificationCode" tanstack-app-field [tanstackField]="form" - label="{{ verificationCodeLabel() }}" + [label]="verificationCodeLabel()" + [description]="enterVerificationCodePrompt()" type="text" placeholder="123456" maxlength="6" @@ -63,7 +64,7 @@ export class TotpMultiFactorAssertionFormComponent { verificationCodeLabel = injectTranslation("labels", "verificationCode"); verifyCodeLabel = injectTranslation("labels", "verifyCode"); - unknownErrorLabel = injectTranslation("errors", "unknownError"); + enterVerificationCodePrompt = injectTranslation("prompts", "enterVerificationCode"); form = injectForm({ defaultValues: { @@ -85,10 +86,7 @@ export class TotpMultiFactorAssertionFormComponent { this.onSuccess.emit(result); return; } catch (error) { - if (error instanceof FirebaseUIError) { - return error.message; - } - return this.unknownErrorLabel(); + return error instanceof FirebaseUIError ? error.message : String(error); } }, }, diff --git a/packages/react/src/auth/forms/mfa/totp-multi-factor-assertion-form.tsx b/packages/react/src/auth/forms/mfa/totp-multi-factor-assertion-form.tsx index cbc775f24..0a69fb230 100644 --- a/packages/react/src/auth/forms/mfa/totp-multi-factor-assertion-form.tsx +++ b/packages/react/src/auth/forms/mfa/totp-multi-factor-assertion-form.tsx @@ -71,6 +71,7 @@ export function TotpMultiFactorAssertionForm(props: TotpMultiFactorAssertionForm {(field) => ( Date: Sat, 8 Nov 2025 15:23:43 +0000 Subject: [PATCH 19/32] fix(angular): Align TOTP MFA enrollment form --- .../totp-multi-factor-enrollment-form.spec.ts | 48 +++++++------------ .../mfa/totp-multi-factor-enrollment-form.ts | 42 +++++++--------- 2 files changed, 35 insertions(+), 55 deletions(-) diff --git a/packages/angular/src/lib/auth/forms/mfa/totp-multi-factor-enrollment-form.spec.ts b/packages/angular/src/lib/auth/forms/mfa/totp-multi-factor-enrollment-form.spec.ts index 6a107e9f8..4145d4add 100644 --- a/packages/angular/src/lib/auth/forms/mfa/totp-multi-factor-enrollment-form.spec.ts +++ b/packages/angular/src/lib/auth/forms/mfa/totp-multi-factor-enrollment-form.spec.ts @@ -14,12 +14,11 @@ * limitations under the License. */ -import { render, screen } from "@testing-library/angular"; +import { render, screen, waitFor } from "@testing-library/angular"; import { CommonModule } from "@angular/common"; import { TanStackField, TanStackAppField } from "@tanstack/angular-form"; import { TotpMultiFactorEnrollmentFormComponent } from "./totp-multi-factor-enrollment-form"; import { FormInputComponent, FormSubmitComponent, FormErrorMessageComponent } from "../../../components/form"; -import { PoliciesComponent } from "../../../components/policies"; describe("", () => { let mockGenerateTotpSecret: any; @@ -57,6 +56,7 @@ describe("", () => { }, prompts: { mfaTotpQrCodePrompt: "Scan this QR code with your authenticator app", + mfaTotpEnrollmentVerificationPrompt: "Add the code generated by your authenticator app", }, errors: { unknownError: "An unknown error occurred", @@ -96,7 +96,6 @@ describe("", () => { FormInputComponent, FormSubmitComponent, FormErrorMessageComponent, - PoliciesComponent, ], }); expect(fixture.componentInstance).toBeTruthy(); @@ -112,7 +111,6 @@ describe("", () => { FormInputComponent, FormSubmitComponent, FormErrorMessageComponent, - PoliciesComponent, ], }); @@ -142,7 +140,6 @@ describe("", () => { FormInputComponent, FormSubmitComponent, FormErrorMessageComponent, - PoliciesComponent, ], }); @@ -152,7 +149,9 @@ describe("", () => { expect(screen.getByAltText("TOTP QR Code")).toBeInTheDocument(); expect(screen.getByText("Scan this QR code with your authenticator app")).toBeInTheDocument(); - expect(screen.getByLabelText("Verification Code")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByRole("textbox", { name: /Verification Code/i })).toBeInTheDocument(); + }); expect(screen.getByRole("button", { name: "Verify Code" })).toBeInTheDocument(); }); @@ -177,7 +176,6 @@ describe("", () => { FormInputComponent, FormSubmitComponent, FormErrorMessageComponent, - PoliciesComponent, ], }); @@ -211,7 +209,6 @@ describe("", () => { FormInputComponent, FormSubmitComponent, FormErrorMessageComponent, - PoliciesComponent, ], }); @@ -240,7 +237,6 @@ describe("", () => { FormInputComponent, FormSubmitComponent, FormErrorMessageComponent, - PoliciesComponent, ], }); @@ -264,7 +260,6 @@ describe("", () => { FormInputComponent, FormSubmitComponent, FormErrorMessageComponent, - PoliciesComponent, ], }); @@ -296,24 +291,19 @@ describe("", () => { }); }); - const { fixture } = await render(TotpMultiFactorEnrollmentFormComponent, { - imports: [ - CommonModule, - TotpMultiFactorEnrollmentFormComponent, - TanStackField, - TanStackAppField, - FormInputComponent, - FormSubmitComponent, - FormErrorMessageComponent, - PoliciesComponent, - ], - }); - - const component = fixture.componentInstance; - - // Since the parent component doesn't have direct access to the child form, - // we test that the enrollment state remains null when user is not authenticated - expect(component.enrollment()).toBeNull(); + await expect( + render(TotpMultiFactorEnrollmentFormComponent, { + imports: [ + CommonModule, + TotpMultiFactorEnrollmentFormComponent, + TanStackField, + TanStackAppField, + FormInputComponent, + FormSubmitComponent, + FormErrorMessageComponent, + ], + }) + ).rejects.toThrow("User must be authenticated to enroll with multi-factor authentication"); }); it("should generate QR code with correct parameters", async () => { @@ -339,7 +329,6 @@ describe("", () => { FormInputComponent, FormSubmitComponent, FormErrorMessageComponent, - PoliciesComponent, ], }); @@ -362,7 +351,6 @@ describe("", () => { FormInputComponent, FormSubmitComponent, FormErrorMessageComponent, - PoliciesComponent, ], }); diff --git a/packages/angular/src/lib/auth/forms/mfa/totp-multi-factor-enrollment-form.ts b/packages/angular/src/lib/auth/forms/mfa/totp-multi-factor-enrollment-form.ts index 00100d50d..79f51a838 100644 --- a/packages/angular/src/lib/auth/forms/mfa/totp-multi-factor-enrollment-form.ts +++ b/packages/angular/src/lib/auth/forms/mfa/totp-multi-factor-enrollment-form.ts @@ -43,7 +43,6 @@ import { FormInputComponent, FormSubmitComponent, FormErrorMessageComponent, - PoliciesComponent, ], template: ` @@ -52,10 +51,10 @@ import { name="displayName" tanstack-app-field [tanstackField]="form" - label="{{ displayNameLabel() }}" + [label]="displayNameLabel()" + type="text" >
-
{{ generateQrCodeLabel() }} @@ -73,7 +72,6 @@ export class TotpMultiFactorSecretGenerationFormComponent { displayNameLabel = injectTranslation("labels", "displayName"); generateQrCodeLabel = injectTranslation("labels", "generateQrCode"); - unknownErrorLabel = injectTranslation("errors", "unknownError"); form = injectForm({ defaultValues: { @@ -91,20 +89,11 @@ export class TotpMultiFactorSecretGenerationFormComponent { onSubmit: this.formSchema(), onSubmitAsync: async ({ value }) => { try { - if (!this.ui().auth.currentUser) { - throw new Error("User must be authenticated to enroll with multi-factor authentication"); - } - const secret = await generateTotpSecret(this.ui()); this.onSubmit.emit({ secret, displayName: value.displayName }); return; } catch (error) { - if (error instanceof FirebaseUIError) { - return error.message; - } - - console.error(error); - return this.unknownErrorLabel(); + return error instanceof FirebaseUIError ? error.message : String(error); } }, }, @@ -129,11 +118,11 @@ export class TotpMultiFactorSecretGenerationFormComponent { FormInputComponent, FormSubmitComponent, FormErrorMessageComponent, - PoliciesComponent, ], template: `
TOTP QR Code + {{ secret().secretKey.toString() }}

{{ mfaTotpQrCodePrompt() }}

@@ -142,10 +131,11 @@ export class TotpMultiFactorSecretGenerationFormComponent { name="verificationCode" tanstack-app-field [tanstackField]="form" - label="{{ verificationCodeLabel() }}" + [label]="verificationCodeLabel()" + [description]="mfaTotpEnrollmentVerificationPrompt()" + type="text" >
-
{{ verifyCodeLabel() }} @@ -166,7 +156,7 @@ export class TotpMultiFactorVerificationFormComponent { verificationCodeLabel = injectTranslation("labels", "verificationCode"); verifyCodeLabel = injectTranslation("labels", "verifyCode"); mfaTotpQrCodePrompt = injectTranslation("prompts", "mfaTotpQrCodePrompt"); - unknownErrorLabel = injectTranslation("errors", "unknownError"); + mfaTotpEnrollmentVerificationPrompt = injectTranslation("prompts", "mfaTotpEnrollmentVerificationPrompt"); form = injectForm({ defaultValues: { @@ -193,13 +183,7 @@ export class TotpMultiFactorVerificationFormComponent { this.onEnrollment.emit(); return; } catch (error) { - if (error instanceof FirebaseUIError) { - return error.message; - } - if (error instanceof Error) { - return error.message; - } - return this.unknownErrorLabel(); + return error instanceof FirebaseUIError ? error.message : String(error); } }, }, @@ -233,9 +217,17 @@ export class TotpMultiFactorVerificationFormComponent { `, }) export class TotpMultiFactorEnrollmentFormComponent { + private ui = injectUI(); + enrollment = signal<{ secret: TotpSecret; displayName: string } | null>(null); onEnrollment = output(); + constructor() { + if (!this.ui().auth.currentUser) { + throw new Error("User must be authenticated to enroll with multi-factor authentication"); + } + } + handleSecretGeneration(data: { secret: TotpSecret; displayName: string }) { this.enrollment.set(data); } From a7c2bcf022004ed7bfb0771a5939df237d51dc66 Mon Sep 17 00:00:00 2001 From: Elliot Hesp Date: Sat, 8 Nov 2025 15:24:08 +0000 Subject: [PATCH 20/32] chore: linting --- .../src/lib/auth/forms/mfa/totp-multi-factor-enrollment-form.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/angular/src/lib/auth/forms/mfa/totp-multi-factor-enrollment-form.ts b/packages/angular/src/lib/auth/forms/mfa/totp-multi-factor-enrollment-form.ts index 79f51a838..e16dd022a 100644 --- a/packages/angular/src/lib/auth/forms/mfa/totp-multi-factor-enrollment-form.ts +++ b/packages/angular/src/lib/auth/forms/mfa/totp-multi-factor-enrollment-form.ts @@ -25,7 +25,6 @@ import { FirebaseUIError, } from "@invertase/firebaseui-core"; import { FormInputComponent, FormSubmitComponent, FormErrorMessageComponent } from "../../../components/form"; -import { PoliciesComponent } from "../../../components/policies"; import { injectUI, injectTranslation, From 721444ed947666e0658c82a50dbffb5cad448b87 Mon Sep 17 00:00:00 2001 From: Elliot Hesp Date: Sat, 8 Nov 2025 15:26:40 +0000 Subject: [PATCH 21/32] fix(angular): Align oauth button --- .../src/lib/auth/oauth/oauth-button.spec.ts | 10 ++++++++- .../src/lib/auth/oauth/oauth-button.ts | 21 ++++++++++++------- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/packages/angular/src/lib/auth/oauth/oauth-button.spec.ts b/packages/angular/src/lib/auth/oauth/oauth-button.spec.ts index bd749f6d5..32856f17e 100644 --- a/packages/angular/src/lib/auth/oauth/oauth-button.spec.ts +++ b/packages/angular/src/lib/auth/oauth/oauth-button.spec.ts @@ -40,13 +40,21 @@ class TestOAuthButtonWithCustomProviderHostComponent { describe("", () => { let mockSignInWithProvider: any; let mockFirebaseUIError: any; + let mockGetTranslation: any; beforeEach(() => { - const { signInWithProvider, FirebaseUIError } = require("@invertase/firebaseui-core"); + const { signInWithProvider, FirebaseUIError, getTranslation } = require("@invertase/firebaseui-core"); mockSignInWithProvider = signInWithProvider; mockFirebaseUIError = FirebaseUIError; + mockGetTranslation = getTranslation; mockSignInWithProvider.mockClear(); + mockGetTranslation.mockImplementation((ui: any, category: string, key: string) => { + if (category === "errors" && key === "unknownError") { + return "An unknown error occurred"; + } + return `${category}.${key}`; + }); }); it("should create", async () => { diff --git a/packages/angular/src/lib/auth/oauth/oauth-button.ts b/packages/angular/src/lib/auth/oauth/oauth-button.ts index 185b07cf2..a4cc149cc 100644 --- a/packages/angular/src/lib/auth/oauth/oauth-button.ts +++ b/packages/angular/src/lib/auth/oauth/oauth-button.ts @@ -14,12 +14,12 @@ * limitations under the License. */ -import { Component, input, signal } from "@angular/core"; +import { Component, input, signal, computed } from "@angular/core"; import { CommonModule } from "@angular/common"; import { ButtonComponent } from "../../components/button"; -import { injectTranslation, injectUI } from "../../provider"; +import { injectUI } from "../../provider"; import { AuthProvider } from "@angular/fire/auth"; -import { FirebaseUIError, signInWithProvider } from "@invertase/firebaseui-core"; +import { FirebaseUIError, signInWithProvider, getTranslation } from "@invertase/firebaseui-core"; @Component({ selector: "fui-oauth-button", @@ -31,6 +31,8 @@ import { FirebaseUIError, signInWithProvider } from "@invertase/firebaseui-core" fui-button type="button" (click)="handleOAuthSignIn()" + [variant]="buttonVariant()" + [attr.data-themed]="themed()" [disabled]="ui().state !== 'idle'" [attr.data-provider]="provider().providerId" class="fui-provider__button" @@ -46,12 +48,16 @@ import { FirebaseUIError, signInWithProvider } from "@invertase/firebaseui-core" }) export class OAuthButtonComponent { ui = injectUI(); - unknownErrorLabel = injectTranslation("errors", "unknownError"); provider = input.required(); - error = signal(undefined); + themed = input(); + error = signal(null); + + buttonVariant = computed(() => { + return this.themed() ? "primary" : "secondary"; + }); async handleOAuthSignIn() { - this.error.set(undefined); + this.error.set(null); try { await signInWithProvider(this.ui(), this.provider()); } catch (error) { @@ -59,9 +65,8 @@ export class OAuthButtonComponent { this.error.set(error.message); return; } - console.error(error); - this.error.set(this.unknownErrorLabel()); + this.error.set(getTranslation(this.ui(), "errors", "unknownError")); } } } From 2a34562c883922916e4b52e8a48c0116d6a71eff Mon Sep 17 00:00:00 2001 From: Elliot Hesp Date: Sat, 8 Nov 2025 15:31:54 +0000 Subject: [PATCH 22/32] chore: Run formatting --- .../sms-multi-factor-assertion-form.spec.ts | 12 +++++++---- .../mfa/sms-multi-factor-assertion-form.ts | 20 ++++++++----------- .../multi-factor-auth-enrollment-form.spec.ts | 11 ++++++++++ .../lib/auth/forms/phone-auth-form.spec.ts | 2 +- .../src/lib/auth/forms/sign-up-auth-form.ts | 7 +------ .../screens/email-link-auth-screen.spec.ts | 3 +++ ...multi-factor-auth-assertion-screen.spec.ts | 2 +- .../auth/screens/phone-auth-screen.spec.ts | 6 +++++- .../auth/screens/sign-in-auth-screen.spec.ts | 6 +++++- .../auth/screens/sign-up-auth-screen.spec.ts | 6 +++++- 10 files changed, 48 insertions(+), 27 deletions(-) diff --git a/packages/angular/src/lib/auth/forms/mfa/sms-multi-factor-assertion-form.spec.ts b/packages/angular/src/lib/auth/forms/mfa/sms-multi-factor-assertion-form.spec.ts index e78cfc360..4221d1f73 100644 --- a/packages/angular/src/lib/auth/forms/mfa/sms-multi-factor-assertion-form.spec.ts +++ b/packages/angular/src/lib/auth/forms/mfa/sms-multi-factor-assertion-form.spec.ts @@ -50,7 +50,8 @@ describe("", () => { verifyCode: "Verify Code", }, messages: { - mfaSmsAssertionPrompt: "A verification code will be sent to {phoneNumber} to complete the authentication process.", + mfaSmsAssertionPrompt: + "A verification code will be sent to {phoneNumber} to complete the authentication process.", }, prompts: { smsVerificationPrompt: "Enter the verification code sent to your phone number", @@ -71,7 +72,8 @@ describe("", () => { verifyCode: "Verify Code", }, messages: { - mfaSmsAssertionPrompt: "A verification code will be sent to {phoneNumber} to complete the authentication process.", + mfaSmsAssertionPrompt: + "A verification code will be sent to {phoneNumber} to complete the authentication process.", }, prompts: { smsVerificationPrompt: "Enter the verification code sent to your phone number", @@ -229,7 +231,8 @@ describe("", () => { sendCode: "Send Code", }, messages: { - mfaSmsAssertionPrompt: "A verification code will be sent to {phoneNumber} to complete the authentication process.", + mfaSmsAssertionPrompt: + "A verification code will be sent to {phoneNumber} to complete the authentication process.", }, errors: { unknownError: "An unknown error occurred", @@ -245,7 +248,8 @@ describe("", () => { sendCode: "Send Code", }, messages: { - mfaSmsAssertionPrompt: "A verification code will be sent to {phoneNumber} to complete the authentication process.", + mfaSmsAssertionPrompt: + "A verification code will be sent to {phoneNumber} to complete the authentication process.", }, errors: { unknownError: "An unknown error occurred", diff --git a/packages/angular/src/lib/auth/forms/mfa/sms-multi-factor-assertion-form.ts b/packages/angular/src/lib/auth/forms/mfa/sms-multi-factor-assertion-form.ts index b09a263d0..0dba04799 100644 --- a/packages/angular/src/lib/auth/forms/mfa/sms-multi-factor-assertion-form.ts +++ b/packages/angular/src/lib/auth/forms/mfa/sms-multi-factor-assertion-form.ts @@ -23,7 +23,12 @@ import { injectUI, } from "../../../provider"; import { FormInputComponent, FormSubmitComponent, FormErrorMessageComponent } from "../../../components/form"; -import { FirebaseUIError, verifyPhoneNumber, signInWithMultiFactorAssertion, getTranslation } from "@invertase/firebaseui-core"; +import { + FirebaseUIError, + verifyPhoneNumber, + signInWithMultiFactorAssertion, + getTranslation, +} from "@invertase/firebaseui-core"; import { PhoneAuthProvider, PhoneMultiFactorGenerator, type MultiFactorInfo, type UserCredential } from "firebase/auth"; type PhoneMultiFactorInfo = MultiFactorInfo & { @@ -33,11 +38,7 @@ type PhoneMultiFactorInfo = MultiFactorInfo & { @Component({ selector: "fui-sms-multi-factor-assertion-phone-form", standalone: true, - imports: [ - CommonModule, - FormSubmitComponent, - FormErrorMessageComponent, - ], + imports: [CommonModule, FormSubmitComponent, FormErrorMessageComponent], template: `
@@ -75,12 +76,7 @@ export class SmsMultiFactorAssertionPhoneFormComponent { }); mfaSmsAssertionPrompt = computed(() => { - return getTranslation( - this.ui(), - "messages", - "mfaSmsAssertionPrompt", - { phoneNumber: this.phoneNumber() } - ); + return getTranslation(this.ui(), "messages", "mfaSmsAssertionPrompt", { phoneNumber: this.phoneNumber() }); }); recaptchaVerifier = injectRecaptchaVerifier(() => this.recaptchaContainer()); diff --git a/packages/angular/src/lib/auth/forms/multi-factor-auth-enrollment-form.spec.ts b/packages/angular/src/lib/auth/forms/multi-factor-auth-enrollment-form.spec.ts index 511458b85..3161adc7a 100644 --- a/packages/angular/src/lib/auth/forms/multi-factor-auth-enrollment-form.spec.ts +++ b/packages/angular/src/lib/auth/forms/multi-factor-auth-enrollment-form.spec.ts @@ -23,6 +23,17 @@ import { ButtonComponent } from "../../components/button"; import { FactorId } from "firebase/auth"; describe("", () => { + beforeEach(() => { + const { injectUI } = require("../../../provider"); + injectUI.mockImplementation(() => { + return () => ({ + auth: { + currentUser: { uid: "test-user" }, + }, + }); + }); + }); + it("should create", async () => { const { fixture } = await render(MultiFactorAuthEnrollmentFormComponent, { imports: [ diff --git a/packages/angular/src/lib/auth/forms/phone-auth-form.spec.ts b/packages/angular/src/lib/auth/forms/phone-auth-form.spec.ts index 98e85ab14..1d55fe19f 100644 --- a/packages/angular/src/lib/auth/forms/phone-auth-form.spec.ts +++ b/packages/angular/src/lib/auth/forms/phone-auth-form.spec.ts @@ -127,7 +127,7 @@ describe("", () => { fixture.detectChanges(); await waitFor(() => { - expect(screen.getByLabelText("Verification Code")).toBeInTheDocument(); + expect(screen.getByRole("textbox", { name: /Verification Code/i })).toBeInTheDocument(); }); expect(screen.getByRole("button", { name: "Verify Code" })).toBeInTheDocument(); }); diff --git a/packages/angular/src/lib/auth/forms/sign-up-auth-form.ts b/packages/angular/src/lib/auth/forms/sign-up-auth-form.ts index acbbb34f9..a1f4386cb 100644 --- a/packages/angular/src/lib/auth/forms/sign-up-auth-form.ts +++ b/packages/angular/src/lib/auth/forms/sign-up-auth-form.ts @@ -46,12 +46,7 @@ import { @if (requireDisplayNameField()) {
- +
}
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 4067b69cf..ef9e77d37 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 @@ -90,6 +90,7 @@ describe("", () => { injectUI.mockImplementation(() => () => ({ multiFactorResolver: null, + setMultiFactorResolver: jest.fn(), })); }); @@ -212,6 +213,7 @@ describe("", () => { const { injectUI } = require("../../../provider"); injectUI.mockImplementation(() => () => ({ multiFactorResolver: { auth: {}, session: null, hints: [] }, + setMultiFactorResolver: jest.fn(), })); const { container } = await render(TestHostWithoutContentComponent, { @@ -236,6 +238,7 @@ describe("", () => { const { injectUI } = require("../../../provider"); injectUI.mockImplementation(() => () => ({ multiFactorResolver: { auth: {}, session: null, hints: [] }, + setMultiFactorResolver: jest.fn(), })); const { fixture } = await render(TestHostWithoutContentComponent, { diff --git a/packages/angular/src/lib/auth/screens/multi-factor-auth-assertion-screen.spec.ts b/packages/angular/src/lib/auth/screens/multi-factor-auth-assertion-screen.spec.ts index 83b3bd1df..2efac64f7 100644 --- a/packages/angular/src/lib/auth/screens/multi-factor-auth-assertion-screen.spec.ts +++ b/packages/angular/src/lib/auth/screens/multi-factor-auth-assertion-screen.spec.ts @@ -55,6 +55,7 @@ describe("", () => { session: null, hints: [], }, + setMultiFactorResolver: jest.fn(), })); }); @@ -168,4 +169,3 @@ describe("", () => { ); }); }); - diff --git a/packages/angular/src/lib/auth/screens/phone-auth-screen.spec.ts b/packages/angular/src/lib/auth/screens/phone-auth-screen.spec.ts index 284d045ca..138ea477d 100644 --- a/packages/angular/src/lib/auth/screens/phone-auth-screen.spec.ts +++ b/packages/angular/src/lib/auth/screens/phone-auth-screen.spec.ts @@ -274,7 +274,11 @@ describe("", () => { const { injectUI } = require("../../../provider"); injectUI.mockImplementation(() => { return () => ({ - multiFactorResolver: { auth: {}, session: null, hints: [{ factorId: TotpMultiFactorGenerator.FACTOR_ID, uid: "test" }] }, + multiFactorResolver: { + auth: {}, + session: null, + hints: [{ factorId: TotpMultiFactorGenerator.FACTOR_ID, uid: "test" }], + }, }); }); diff --git a/packages/angular/src/lib/auth/screens/sign-in-auth-screen.spec.ts b/packages/angular/src/lib/auth/screens/sign-in-auth-screen.spec.ts index ed9e18b57..77e1c0d28 100644 --- a/packages/angular/src/lib/auth/screens/sign-in-auth-screen.spec.ts +++ b/packages/angular/src/lib/auth/screens/sign-in-auth-screen.spec.ts @@ -274,7 +274,11 @@ describe("", () => { const { injectUI } = require("../../../provider"); injectUI.mockImplementation(() => { return () => ({ - multiFactorResolver: { auth: {}, session: null, hints: [{ factorId: TotpMultiFactorGenerator.FACTOR_ID, uid: "test" }] }, + multiFactorResolver: { + auth: {}, + session: null, + hints: [{ factorId: TotpMultiFactorGenerator.FACTOR_ID, uid: "test" }], + }, }); }); diff --git a/packages/angular/src/lib/auth/screens/sign-up-auth-screen.spec.ts b/packages/angular/src/lib/auth/screens/sign-up-auth-screen.spec.ts index b6ab33330..02c113fc9 100644 --- a/packages/angular/src/lib/auth/screens/sign-up-auth-screen.spec.ts +++ b/packages/angular/src/lib/auth/screens/sign-up-auth-screen.spec.ts @@ -273,7 +273,11 @@ describe("", () => { const { injectUI } = require("../../../provider"); injectUI.mockImplementation(() => { return () => ({ - multiFactorResolver: { auth: {}, session: null, hints: [{ factorId: TotpMultiFactorGenerator.FACTOR_ID, uid: "test" }] }, + multiFactorResolver: { + auth: {}, + session: null, + hints: [{ factorId: TotpMultiFactorGenerator.FACTOR_ID, uid: "test" }], + }, }); }); From daba3c7ce9a2c54ef7b448bbe89fd5ec2cd8c173 Mon Sep 17 00:00:00 2001 From: Elliot Hesp Date: Sat, 8 Nov 2025 15:33:56 +0000 Subject: [PATCH 23/32] fix(angular): Add missing exports --- packages/angular/src/public-api.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/angular/src/public-api.ts b/packages/angular/src/public-api.ts index d5cd736b9..aaa32705e 100644 --- a/packages/angular/src/public-api.ts +++ b/packages/angular/src/public-api.ts @@ -20,6 +20,7 @@ import { registerFramework } from "@invertase/firebaseui-core"; export { EmailLinkAuthFormComponent } from "./lib/auth/forms/email-link-auth-form"; export { ForgotPasswordAuthFormComponent } from "./lib/auth/forms/forgot-password-auth-form"; export { MultiFactorAuthAssertionFormComponent } from "./lib/auth/forms/multi-factor-auth-assertion-form"; +export { MultiFactorAuthEnrollmentFormComponent } from "./lib/auth/forms/multi-factor-auth-enrollment-form"; export { PhoneAuthFormComponent } from "./lib/auth/forms/phone-auth-form"; export { SignInAuthFormComponent } from "./lib/auth/forms/sign-in-auth-form"; export { SignUpAuthFormComponent } from "./lib/auth/forms/sign-up-auth-form"; @@ -29,7 +30,9 @@ export { SmsMultiFactorAssertionPhoneFormComponent, SmsMultiFactorAssertionVerifyFormComponent, } from "./lib/auth/forms/mfa/sms-multi-factor-assertion-form"; +export { SmsMultiFactorEnrollmentFormComponent } from "./lib/auth/forms/mfa/sms-multi-factor-enrollment-form"; export { TotpMultiFactorAssertionFormComponent } from "./lib/auth/forms/mfa/totp-multi-factor-assertion-form"; +export { TotpMultiFactorEnrollmentFormComponent } from "./lib/auth/forms/mfa/totp-multi-factor-enrollment-form"; export { GoogleSignInButtonComponent } from "./lib/auth/oauth/google-sign-in-button"; export { FacebookSignInButtonComponent } from "./lib/auth/oauth/facebook-sign-in-button"; @@ -42,13 +45,20 @@ export { OAuthButtonComponent } from "./lib/auth/oauth/oauth-button"; export { EmailLinkAuthScreenComponent } from "./lib/auth/screens/email-link-auth-screen"; export { ForgotPasswordAuthScreenComponent } from "./lib/auth/screens/forgot-password-auth-screen"; export { MultiFactorAuthAssertionScreenComponent } from "./lib/auth/screens/multi-factor-auth-assertion-screen"; +export { MultiFactorAuthEnrollmentScreenComponent } from "./lib/auth/screens/multi-factor-auth-enrollment-screen"; export { OAuthScreenComponent } from "./lib/auth/screens/oauth-screen"; export { PhoneAuthScreenComponent } from "./lib/auth/screens/phone-auth-screen"; export { SignInAuthScreenComponent } from "./lib/auth/screens/sign-in-auth-screen"; export { SignUpAuthScreenComponent } from "./lib/auth/screens/sign-up-auth-screen"; export { ButtonComponent } from "./lib/components/button"; -export { CardComponent } from "./lib/components/card"; +export { + CardComponent, + CardHeaderComponent, + CardTitleComponent, + CardSubtitleComponent, + CardContentComponent, +} from "./lib/components/card"; export { CountrySelectorComponent } from "./lib/components/country-selector"; export { DividerComponent } from "./lib/components/divider"; export { PoliciesComponent } from "./lib/components/policies"; From 35b4faeceb0a53bcb4e0e6cc66b68f65dd5354c9 Mon Sep 17 00:00:00 2001 From: Elliot Hesp Date: Sat, 8 Nov 2025 16:20:54 +0000 Subject: [PATCH 24/32] chore: Update angular example styling --- examples/angular/package.json | 8 +- .../angular/public/firebase-logo-inverted.png | Bin 0 -> 10096 bytes examples/angular/public/firebase-logo.png | Bin 0 -> 10920 bytes examples/angular/src/app/app.component.html | 6 +- examples/angular/src/app/app.component.ts | 165 ++++++++++++++---- examples/angular/src/app/app.routes.server.ts | 36 ++-- examples/angular/src/app/app.routes.ts | 48 ++--- .../app/components/header/header.component.ts | 117 ------------- .../pirate-toggle/pirate-toggle.component.ts | 52 ++++++ .../screen-route-layout.component.ts | 41 +++++ .../theme-toggle/theme-toggle.component.ts | 63 +++++++ .../angular/src/app/home/home.component.ts | 69 ++------ examples/angular/src/app/home/index.ts | 16 -- examples/angular/src/app/pirate.ts | 96 ++++++++++ examples/angular/src/app/routes.ts | 126 +++++++++++++ ...mail-link-auth-screen-w-oauth.component.ts | 33 ++++ .../email-link-auth-screen-w-oauth/index.ts | 2 + ...ssword-auth-screen-w-handlers.component.ts | 50 ++++++ .../index.ts | 2 + .../forgot-password-auth-screen.component.ts} | 14 ++ .../forgot-password-auth-screen/index.ts | 2 + .../screens/mfa-enrollment-screen/index.ts | 2 + .../mfa-enrollment-screen.component.ts | 43 +++++ .../phone-auth-screen-w-oauth/index.ts | 2 + .../phone-auth-screen-w-oauth.component.ts | 33 ++++ .../sign-in-auth-screen-w-handlers/index.ts | 2 + ...ign-in-auth-screen-w-handlers.component.ts | 51 ++++++ .../sign-in-auth-screen-w-oauth/index.ts | 2 + .../sign-in-auth-screen-w-oauth.component.ts | 33 ++++ .../sign-up-auth-screen-w-handlers/index.ts | 2 + ...ign-up-auth-screen-w-handlers.component.ts | 43 +++++ .../sign-up-auth-screen-w-oauth/index.ts | 2 + .../sign-up-auth-screen-w-oauth.component.ts | 33 ++++ .../index.ts => services/user.service.ts} | 16 +- examples/angular/src/index.html | 7 +- examples/angular/src/styles.css | 1 + packages/angular/package.json | 6 +- .../angular/src/lib/components/card.spec.ts | 57 +++--- packages/angular/src/lib/components/card.ts | 40 +++-- .../src/lib/components/policies.spec.ts | 11 +- .../angular/src/lib/components/policies.ts | 36 ++-- pnpm-lock.yaml | 72 +------- 42 files changed, 1034 insertions(+), 406 deletions(-) create mode 100644 examples/angular/public/firebase-logo-inverted.png create mode 100644 examples/angular/public/firebase-logo.png delete mode 100644 examples/angular/src/app/components/header/header.component.ts create mode 100644 examples/angular/src/app/components/pirate-toggle/pirate-toggle.component.ts create mode 100644 examples/angular/src/app/components/screen-route-layout/screen-route-layout.component.ts create mode 100644 examples/angular/src/app/components/theme-toggle/theme-toggle.component.ts create mode 100644 examples/angular/src/app/pirate.ts create mode 100644 examples/angular/src/app/routes.ts create mode 100644 examples/angular/src/app/screens/email-link-auth-screen-w-oauth/email-link-auth-screen-w-oauth.component.ts create mode 100644 examples/angular/src/app/screens/email-link-auth-screen-w-oauth/index.ts create mode 100644 examples/angular/src/app/screens/forgot-password-auth-screen-w-handlers/forgot-password-auth-screen-w-handlers.component.ts create mode 100644 examples/angular/src/app/screens/forgot-password-auth-screen-w-handlers/index.ts rename examples/angular/src/app/{app.component.css => screens/forgot-password-auth-screen/forgot-password-auth-screen.component.ts} (55%) create mode 100644 examples/angular/src/app/screens/forgot-password-auth-screen/index.ts create mode 100644 examples/angular/src/app/screens/mfa-enrollment-screen/index.ts create mode 100644 examples/angular/src/app/screens/mfa-enrollment-screen/mfa-enrollment-screen.component.ts create mode 100644 examples/angular/src/app/screens/phone-auth-screen-w-oauth/index.ts create mode 100644 examples/angular/src/app/screens/phone-auth-screen-w-oauth/phone-auth-screen-w-oauth.component.ts create mode 100644 examples/angular/src/app/screens/sign-in-auth-screen-w-handlers/index.ts create mode 100644 examples/angular/src/app/screens/sign-in-auth-screen-w-handlers/sign-in-auth-screen-w-handlers.component.ts create mode 100644 examples/angular/src/app/screens/sign-in-auth-screen-w-oauth/index.ts create mode 100644 examples/angular/src/app/screens/sign-in-auth-screen-w-oauth/sign-in-auth-screen-w-oauth.component.ts create mode 100644 examples/angular/src/app/screens/sign-up-auth-screen-w-handlers/index.ts create mode 100644 examples/angular/src/app/screens/sign-up-auth-screen-w-handlers/sign-up-auth-screen-w-handlers.component.ts create mode 100644 examples/angular/src/app/screens/sign-up-auth-screen-w-oauth/index.ts create mode 100644 examples/angular/src/app/screens/sign-up-auth-screen-w-oauth/sign-up-auth-screen-w-oauth.component.ts rename examples/angular/src/app/{components/header/index.ts => services/user.service.ts} (64%) diff --git a/examples/angular/package.json b/examples/angular/package.json index 1e12ca98b..1c116b2e5 100644 --- a/examples/angular/package.json +++ b/examples/angular/package.json @@ -32,10 +32,10 @@ "@angular/platform-server": "^20.2.2", "@angular/router": "^20.2.2", "@angular/ssr": "^20.2.2", - "@invertase/firebaseui-angular": "0.0.3", - "@invertase/firebaseui-core": "0.0.3", - "@invertase/firebaseui-styles": "0.0.7", - "@invertase/firebaseui-translations": "0.0.4", + "@invertase/firebaseui-angular": "workspace:*", + "@invertase/firebaseui-core": "workspace:*", + "@invertase/firebaseui-styles": "workspace:*", + "@invertase/firebaseui-translations": "workspace:*", "@tailwindcss/postcss": "^4.0.6", "express": "^4.18.2", "postcss": "^8.5.2", diff --git a/examples/angular/public/firebase-logo-inverted.png b/examples/angular/public/firebase-logo-inverted.png new file mode 100644 index 0000000000000000000000000000000000000000..b6f4ef80a14fc7bcf413348af51d1957b39e9cb3 GIT binary patch literal 10096 zcmc(Dg;!h86E9NSD_9e>6fKlu!J)WQyf_UGDWSM)ae}*-;_k(Zlt7C+1VU3>0;RaW ze1GS?zv10;_S~I0`2Vb8P%N zESd8Px`ResqqJ(%G>2sa&RBU`>b@AYH>(=bQLQ|517CQgtgbI_Z;(T$g4d|r8~=%` zz{+enEXg((ueJL#A1M}$Jy?@|#fF>n7Tqk7ID$M>g<*;`)^Le*KSk@;cd_yxdoOEE zX$qN5aNOg_hTK1~P~<-vuGggeCOFI9}6x%^-_C8sJQ**wWO^cG0}|N+zB| z^oz%l9VFWC&$j)3uBlki3pmbfa5Ag0{%hK_Rt@Ci!k6K!#wYhxjeI{s`N86LJAH~~ zB}nVmO6ZWF95Q%>N`hauEJtZoJfvE&o-qO5iJQGIJGUI&^*^|At4E%i<s?Jm25dZ-6XK_K=l zm=;HeG8};3eHzqhIMwVdBoNmOXU+fOR>oSVDIL#nu%lfwL?>xc+~9d?2@gwV9nTyT zE}eQJxp-W-5yid}?wSf|f+pz?4k(ULX)RiQo$34h>U|De5+Oo>Bvew@5Vw-&CCQx( z14{K%>9z)ZM&tNTg#iWosYq8jS(8k}9!x0#{Zy&+YSUNSr*FhMN96vYVd{01@2O(nu3cbm3CjU4s~gq+KJ#k21u3M65ji%qi?Y2O(x;{rp(& zLzYN+>x6UoKSDUx3nH{SDE9x(BXn@6C-05uQM6cy@zVfoYIT}&%UiHjI2h}(@Jv5f z^or%Z2Y)!2b|gDq*4VIId9iP%mGmshPt@qM-j3gNUujj587Wa15mc& zRu4{$;ou22_2=Q@l?x1n@O*((0BT!fxnh+P7GSr5!1;#YPpB~OWCDZ?@BIFY31gmk zPE;7lk)$#8kh!)@ziyiu~Ik7WnxD z{Cs;6d1REI=K5*s-aXI~6O4AYOgoyFJog#dzow*tnzk&r-=#6Yx zRkTHSfgk)|AcO^j*6wqQ*KSkb|CED6TgOMPPL$5Y26PGGLZcLrzq>Ia^x;cCMJk#( zNyb=B`Xw6KX4~!$dw(66yt?su1WaC4U#?qm&+iktDi15ZQE!@#-QBK8*phh4%A1MnSeSz z;_<=_ke#ukL{ie}XEM2~k$*wic$-Q`(f;=&*z<3qO#^n#L@6a?LE{aOxSJUru!Te; zBr&L^eedCl527s9{Qmk0c6w7@kO|_`l;`-jgjB+nR!c4yCy7LxrqgN>?+ zVYjs>SN}+ac^*a#N~CBkk+7^`y9e}kv*ft;r_r<8*>dOT`W+_HR|-Il}}DZK=Y%dp&H(*n;=YNW^eAG>BMuLxCntHBhJ zl-^U8v*X!y-CR)vOj~pP#iVctrMN*e8;_+(ih;(rokKMQF zHG%(j$a9TD)p2|VOb(f*IHQ(xG-UA6Z>xa%p{r>Bm)0V}B_HbTqJ8k70sx9Hvn(30uef(65 zG^{U-9PV$ce%ajC0f%^r`9<`hx>Hnt_w$NsvKZ$!ZR1uIJOIV)d~NMc8T-kH>j(Iw z?bl)%+QhlgxmU|op;Alaoqwr3K|_;t0y8rV`##H}a=Q)EZ;u+t#Q|*_h{v&5{qMq( zrollkLTIGk@`(=3Y?bG|k*qW3R!z8Tu<7UZ50^@1Ps@B%zf4vFzXvtk**~tI9N@34 zc_CN+zeS0Uq;BG%q$*4k24TnTYt)7MeG%ZEXHKTRQnda^x3y>PK;WC5G#S+dv9)Zr zu@1hCS5NxM;BcyRfIbr%rfN4DhLXm&isLtjJOc^QRIs9HOVWzdVdt!mZ$9ya5Q_jNMvOktXm_p~2I@{KVA9HK zj>5_o@@*?+itMK!L4ov#BH03DL&2&E%lu&-C}3X z5$8=IHYyb${*tKEtNxyd8BTf>N1m1WXp%pr9!tT_ znLxrr`!f(`L41=lW`c!Tb?YQ+m|MKDXfkr7Q8ky!V25KA_hFw-Rs>KW{bgg(3*1+c zZ&>32qHq##o$wrbWWgQF3|OPe_0{y1u8y1r-@K&qul+!%v<##<^EaYEEnj6-p==Gd&lLWN{`*&|g_yHCy0K*T4(%#ERSTRsK20Ci@ zGY(H{W*R)_Vw`E{7%!|76kV<`mBp z*?XC8uc`1EkObW}6yl}GAF9g#A%?qsVlvWzOL+RL2vp9ZqbpQvFcHpQc|yBByOtV> z3PGjPBa#d&;E$8Osls9^&wPz*G)tN|L|a)~xmsfQ02RhA^Qa=pf;*kb(8bDGt`6u| zM>I#8rsueXt*83>hiaXwOI28wJ$@r~DdvCYdfnkcVDYgHsx^N|;e4g~PM&Nl@_x1A z2Hm9_i{HQ|C9qF83aKa%kdk4fQLQ9EO-xQ4EcNiYP$haL!QuBsmr>Q6*upi;vdOMQ zK$k?iOrX@&gepk$3&ts>HPm2oeYVTzmviT86sLM3ha3^z9}J?~HZqIALvvo%WoYES zmefi zwoeU)RUQ`mh_|~_tFhBRSDY$m$r?+BXzd1hVx$0^lD>`(hiB43O!k_o0w1W~ebP&1 zxU-+@K%pcH?$sv~Yao@gG0>-~37_q^)#cC$1%bnk67ebMCJRyk1 zJ{02gb=Di>7Vms{WDtxNsmq2(MFOSFiibLm5(=R4;?o_iGMahN{ky7JQj|k*I{o|o zQ__A{@)CGx4{r-vQZB87POLUbXZ?NjZ_cd=n-_UVp2A|mVJ_ayyR(PF4I^&_}zfb#Ji3|=nmBnoXO}I#g6N0$PO;`x{x}R@< zXw9L7RXJ;^CAg1YPFq$N5}2sPk>1wQ$$8CVoO)Q~&!2wIw|@1yoSRb#ctSbq8{~Pf ztEQNnLeRNBmO-YJmpAR*ck$jcI>nxYg^KR|&hjI!DdFQ;{qEjcA)<@TLh};_Wqd>! zVIsczN~b1>(#>&bgs!kDS?XORxU%k!5z4@8b1 zr2h^aNqn8%2~95M_O1Kn$|0-cSe*s2Qy-sF--v;_%O82*2|xPUD)eTjQ%arcR1V~; zd|$j`d+4FUjiV!%wC5h+{2Cb@Fu-HcEb99vvXRvyoBT)x@5ifV+52^bUWL;@ecoFR zQQz#^l@zkD!n=mJ_Qzj8wyVCLf02VHN1%=!4#u82hZB(@N;a{tE`nIfM+S%Wm+3IU zhD7yV=1}0i0jJe!yZwl0&-J8};=N1>$1*_eO_1aB_O=bOklu*{^aBE+Vo)>hsiX&9 z#y-)mCBk$P)P9QFy9a|#>H=Qzz;3c2ez$*v8fPrulb+V%&+R3b0tGG9`34)l=bh{E z#X0zI;U&4;H4L{$wEC0j2v6R`6bkk?;J8UHg8OWZ^UNCzit*P)9+WqJ@e%rOO3t+6 zMkGCbR%mR-e8Q>%xygcstWfb>7&Uq-MpmBiJtPb(9TA)6;;$QN6REK?8&Vf{;E)}k@f^Z2VZ1#dXnlIcx@E?fHtfm} zr}STuB*e@_V_%<^iC5fr+4P_wlP~*vJ_cG%g`G7+PD?gK9I-P=p7pD8+K_PQni3}T z3-@3CV28HC=RQy|?odlM5bdqJO#56GB+PX5WB#!<7yneg1KE()*H2RhO+-UOlrXaB zH%x08%Qt9NgaOKaf2EmAl6n>fb=GD`H2y&zR9S?!O1DsEbzeqF->UpB8J0gpJYLtw z_ZEqSI)_{dv_mYeMA-Dm@Ymy7r5!on435)NC)pFA79T%P^gEyHWiE1Hrri$sXVu!F z#ZfXGowUCeFcFyb#WfrpJ6iD5W+~a^@muk)o;|mD&Gc~svZ*W(cr35pFW)#cGr)ve zUP&Xqd)nr-Mad)sF!M@p*V};xwMf+siLAB)faj!qGgq%ElbiWGF&de-A{(8gfy?As zs*JbBjVLim+BnJ8>oYhIIUMWgzD)@OrWiI|pdZM;{tEo)62#g)^P|$!Jie#@b>2s2 zfOut7_tJ9D<;D3q#k7-g=xnle6?4|`u{DdxZv5fP{)PcD!h+Si*0;%; z8HcO@zrrVwB;)w`7Fw+rAo@7Q2SJjb%W~7{)R3}YbN@BFLNzgqSojnUmmVOV&y8vn z_rD(!8V~G|tDvKiQtW=sug-B%ivsbyA=FR@*StzOovO)mwk{Pd_g+jvs_&$4f}n0` zroE~}(8q}Z8=yHa2Hbq(p$DQT+cVe$V%`#Ijm^Y&3CU;jN(RO&gH`Jh<~vN3Fzn1$ zC8<`n5$k1!ELEx`gQ9?dYb)whHG&i;4*GVt!`zipvxWykc<@u=5err7n1+v{+LKwH zBNkLK%q#%hmIjSvZ63Y`NHqH=k#=5!(BGH}{#S;=jPM9HB<9)3+v8HQA4N=wgLkNdBa#>CU3R~n>U5ZYh&0NDNfZ@1=~1%2LUGiW2F;T>LT<{k{hU*3Wg zML@8I_3Ulj|zu;+_Mwuk1ktaMdk^%LPm>Tui4(MF0YL-Y3`B0pQC#R7{;y7d4 zjEP)jl7oKNNV%J;ihkb=->D!3e&lf@pt}#=k2`JQBLc zBKh|sS@!@0IKUb~c=jN4*=+3ZEC=P0Q6N-m{u$xWvA0Gj{$Dfr|Ic&F_81F1igzRD z0{2x*w&uXne)}QJh6&ipyK~Tfw9F`S@XGA6tf70gxz~@WJa1oUFoW9nhx=LWX# zjQsgRc=y30$BmcV|HQFa=HXC(joW8eKrE_#Fz7I88h3qa8Vl;cHz8dP4+|c*e>6g= zf<@LpG66qK*iE2vT}T{%kkUQ+_3gPbe9>*WrEP$1yE3SayBvs$(!SEG5@)wC`t#iy zok;Vqmq9lkU)!K=z*P~!j7})Rl2Pz&|N97QHXiyYn2k^g%P91}N$5`EZg$s&MA{6) zX;wowa1*GkFFg*+%@cCZl}5UzB`k&C`jEZwliFR89?MUGDRaDuZx();!zt*;IQ=mZx2HNOguOZHVxVqDwW;($Kz&cM`8U2n=XnVBqIq_a5{}8KuTBdQ^O;&YST0#S3+qI1ui0Ndl27PUsxQyuG zq}zFU^LUzXvXnYj1+9TVtr>X`^iS#Dhb(1eZ@?E~zs?eL2G1f7YdVco>9;3pMkpYt zU}uiOg{k0Eq`~6-c+Pd_bXR|f)CHShqV2AScl}-$kx~%7X-gXI$$Hr|L$?vWjwB@A z{f~fs`}O$BzsttT8pU-~E8J+oTR7OHps98x=yZuK$hsCIw}*dR9$7*WoL*V>WlqJPb?Cq`1Bk)SnpiN4wryX}BIQn_ zE_TEmp#e9R*baGW`P1~IGz?JCT!m3+(|;VoIVEM+7&|gItT#!$bLD>_45(5E!PMIq zYKOg5>ZW5xdPWuX^DkB!Jh;jyj0W?B;GbH!@v`QMw$AYE8kf{-6-#TJknV}=DIRHJ zfGT^?o`?=A!f&01hZS78|APOJtxoBgRHV;!iB8ANz*Cap=I`K9RYq1_^bK@ry{oQ9 zwAVQQCK*mY3f!N()CxQE=~XN(aYF92IEW1&M|Y?KVfrrsjLIrQE?YljxU3s>f}JXC z>bF1&{jSzrghQX*{&I$_rsJq8gwNko3{4{G5$0&>?n-$=4Pl^x6<*Y6WAk7 zRU>X)Xo3kEwC=CA*vKo6JgVtv%|2Ufc_-IA3L402`1QKx@kig4wpxc|1z`KiNHy=V z*j-QVIK5->BT;DATKyI}?MQ6JE)sgWeS&SeeFMAbx`m3#!B#(8`ZEj1w9upMk zI`o##_{%l08#`!6=?y+m$an5YfcqFq65jehTXSUCmd<&llVPJa#^(l?Ix16rKIB4q z`J*FaMWl@TWxF@hi!&faAKo%sk)Zohdng(2%8bmzZw1$tdyKdH{vY&tnnYQVbzhOJ z$(1MhOQ(ES(Oyl{6R8=3x>&y{Nxk)$(5bSgTlD50Y9+a)=PY!-D;J^rcPHAje3K!1 ztZB}QvFR`1I&Vk?hYwtv4c5DpCvs&2a{`H$*LQD(QvGABvg$_%5NBeCgF;77U`n+! z@i@Zij_##pvjc438H3SJ~ExDl;DU7P3PB*bN2AeWsd3Qf8Nw? zh~mvG_^x$>E-6N(fXD223wWh z%Ia*$u>$A;s<$>;gzN2F$jRYl34hRf%f zWi;o|I~w7ioV-ivKA1G)HqQx!Od{TdB;?&a&fPyd_d)(b5-d3NJn2S; zgZ)eqmdNtXY^!V&1XOz)oaLN1HhX;N^6IvB!s*}|*-o#l@$Z=z*H!2kLMamFwf6oE z$e-gQe(9DF`^f^EnHOV!gCqg$Vuk{uj~J3->y;rt*{t%^xFab_e@|;wa@!SPUUowg z^s&j~n#A&i`^Q0}~YJ^&yu966s8y zdZ&@#5OXAD^u9Cs7_p~|t_8vN4RHN*u-AL}GgUJHN_E8pWW9zXXW(1pQD2OkN6EIS zgGu#LSHvuwVM%1 zWXybXV;h4Z~cvu_uB;Cwgc^yFnd*1)|wnRC-B=|V7EXs|iisg+=@ zFVu>g3xJSqtN-dloy;vR+l3#N3U*}%jN336^ zdfO?M_BkQ#D6-VdjEPp(yCw|3MaID{I=G$*EGZbfqrW%g^4dtJH{n8Bi1St3707F= zO_aZ1qM6q}aM%_6avt$tO;j(KkcpYn8C8gM>hmZ_p##pl1n^yB>~G67 zE|?U+K^7_j=h+im=9na+VIlDFAAHhH^G88XhOfpR&d-^X{F}Bbj47?NT~%VODWBQJ zWcXP88cdU_D>u2*{P`YNAjguqtDo{G`VQT#y62Xb;pPRXhF@b#2rkW&%8f|XH|<$- zE!}#fJ!&B3+JSdZSEX$Yw`zg9V}X7IIp+Hk;b+-FO!ucPD`~zf>b53)&%OR%t~}@1 z((&C#u&?6PuWyD`6dLV&)^vV8?(xlfo0taPDcI05kvdJadJ$rb@e`%Z|OKL`8Ge{hva; z;tw?8V7NOIlMAo;3w;Mv7_PiXrHUAA#C27 zFQhk1ySv6V2-76aai2X`oln^pu&u3cPY&IeFku9bC>o>&@?P=t zsz@>}8uv#1qwqJYJMJp7UeMfyb#{p%_39~X286Ugo<==7r?u8=aoAQ#a+az1wYb2HYlJf?|b#F7)YD_(4V@w%Lf zjD`j9v9Cq^6VLM7E`$mQeMhX_s_jI8OnE*!e92>jQ7P*Kp7uYGG7`hNh?#k%4E literal 0 HcmV?d00001 diff --git a/examples/angular/public/firebase-logo.png b/examples/angular/public/firebase-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..1cf73144090adbd7a01a4e92ad08a39eaab5d31b GIT binary patch literal 10920 zcmc&ag;!M1_mrdvh?2W>hje$RfGFK8y>xeXEDcIace6AIC`-pmrz=QEOZRU--+$uw z&YAP>y)$#qeRpPV&5P60P{PNhz(qkp!BRYJBe*qBHPMbJ4v@`dB7Y~X={ zf=B#cLq*BSBSR8VJ+zf%QR=4v`$z=CR_4773QAKl-h;&p6cjcI6?vIYzNkm*o5^{L zL7QhMhxN(|S-*6mR^kqH<>N3X>^qA4IdFp=q#-Mni&GESAr z<09%u$+{ou{mxCfbHeiaA6t#z6wze>W?3i_nFYvP?8v-rAD=VB9P@qk>T$Gq2Z@s0 z?pgiH`suO-$QDV(kAB(FRD_(?AL41zRnOZ+Ubl{|os1l(ne!jiu)7acZ=hwwrqMfG zZ&Y%+84Vsb)Q;{G-@DUDyk&z>aT7yqN>UY7Ym+9(CaZ8aYO8OUrH>o;LSVC zS&}7EIBQ3S8Tn2yYEHxwuOO4&bTS4h*)jA(WCjkImxRODn~0t;bPa2 z^F&8)O#FSaePtJGFJY{1W@(f`q9_WkPX|q18kzc~Xah(_7RKBg4*J{_NWR?2-o}B& zcVyjgc@FI`=}7u`x6*;1bLO6{5q7J(l%(I#oggE1$FyQ$OE##c*h<^*$^v_{3-cj# zMC=5UsCgG$^1>WMy8=ixNQju7iAajVRfZUw;~sJsSOZ0|`geZ`B>lI_^CO$kb2utjJBz@oTjs~-7{*$PGW4c@$#kKt{MN(M_mKZPt;8qyy z0HQ#jjmUqc3h9W^Ak!>yBGG7y_g!NV`AvndC<+X;8})ZxY7X)sM2b1nNUUh}@mCbp z;SCXl41V<%iifR0;5~<}#O=wP3^8uL-3{L(gcQwvPM?&jz!H;;1pQ_aAPLp^ULr5d z1knb`U=CSmjnGbi*N+z3!~CWiblA|F|Z^ZAogG|wW!6t4~P zYmM0X$5oJ)9TqS?DT+)pM zSq9s;h_DFgDv8kOpT&l)yJcYBstPn*&;!7WPLwhB0;K15F4f^Ha}*X{}tzEHKIhi0Sf=uKncDaOtb)c#mp`*nBMKGR=rMpd215p$47`%{&XGtI66db4RHsJ2qOovNE`A;C3q#+4o8>jx>c zLrZ<6qGj*CYC9zUKx(VH$UPF2S3oQu@hZokTaJmBLzATGe`cVQqY{G=Ho_grYzgdC z#(M9C4IA;S)P^~($;jCny}*XuIAZ_)@iJ$O5JE)$>el>dcyCtbBmN){U8@jNBsqT> zJ%C;|0A%QhiI4t$Tuyk{N?Oy#QvY|Yd?bir?S-sxx#H^Q<%n_s5`9I0Gi-%#be#TXl20fE%$os*&=)dDOiY{sfQWju zMI(Z6F76{wb<2vy^8sNbXx!_wAa=;726RFQr%=&$EO!H3<0!_}u{%usU1R+0)J+TCdqo3seQgTlnvTCd9DGh-J1O?M@-a&ohg;*2%a|f&MKcS zPq#iO2S2EVzka&oUAw-^_$8SEV1B8Vq~vb2B0HoN%yS=-6!EXbN8$KMF*FJjD002M z+RWY&~oF8N6-1$z0EZA=gy#e6q(TbAvv1}f4IjIcHgrU@yAI?z$_M3Fsnt||+T@>j!N+9vQm*`UiK z&K9+i!{gP*Gc2nIL`RzR+oC8&B@mDgxy@!|mDnRe_xP{%FS`|n{K^e3b8Z(o-*_-r zq|3I6K3%0|y~HStnfh|2V!`b@huE_j{yyzrW3=XN+hR)w*bp^|GhRb#O*X*;8fbo5 z5+c2;K3==|bnS*^wT4(yvD-`_X&0k7$op>#2^u7IpAz{9m(P&%Ex@S`R2D?!_M0Ru zPnsZN0KIKD6$K20-GDNZB$`SpX~`v0{&UAJpO>@2kWw}IsEIcgIs0C#puUtLeBPNw z0t!pLDpC1vNdPN_rX_xLa6*#gnEx%?MuODo{MtzplOOs}+TsNP>$5-Wk=EPim`=XUj2Apij}he(d{3NUFhM`kEUM!1-hPE^NfN&v`LiH9Jg z1p6e~p|gwHP&jjLP*5PqN@5r(2+H_iIQU5C#8RAAqEoj#_STCA0J5@xHv@47%K(p0M+AB&?q2B=KvKQf4^vBG7VoOw`z(M8-dCbo-x0G)9|vFoDb2 zv9q-4Y1pDRq8!q*WHm&ZKz_HFEp)fd<#2@=9*+kzoE*!fG!WS9$xgqn{6d&y!14_{ z?8T(3t7TytX!R1q%-(N)CHlM(3VLkiW4bdmz(w3-{bEm6K8U&0@~<_M3OitnaxGnV zGaQe(B!u7-`f0YsNdd4Tw-#G_E|lB4f@^{Xn;u``z`l@i#R4>X)P$i0MqRbc9feUK zH8t}Lz(H9)Pe5AeCtbAXxL*f`U0maA2>qHjq9An&#FFNg25Ie&GNrjnxltY}0z!yK zQw=p*U48j1q)=YC>kF^Mz@ou^leNU6SukDY^i$j;e|=eoH#JfqDkybm1IDlrq>LES zDmFzrP)q&^MDs72fiB-wJS{i;*6@@6;pW>p`Blnm2BOi(63UYf=FgL=5Ke=piV!6& zbjnS#dsXptZ3mB5QPd+y!h@0h4+lgHHJ-!F$%uf8vLT{_sXP4EaRM}y&0nHwQkYRa z$5iF&Dd&HMalY(;z^5Y9sA{6BS6xq!EY)%6|5QiX;^3u6k&p(G(5}(Wy%GrEh+VX7C;T&Nnr4 zOp~q;?)q&udV|)ryImIZU{y$_*bJLj8Zk@SOBf>bR6fgO?5G<~wm|N2Efmh?wQZw{ z-p^cSSO@J~(8Cy)U`YC6=$k$Dd902hFrR@ElK(8sA##k9)dGdN%$fNYRdl)EvmU|^MGd`}cJ0q@(oWP$qzzElo3hoLVn3;59TOQB8g-NHN?o{a99?l11?`kBE=+*0uD4tg^gw zv9OP;QX`GeviyTPe+Jc%{(RSOgoN3Hs#a>IPW4>0>l@YNFZ#Rc3P;Iif!`d&GxtZ{ zl?@XOx+TIv9q}vFQcqDuxGM|FRcGvxC35>b=-`k*Syi*%n5ob4FSjYmh8qYW=`0e_ zE&EZ7kdRh30Xbhb^D@N+(Ov?Ca2On|!8rkBgeL=?C;BIR#7!1Mv~U=Ln0b@bhuefc|<&~ZY@dF?B;WY4L{W^;}rtX0*-mk;mA<$Or!0K2Y~Zf?J= zEF_jpM*j>I;;Am2jK3AN*eQ{-!8ywF``DlNCi1>b@<}7yplVjsQj1R1BCB}2Lmg@6 zCmek>qeYfl1+p}2epbA|BZ8=TKIvbzi6#H_(iM~zr=h8v8(A8L^OVn`&pT)u_V$&T z31Tek@5aOUrNQ8b(-1t>V$z3=>y58-dl6|RT>cH`XDl+>4jf;geBKLoO#c*w{fn== zP}I=iD-`4xCRF9v(X;?`&!-4un`18x4>Y)J>R(SkezJzrJ_}uz6#zQE^JteiH(=xE zTPvY?%v((K4{>Qa6W5wR{EqRd<`3hrhZ|5jWKTE)`mPLfbPWXGp5ZOr7Q@`(v62C(N1**rd=H%f`Z;;!H`=-i!>Sc63e+CV+ffy*d zjzBWVOf6Lkv~j5p6mcEs;9_}$(0`5Q9{<7!V!%z3*nf9|u^ogxtW5-Aq@w{Igf^LT zn8&_4N|~x;3(7L*AVBEzlBNhT%tx`|z8vPES5;F?RN^kx zjktr~MX5n^-gE0lyVfeFq23l`Y`7FIL2>Wd!9&%anp&cRO|tK3>f(XVpX81#vNn|& z9aj)iPYT;*BapSZ*IOc(zIrjPwFAEzwwtML^%!;EVs3qIlQLxG9iE~fNv*^}=o90Q zI7!@*G71V{Y?V$+k<}d&tFbmqrQmIb=(&_v0ucHo17>68wIhsiUx9E@vnNvr{NW}R z)zpoPb#OQStujS5l=_d^sj=jpgee$R*ej%-+Z&1unc=&J?^+R~+&(`5h^-y5a{P!8*~H2kF&Djf~3 zOYTo!u(#A%)hU_gRj)9>hDAF#xYbgG7Z%jt zQcYdxvzy>l}<$i!(`x&Ae8_XUpkroYSp*HENIvs?f~=BGiIXd?T}`7WYw;_ivm= zZEJ_R77V|4<^v6Yp4%~5d@jw(c8O{^ljC)rY|P#7h!0v*0CnfD!YS-vu=^M9A6Li> zK8=+jguO&d-D^B7@fe)_5UG_&gdnRbb4rV}3iKl_{@tWCVUm*(|?HoC6D$X!5^W2)ciCo^`)BqvEbS4cbCo4bUM2jgb*AGV~Q*$p|ZcI ze~vuN1KK;P&kJZ+BSC}w42zfJ<+WqvPK9f4$nd@g;mfl#9)D0(Cq=~4l4elW%YQMP zUqw63(am-0JC_W@mJ_yQ!XL+P;#nrE4^``<6mrh zbiZh$PabCxU$lNxV1lzq`P9z$u=2C;G8Ia+QpkBzYZzEWyF^?>6G`t-X0eig{fsU4 zUs?G7=Na^3K;!DW`K|Kk?#@jHsI9N3de~EvS|LUm`Fj3uoXbTyf)iI1{|D!z`v>av zs^|HiuqkjuFO;1n|3u{bdpxJ+g_CO+m6l14^}Y+ZT6SxxZ}8N$=cSuY;rZd^!6Fy7 z>iNU@v=92!R~830<)Qf2lq@o?ci6)Lhax1!XeeQZq!1$`Ba_GGqx(R-k#Zp*ie14% z)K? z6vCpU-gzXf264eb!LTzCrjiN{)Kd$`qTMVhn;b_&L0v#A2>3`X^Yv5o`uchVsj07p z5<1-H;rCQ>$ir3viZsfJtFeJWCp=@{tfBwu?hmP1;H;>@yLWznGZ#$*oQzOtwAgJ& z$VmNnTQNF?9-r6x^MCGg28)#;*3f5BEl`5fko5la3aKpX$+OZoE3ieZ1iwX@!{ulv7EJzZUY zFc^F%VT#c+Q_O@?CJU}pt6CB$}jJ`J7)>q-Rl`u7mzc-E5UpKCN!Zms#9{UF7X)SNT( z&5@8M3Ap`}Ep$2LucMe7G--k}GHgKw1m=(PZjU%Ic`JqtM?XQ5LS5DuPnuwDbW%#) z-!9fVj?RsHM|Pak&GB9z+>4YD7zMPjt95)^{wdY>0cI-Wof?c}YIFzVYu-LD5WHPU z28ux|NZmBqQZ5A+Fa2T0G<^=cJ&{0gcj~eIxBj!cgNDrv={kI+&BZ#bo$t+r@WM9E zaog32-VUb{uG}dy_pnGRO;RdSbac0Z0!i;n`TneHc>-)D_AqhL$K)5i4Fzv5g0om} zJjj~y^r^+HP0SSfZG!5$3B*soX^cZIxmn~)h35YP9LF(xiOt#wG^w?t&v1%q`)Pmd z{{$IvoDROtg|IT_nhH*3S{??yYwKYrXxs?IQBR=Wd4uidts!PU5ye+1N5 z&NQ8$pPrs-&$1&9!0XMRx`y_Q6o8qYLgD$bm&;!fcZ2EFeCti=7hf8O$O>PWaM-PW><>8x zqVC)jr!%AZf8j&Zx*@yl=LOQjn9X>Kt4sB&Dr+z7nN{lX#L(FlHuOE4x92=g>c$cT zJ*0Jseycg!F6%C-n!aunvkl|Ly8Hg#POjRzcfI@ER0u&f5?(X$x&&{ye(t+Q7B94U z{uglWbmPzi_m%SEju%+#d_zvxavt8}Q#G$cd|~${@G*0^!nm` zhU?1~PR3c{)oLvhobBi^J^Ck~rBiN8bsquCO$%W@ax(744w|A(BT%fJ2k z&DR#Yki=+}sK#&S4zC(#akb$hT;!(hiO$7l?@LdCT$SFJL_OzbB$72gi#$`aF(xB- zh@~ImezgT&CAUJu&)gzYvzJ#<(X($QQrp+vozyxvi9C7#9`rUke%2hJIaOGb-eSLN zx!Os(Zb1k+AR$o#jQ0FSR zPhAVxiulY`De!@F{HJR2-JT*NLDnRbrDR}jVF;JwP6$7|5|DSF_XL>8l!~t!JLOrS{?3cUJq|fvlhDf)sMAHocJsqEWnm) z(c9}ddCaLxEwIh{gC8$?o*NK$Knh8x8bQWW&%Hwqp9#`}isP1^s%ax(ym8=a%2a(3 zcVWhEo-Lto@V#M<$bm<5XF|xLL^N6r$rp`7p6eh%t(CsJwU>tsP{Nfg_lt|A7yZ=N z59j{B%}l)qGz@%T_iN3}U6g}<61v{RIZuhay~irf)C}xi{%tOXbwje&8%bk)Wbe!I zcMk(Dt_ySCI&4v{RTKvQ^@?bz3VrQ61-2++Cf^p>wfC49*kJ|7F`p|~mJO#=YZaL1 zZCB&#i5F5Cik`PMz>Yr^T!S{jFnP3<>_guePr+qJ%6nivaQw}!$dd7OnT&+@?o@uA z!LxH2{aI;4kS1@lIrR0~&53SK+uyz{JL)!%F=n#$rp?zI@|%^ztP*HrUP^xcB-Mf#w?f(?SxARojvH7^H6UA?4y%TCFdXyWjD>Vs)q9m3hHhJE>-g~ zlp5SyalK9OSM(uH^LXrGp6*atRr9}@qoyimy73&Kb3t&(Rp8&@+K|B00j%ESvZr^O z9?tK?UWrU8Z9*T{d3Q#@m1d`b8{~sLxobZBdK3qD;(9~huYcndm%+vh80*(a=OXiY z(U?jx$`=R-2qG|}vq!^+&pr00?v%P`Z?p0){YVq2hW?Hxq3zv?x7T`X^~-lD`WtI0`T~LUVHpFU-Bl!Px=DOk*l4n%D2yLiT zZ>K34{BTo`{}C4ekOFWbvZz{`(gJu5d7hHe2H^()hcS^8AjJ9Z<3^g?>Sa93am=R= z%v_DUFCR$vk5nZy4;6|@FAGfeB#pbb3Z^|ONV|dCW@|U5+?_?@Bj{4;@zaIvX7KBm7n9 z6Qn?1)Qj(XDyV_)#>z*#Rg=Krn#i^if62D)y%5Nv=S!2BuNoA_yJKBMom;N+x__aa zYhk?`WtkY`vcd}`?E9>3P5pLIwl1U4HXYKo%)ADqWl_cbE1U;w`{*B2N`j58ROIZJ zD>Y_%)Qjspl7XLiq7uh+m1=L&=0`qAWGadP%Pz3$v6hAYa1L-zK0Wi#2+(#{p0wO@ zyL6KsBN(lh+~9CaNHXNksK6AwnzU74{Vv?O@^*g1@CBY=;g*f3YW?D>s)22_!?Nk!F4H<*iJQ~kxr!CY5m$Xeb@EW^_sINN2o8J8)uPOqrluyOw ze&T**A0)14rT=%jS#acWf5S1T5UYTm-!b&T-X3MJiORG$&M#(~KdvvEZMC!~I6LdY zKRv{Uv8MiJb(9b2?rx$Q#Hm+v__pw+{e3r>ueEV)W_m0d-HS2zjD+&~#To*E=)>HG zyDV&OW=0oHHetdbz=K=y>ElBK-DLaX>Wu`G?Y=S^{K1Xw&CC0#*#7+Dow#g~bcbd}biEhZ#eq>o))vEu2U^bUl$74S$Jpox4JQIO zK_e> z-nmG7Nu@;?$`ERTRk?~MMiBl z-st?!FWBX^ z-`+v^NI&B5GyI$`PPvj1gF-8sx2=RUBnrIKTRMO3+GbARu2Io0CduS7cn1}NK}-n+ zS$obC*N^IArY$@1#1H}ZfS5x+$)Mg5ky`K99C#U{#EQ5!Ev)hSGjorLN1-kTt&<9x z58JP<+&mOF1{Uw0xWM|G|0@5!nl9`?`Z!6^nhbLPse>)u4{c0%irs!3{!`l`n!*bl zr57yDufPRbO$l=S&y(NMvNEj1!25H3u*@td6r9X2c@6T4<)AMCI~A16y!b8!0VoAHf~&i>_|q`zxlSJfn^a;?`bl+ zr>(5K(n)gDb-nXQASA}@Dg3G4L-N#^yx{yoSLoa9?5v1W0*;ATV+{c~z0(fiP)60; z5fBp-(|bnjI9BkKD{(T>A^OG;cW&3L(i~GsPw^;k)w#`{ecq?~<&!sT<>^TopVa1xUsap!Katj?vH-iF$HSmHGBzGYR&=Ef_K%3A!m zdmPfXI`zBu>FhnuwlkX1@pM$-`PRyd`M=SwjLGoo26AMDD?ZO?k=v?6J(Xj-cV(wl z9yEQR(s34TK%%9$J|yc6Hi(;osBM5iC)3`%8Fzj{Lmm6MWr@}kgOJVtc8*}|y}1K{ zvQOF@-gnLu)y}#K+41iSPscknB3ra+#oqO8J(KwT3d5RO?3Js``aKoJKh&XZNI!LZ zu-M@8{)rN?#Ep+VlE0r`$|7Klt=HU>9k0eOmLM7@tKSHgE~sax5%@K2z5I1<>Q>Y_ zkJzSwITD248QRL~Em0L~6m~0%Q!}|%QGM!j!sxNTh*lGh=UuV4CDAmX`!xZ?@p~%2 z#n5ssGqKh{dT5@ceNRP!Y3laNE)HTzOV@uk{g3A_?j-eE39SHk&0FaBAAxtQ8FQHS zy=xT-zFb#)hqfv>q>jr%bltzy#D}ld^TayEj%!8ho%%8{x~Kp9`p@|s(3{Hj598P> zVls!n7t@CKt4WR9(%~9Y#T(gpVfoXN_lKXds@KJ@T5$VR3^Lo(C_l`?e8>7SNfIP| zYPZ1yrVAomC0ij=yI!kWR1IuX+xo@Pr~d1bc(_Z|jFR_8doem{Mv!~L_JWY7TPJ)} zu;+BoZfuR}+T3+uT`XbKR>1Y~dRgI*+CSC9CkKDB%{Jx}eA&lEgSU7J3WM+Vtv7>= zW#0pzj}lk0mpVPHP4A15x39gtw;W-3R%_QQmwZcUvvaU|t6$F2(3;;GsX|2t@xki` zQ@4 zHBWk}gEl6+pk$3Q>l8MWcQD(_HcxB*jNLvER|(Cn%1=o5>Jk_fcsyeF%EQNDWcEgX q^=005khd+ literal 0 HcmV?d00001 diff --git a/examples/angular/src/app/app.component.html b/examples/angular/src/app/app.component.html index a057c5171..a166cf8f1 100644 --- a/examples/angular/src/app/app.component.html +++ b/examples/angular/src/app/app.component.html @@ -14,6 +14,6 @@ limitations under the License. --> -
- -
\ No newline at end of file + + + \ No newline at end of file diff --git a/examples/angular/src/app/app.component.ts b/examples/angular/src/app/app.component.ts index 4c071815b..3716f4c9d 100644 --- a/examples/angular/src/app/app.component.ts +++ b/examples/angular/src/app/app.component.ts @@ -14,43 +14,146 @@ * limitations under the License. */ -import { Component } from "@angular/core"; -import { RouterOutlet } from "@angular/router"; +import { Component, computed, inject, input } from "@angular/core"; +import { RouterModule, Router } from "@angular/router"; import { CommonModule } from "@angular/common"; -import { HeaderComponent } from "./components/header"; +import { Auth, multiFactor, sendEmailVerification, signOut, type User } from "@angular/fire/auth"; +import { routes } from "./routes"; +import { ThemeToggleComponent } from "./components/theme-toggle/theme-toggle.component"; +import { PirateToggleComponent } from "./components/pirate-toggle/pirate-toggle.component"; +import { MultiFactorAuthAssertionScreenComponent } from "@invertase/firebaseui-angular"; +import { injectUI } from "@invertase/firebaseui-angular"; @Component({ - selector: "app-root", + selector: "app-unauthenticated", + standalone: true, + imports: [CommonModule, RouterModule, MultiFactorAuthAssertionScreenComponent], + template: ` + @if (mfaResolver()) { + + } @else { +
+
+ + Firebase UI +

+ Welcome to Firebase UI, choose an example screen below to get started! +

+
+
+ @for (route of routes; track route.path) { + +
+

{{ route.name }}

+

{{ route.description }}

+
+
+ +
+
+ } +
+
+ } + `, +}) +export class UnauthenticatedAppComponent { + ui = injectUI(); + routes = routes; + + mfaResolver = computed(() => this.ui().multiFactorResolver); +} + +@Component({ + selector: "app-authenticated", standalone: true, - imports: [CommonModule, RouterOutlet, HeaderComponent], + imports: [CommonModule, RouterModule], template: ` - -
- +
+
+

+ Welcome, {{ user().displayName || user().email || user().phoneNumber }} +

+ @if (user().email) { + @if (user().emailVerified) { +
Email verified
+ } @else { + + } + } +
+

Multi-factor Authentication

+ @for (factor of mfaFactors(); track factor.factorId) { +
+ {{ factor.factorId }} - {{ factor.displayName }} +
+ } + +
+ +
`, - styles: [ - ` - .app-container { - max-width: 1200px; - margin: 0 auto; - } - - :host { - display: block; - min-height: 100vh; - background-color: #f9fafb; - font-family: - system-ui, - -apple-system, - BlinkMacSystemFont, - "Segoe UI", - Roboto, - sans-serif; - } - `, - ], }) -export class AppComponent { - title = "Firebase UI Angular Example"; +export class AuthenticatedAppComponent { + user = input.required(); + private auth = inject(Auth); + private router = inject(Router); + + mfaFactors = computed(() => { + const mfa = multiFactor(this.user()); + return mfa.enrolledFactors; + }); + + async verifyEmail() { + try { + await sendEmailVerification(this.user()); + alert("Email verification sent, please check your email"); + } catch (error) { + console.error(error); + alert("Error sending email verification, check console"); + } + } + + navigateToMfa() { + this.router.navigate(["/screens/mfa-enrollment-screen"]); + } + + async signOut() { + await signOut(this.auth); + } } + +@Component({ + selector: "app-root", + standalone: true, + imports: [ + CommonModule, + RouterModule, + ThemeToggleComponent, + PirateToggleComponent, + ], + templateUrl: "./app.component.html", +}) +export class AppComponent {} diff --git a/examples/angular/src/app/app.routes.server.ts b/examples/angular/src/app/app.routes.server.ts index a75f17913..4d2d69b8a 100644 --- a/examples/angular/src/app/app.routes.server.ts +++ b/examples/angular/src/app/app.routes.server.ts @@ -24,47 +24,63 @@ export const serverRoutes: ServerRoute[] = [ }, /** Static auth demos - good for SSG as they showcase Firebase UI components */ { - path: "sign-in", + path: "screens/sign-in-auth-screen", renderMode: RenderMode.Prerender, }, { - path: "oauth", + path: "screens/oauth-screen", renderMode: RenderMode.Prerender, }, /** Interactive auth routes - better as CSR for user interaction */ { - path: "sign-up", + path: "screens/sign-up-auth-screen", renderMode: RenderMode.Client, }, { - path: "forgot-password", + path: "screens/forgot-password-auth-screen", renderMode: RenderMode.Client, }, /** Dynamic auth routes - good for SSR as they may need server-side data */ { - path: "email-link", + path: "screens/email-link-auth-screen", renderMode: RenderMode.Server, }, { - path: "email-link-oauth", + path: "screens/email-link-auth-screen-w-oauth", renderMode: RenderMode.Server, }, { - path: "phone", + path: "screens/phone-auth-screen", renderMode: RenderMode.Server, }, { - path: "phone-oauth", + path: "screens/phone-auth-screen-w-oauth", renderMode: RenderMode.Server, }, { - path: "sign-in-oauth", + path: "screens/sign-in-auth-screen-w-oauth", renderMode: RenderMode.Server, }, { - path: "sign-up-oauth", + path: "screens/sign-up-auth-screen-w-oauth", renderMode: RenderMode.Server, }, + { + path: "screens/sign-in-auth-screen-w-handlers", + renderMode: RenderMode.Client, + }, + { + path: "screens/sign-up-auth-screen-w-handlers", + renderMode: RenderMode.Client, + }, + { + path: "screens/forgot-password-auth-screen-w-handlers", + renderMode: RenderMode.Client, + }, + { + path: "screens/mfa-enrollment-screen", + renderMode: RenderMode.Client, + }, /** All other routes will be rendered on the server (SSR) */ { path: "**", diff --git a/examples/angular/src/app/app.routes.ts b/examples/angular/src/app/app.routes.ts index 81960a6f7..0902873c8 100644 --- a/examples/angular/src/app/app.routes.ts +++ b/examples/angular/src/app/app.routes.ts @@ -15,6 +15,10 @@ */ import { type Routes } from "@angular/router"; +import { routes as routeConfigs, hiddenRoutes } from "./routes"; +import { ScreenRouteLayoutComponent } from "./components/screen-route-layout/screen-route-layout.component"; + +const allRoutes = [...routeConfigs, ...hiddenRoutes]; export const routes: Routes = [ { @@ -22,44 +26,12 @@ export const routes: Routes = [ loadComponent: () => import("./home").then((m) => m.HomeComponent), }, { - path: "email-link", - loadComponent: () => import("./auth/email-link").then((m) => m.EmailLinkComponent), - }, - { - path: "email-link-oauth", - loadComponent: () => import("./auth/email-link-oauth").then((m) => m.EmailLinkOAuthComponent), - }, - { - path: "forgot-password", - loadComponent: () => import("./auth/forgot-password").then((m) => m.ForgotPasswordComponent), - }, - { - path: "oauth", - loadComponent: () => import("./auth/oauth").then((m) => m.OAuthComponent), - }, - { - path: "phone", - loadComponent: () => import("./auth/phone").then((m) => m.PhoneComponent), - }, - { - path: "phone-oauth", - loadComponent: () => import("./auth/phone-oauth").then((m) => m.PhoneOAuthComponent), - }, - { - path: "sign-in", - loadComponent: () => import("./auth/sign-in").then((m) => m.SignInComponent), - }, - { - path: "sign-in-oauth", - loadComponent: () => import("./auth/sign-in-oauth").then((m) => m.SignInOAuthComponent), - }, - { - path: "sign-up", - loadComponent: () => import("./auth/sign-up").then((m) => m.SignUpComponent), - }, - { - path: "sign-up-oauth", - loadComponent: () => import("./auth/sign-up-oauth").then((m) => m.SignUpOAuthComponent), + path: "screens", + component: ScreenRouteLayoutComponent, + children: allRoutes.map((route) => ({ + path: route.path.replace(/^\/screens\//, ""), + loadComponent: route.loadComponent, + })), }, { path: "**", diff --git a/examples/angular/src/app/components/header/header.component.ts b/examples/angular/src/app/components/header/header.component.ts deleted file mode 100644 index 54f31be69..000000000 --- a/examples/angular/src/app/components/header/header.component.ts +++ /dev/null @@ -1,117 +0,0 @@ -/** - * 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 } from "@angular/core"; -import { CommonModule } from "@angular/common"; -import { RouterModule } from "@angular/router"; -import { Auth, type User, authState, signOut } from "@angular/fire/auth"; -import { Router } from "@angular/router"; -import { type Observable } from "rxjs"; - -@Component({ - selector: "app-header", - standalone: true, - imports: [CommonModule, RouterModule], - template: ` -
-
- -
- -
-
-
- `, - styles: [ - ` - .border-b { - border-bottom-width: 1px; - } - .border-gray-200 { - border-color: #e5e7eb; - } - .max-w-6xl { - max-width: 72rem; - } - .mx-auto { - margin-left: auto; - margin-right: auto; - } - .h-12 { - height: 3rem; - } - .px-4 { - padding-left: 1rem; - padding-right: 1rem; - } - .flex { - display: flex; - } - .items-center { - align-items: center; - } - .font-bold { - font-weight: 700; - } - .flex-grow { - flex-grow: 1; - } - .justify-end { - justify-content: flex-end; - } - .text-sm { - font-size: 0.875rem; - line-height: 1.25rem; - } - .gap-6 { - gap: 1.5rem; - } - button { - background: none; - border: none; - cursor: pointer; - font: inherit; - color: inherit; - } - a { - text-decoration: none; - color: inherit; - } - *:hover { - opacity: 0.75; - } - `, - ], -}) -export class HeaderComponent { - private auth = inject(Auth); - private router = inject(Router); - user$: Observable = authState(this.auth); - - async onSignOut() { - await signOut(this.auth); - this.router.navigate(["/auth/sign-in"]); - } -} diff --git a/examples/angular/src/app/components/pirate-toggle/pirate-toggle.component.ts b/examples/angular/src/app/components/pirate-toggle/pirate-toggle.component.ts new file mode 100644 index 000000000..8fc95d0b9 --- /dev/null +++ b/examples/angular/src/app/components/pirate-toggle/pirate-toggle.component.ts @@ -0,0 +1,52 @@ +/** + * 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, computed } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { injectUI } from "@invertase/firebaseui-angular"; +import { enUs } from "@invertase/firebaseui-translations"; +import { pirate } from "../../pirate"; + +@Component({ + selector: "app-pirate-toggle", + standalone: true, + imports: [CommonModule], + template: ` + + `, + styles: [], +}) +export class PirateToggleComponent { + private ui = injectUI(); + + isPirate = computed(() => this.ui().locale.locale === "pirate"); + + toggleLocale() { + const currentUI = this.ui(); + if (this.isPirate()) { + currentUI.setLocale(enUs); + } else { + currentUI.setLocale(pirate); + } + } +} + diff --git a/examples/angular/src/app/components/screen-route-layout/screen-route-layout.component.ts b/examples/angular/src/app/components/screen-route-layout/screen-route-layout.component.ts new file mode 100644 index 000000000..55c5ad7d0 --- /dev/null +++ b/examples/angular/src/app/components/screen-route-layout/screen-route-layout.component.ts @@ -0,0 +1,41 @@ +/** + * 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 } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { RouterModule } from "@angular/router"; + +@Component({ + selector: "app-screen-route-layout", + standalone: true, + imports: [CommonModule, RouterModule], + template: ` + + `, + styles: [], +}) +export class ScreenRouteLayoutComponent {} + diff --git a/examples/angular/src/app/components/theme-toggle/theme-toggle.component.ts b/examples/angular/src/app/components/theme-toggle/theme-toggle.component.ts new file mode 100644 index 000000000..ede44c227 --- /dev/null +++ b/examples/angular/src/app/components/theme-toggle/theme-toggle.component.ts @@ -0,0 +1,63 @@ +/** + * 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 } from "@angular/core"; +import { CommonModule } from "@angular/common"; + +@Component({ + selector: "app-theme-toggle", + standalone: true, + imports: [CommonModule], + template: ` + + `, + styles: [], +}) +export class ThemeToggleComponent { + toggleTheme() { + const htmlElement = document.documentElement; + const isDark = htmlElement.classList.contains("dark"); + htmlElement.classList.toggle("dark", !isDark); + localStorage['theme'] = htmlElement.classList.contains("dark") ? "dark" : "light"; + } +} + diff --git a/examples/angular/src/app/home/home.component.ts b/examples/angular/src/app/home/home.component.ts index addba1d04..71e2037d8 100644 --- a/examples/angular/src/app/home/home.component.ts +++ b/examples/angular/src/app/home/home.component.ts @@ -16,69 +16,24 @@ import { Component, inject } from "@angular/core"; import { CommonModule } from "@angular/common"; -import { RouterModule } from "@angular/router"; -import { Auth, type User, authState } from "@angular/fire/auth"; -import { type Observable } from "rxjs"; +import { AsyncPipe } from "@angular/common"; +import { UserService } from "../services/user.service"; +import { UnauthenticatedAppComponent } from "../app.component"; +import { AuthenticatedAppComponent } from "../app.component"; @Component({ selector: "app-home", standalone: true, - imports: [CommonModule, RouterModule], + imports: [CommonModule, AsyncPipe, UnauthenticatedAppComponent, AuthenticatedAppComponent], template: ` - + @if (user$ | async; as user) { + + } @else { + + } `, - styles: [], }) export class HomeComponent { - private auth = inject(Auth); - user$: Observable = authState(this.auth); - - signOut() { - this.auth.signOut(); - } + private userService = inject(UserService); + user$ = this.userService.getUser(); } diff --git a/examples/angular/src/app/home/index.ts b/examples/angular/src/app/home/index.ts index 099bc938e..d7102cdb5 100644 --- a/examples/angular/src/app/home/index.ts +++ b/examples/angular/src/app/home/index.ts @@ -1,17 +1 @@ -/** - * 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. - */ - export * from "./home.component"; diff --git a/examples/angular/src/app/pirate.ts b/examples/angular/src/app/pirate.ts new file mode 100644 index 000000000..f4feecae3 --- /dev/null +++ b/examples/angular/src/app/pirate.ts @@ -0,0 +1,96 @@ +import { registerLocale } from "@invertase/firebaseui-translations"; + +export const pirate = registerLocale("pirate", { + errors: { + userNotFound: "Arrr! No account found with this email address, matey", + wrongPassword: "Arrr! Incorrect password, ye scallywag", + invalidEmail: "Avast! Enter a valid email address, ye bilge rat", + userDisabled: "This account has been marooned, arrr!", + networkRequestFailed: "Can't connect to the server, ye land lubber! Check yer internet connection", + tooManyRequests: "Too many failed attempts, ye scurvy dog! Try again later", + missingVerificationCode: "Enter the verification code, ye scallywag", + emailAlreadyInUse: "An account already exists with this email, arrr!", + invalidCredential: "The credentials ye provided be invalid, matey", + weakPassword: "Ye password ain't long enough! It should be at least 8 characters", + unverifiedEmail: "Verify yer email address to continue, ye scallywag", + operationNotAllowed: "This operation ain't allowed, arrr! Contact support, matey", + invalidPhoneNumber: "The phone number be invalid, ye bilge rat", + missingPhoneNumber: "Provide a phone number, ye scallywag", + quotaExceeded: "SMS quota exceeded, arrr! Try again later, matey", + codeExpired: "The verification code has expired, ye scurvy dog", + captchaCheckFailed: "reCAPTCHA verification failed, arrr! Try again, matey", + missingVerificationId: "Complete the reCAPTCHA verification first, ye scallywag", + missingEmail: "Provide an email address, ye bilge rat", + invalidActionCode: "The password reset link be invalid or has expired, arrr!", + credentialAlreadyInUse: "An account already exists with this email, arrr! Sign in with that account, matey", + requiresRecentLogin: "This operation requires a recent login, ye scallywag! Sign in again", + providerAlreadyLinked: "This phone number be already linked to another account, arrr!", + invalidVerificationCode: "Invalid verification code, ye scurvy dog! Try again", + unknownError: "An unexpected error occurred, arrr!", + popupClosed: "The sign-in popup was closed, ye scallywag! Try again", + accountExistsWithDifferentCredential: + "An account already exists with this email, arrr! Sign in with the original provider, matey", + displayNameRequired: "Provide a display name, ye bilge rat", + secondFactorAlreadyInUse: "This phone number be already enrolled with this account, arrr!", + }, + messages: { + passwordResetEmailSent: "Password reset email sent successfully, arrr!", + signInLinkSent: "Sign-in link sent successfully, matey!", + verificationCodeFirst: "Request a verification code first, ye scallywag", + checkEmailForReset: "Check yer email for password reset instructions, ye bilge rat", + dividerOr: "or", + termsAndPrivacy: "By continuing, ye agree to our {tos} and {privacy}, arrr!", + mfaSmsAssertionPrompt: + "A verification code will be sent to {phoneNumber} to complete the authentication process, matey.", + }, + labels: { + emailAddress: "Email Address, ye bilge rat", + password: "Password, ye scallywag", + displayName: "Display Name, ye bilge rat", + forgotPassword: "Forgot Password, ye scallywag?", + signUp: "Sign Up, Matey", + signIn: "Sign In, Matey", + resetPassword: "Reset Password, ye scallywag", + createAccount: "Create Account, ye bilge rat", + backToSignIn: "Back to Sign In, ye scallywag", + signInWithPhone: "Sign in with Phone, ye scallywag", + phoneNumber: "Phone Number, ye bilge rat", + verificationCode: "Verification Code, ye scallywag", + sendCode: "Send Code, ye scallywag", + verifyCode: "Verify Code, ye scallywag", + signInWithGoogle: "Sign in with ye Google Account", + signInWithFacebook: "Sign in with ye Facebook Account", + signInWithApple: "Sign in with ye Apple Account", + signInWithMicrosoft: "Sign in with ye Microsoft Account", + signInWithGitHub: "Sign in with ye GitHub Account", + signInWithTwitter: "Sign in with ye X Account", + signInWithEmailLink: "Sign in with Email Link", + sendSignInLink: "Send Sign-in Link", + termsOfService: "Terms of Service", + privacyPolicy: "Privacy Policy", + resendCode: "Resend ye Code", + sending: "Firing...", + multiFactorEnrollment: "Multi-factor Enrrrrrrollment!", + multiFactorAssertion: "Multi-factor Authentication, arrr!", + mfaTotpVerification: "TOTP Verification, arrr!", + mfaSmsVerification: "SMS Verification, arrr!", + generateQrCode: "Generate ye QR Code", + }, + prompts: { + noAccount: "Don't have an account, ye scallywag?", + haveAccount: "Already have an account, matey?", + enterEmailToReset: "Enter yer email address to reset yer password, ye bilge rat", + signInToAccount: "Sign in to yer account, matey", + smsVerificationPrompt: "Enter the verification code sent to yer phone number, ye scallywag", + enterDetailsToCreate: "Enter yer details to create a new account, ye bilge rat", + enterPhoneNumber: "Enter yer phone number, matey", + enterVerificationCode: "Enter the verification code, ye scallywag", + enterEmailForLink: "Enter yer email to receive a sign-in link, ye bilge rat", + mfaEnrollmentPrompt: "Select a new multi-factor enrollment method, arrr!", + mfaAssertionPrompt: "Complete the multi-factor authentication process, ye scallywag", + mfaAssertionFactorPrompt: "Choose a multi-factor authentication method, matey", + mfaTotpQrCodePrompt: "Scan this QR code with yer authenticator app, ye bilge rat", + mfaTotpEnrollmentVerificationPrompt: "Add the code generated by yer authenticator app, arrr!", + }, +}); + diff --git a/examples/angular/src/app/routes.ts b/examples/angular/src/app/routes.ts new file mode 100644 index 000000000..37a989b6f --- /dev/null +++ b/examples/angular/src/app/routes.ts @@ -0,0 +1,126 @@ +/** + * 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 { Type } from "@angular/core"; + +export interface RouteConfig { + name: string; + description: string; + path: string; + loadComponent: () => Promise<{ default: Type } | Type>; +} + +export const routes: RouteConfig[] = [ + { + name: "Sign In Screen", + description: "A sign in screen with email and password.", + path: "/screens/sign-in-auth-screen", + loadComponent: () => import("./screens/sign-in-auth-screen").then((m) => m.SignInAuthScreenWrapperComponent), + }, + { + name: "Sign In Screen (with handlers)", + description: "A sign in screen with email and password, with forgot password and register handlers.", + path: "/screens/sign-in-auth-screen-w-handlers", + loadComponent: () => + import("./screens/sign-in-auth-screen-w-handlers").then((m) => m.SignInAuthScreenWithHandlersComponent), + }, + { + name: "Sign In Screen (with OAuth)", + description: "A sign in screen with email and password, with oAuth buttons.", + path: "/screens/sign-in-auth-screen-w-oauth", + loadComponent: () => + import("./screens/sign-in-auth-screen-w-oauth").then((m) => m.SignInAuthScreenWithOAuthComponent), + }, + { + name: "Sign Up Screen", + description: "A sign up screen with email and password.", + path: "/screens/sign-up-auth-screen", + loadComponent: () => import("./screens/sign-up-auth-screen").then((m) => m.SignUpAuthScreenWrapperComponent), + }, + { + name: "Sign Up Screen (with handlers)", + description: "A sign up screen with email and password, sign in handlers.", + path: "/screens/sign-up-auth-screen-w-handlers", + loadComponent: () => + import("./screens/sign-up-auth-screen-w-handlers").then((m) => m.SignUpAuthScreenWithHandlersComponent), + }, + { + name: "Sign Up Screen (with OAuth)", + description: "A sign in screen with email and password, with oAuth buttons.", + path: "/screens/sign-up-auth-screen-w-oauth", + loadComponent: () => + import("./screens/sign-up-auth-screen-w-oauth").then((m) => m.SignUpAuthScreenWithOAuthComponent), + }, + { + name: "Email Link Auth Screen", + description: "A screen allowing a user to send an email link for sign in.", + path: "/screens/email-link-auth-screen", + loadComponent: () => + import("./screens/email-link-auth-screen").then((m) => m.EmailLinkAuthScreenWrapperComponent), + }, + { + name: "Email Link Auth Screen (with OAuth)", + description: "A screen allowing a user to send an email link for sign in, with oAuth buttons.", + path: "/screens/email-link-auth-screen-w-oauth", + loadComponent: () => + import("./screens/email-link-auth-screen-w-oauth").then((m) => m.EmailLinkAuthScreenWithOAuthComponent), + }, + { + name: "Forgot Password Screen", + description: "A screen allowing a user to reset their password.", + path: "/screens/forgot-password-auth-screen", + loadComponent: () => + import("./screens/forgot-password-auth-screen").then((m) => m.ForgotPasswordAuthScreenWrapperComponent), + }, + { + name: "Forgot Password Screen (with handlers)", + description: "A screen allowing a user to reset their password, with forgot password and register handlers.", + path: "/screens/forgot-password-auth-screen-w-handlers", + loadComponent: () => + import("./screens/forgot-password-auth-screen-w-handlers").then( + (m) => m.ForgotPasswordAuthScreenWithHandlersComponent + ), + }, + { + name: "OAuth Screen", + description: "A screen which allows a user to sign in with OAuth only.", + path: "/screens/oauth-screen", + loadComponent: () => import("./screens/oauth-screen").then((m) => m.OAuthScreenWrapperComponent), + }, + { + name: "Phone Auth Screen", + description: "A screen allowing a user to sign in with a phone number.", + path: "/screens/phone-auth-screen", + loadComponent: () => import("./screens/phone-auth-screen").then((m) => m.PhoneAuthScreenWrapperComponent), + }, + { + name: "Phone Auth Screen (with OAuth)", + description: "A screen allowing a user to sign in with a phone number, with oAuth buttons.", + path: "/screens/phone-auth-screen-w-oauth", + loadComponent: () => + import("./screens/phone-auth-screen-w-oauth").then((m) => m.PhoneAuthScreenWithOAuthComponent), + }, +] as const; + +export const hiddenRoutes: RouteConfig[] = [ + { + name: "MFA Enrollment Screen", + description: "A screen allowing a user to enroll in multi-factor authentication.", + path: "/screens/mfa-enrollment-screen", + loadComponent: () => import("./screens/mfa-enrollment-screen").then((m) => m.MfaEnrollmentScreenComponent), + }, +] as const; + diff --git a/examples/angular/src/app/screens/email-link-auth-screen-w-oauth/email-link-auth-screen-w-oauth.component.ts b/examples/angular/src/app/screens/email-link-auth-screen-w-oauth/email-link-auth-screen-w-oauth.component.ts new file mode 100644 index 000000000..9e8ad65fc --- /dev/null +++ b/examples/angular/src/app/screens/email-link-auth-screen-w-oauth/email-link-auth-screen-w-oauth.component.ts @@ -0,0 +1,33 @@ +/** + * 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 } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { EmailLinkAuthScreenComponent, GoogleSignInButtonComponent } from "@invertase/firebaseui-angular"; + +@Component({ + selector: "app-email-link-auth-screen-w-oauth", + standalone: true, + imports: [CommonModule, EmailLinkAuthScreenComponent, GoogleSignInButtonComponent], + template: ` + + + + `, + styles: [], +}) +export class EmailLinkAuthScreenWithOAuthComponent {} + diff --git a/examples/angular/src/app/screens/email-link-auth-screen-w-oauth/index.ts b/examples/angular/src/app/screens/email-link-auth-screen-w-oauth/index.ts new file mode 100644 index 000000000..6a1ddb82b --- /dev/null +++ b/examples/angular/src/app/screens/email-link-auth-screen-w-oauth/index.ts @@ -0,0 +1,2 @@ +export * from "./email-link-auth-screen-w-oauth.component"; + diff --git a/examples/angular/src/app/screens/forgot-password-auth-screen-w-handlers/forgot-password-auth-screen-w-handlers.component.ts b/examples/angular/src/app/screens/forgot-password-auth-screen-w-handlers/forgot-password-auth-screen-w-handlers.component.ts new file mode 100644 index 000000000..dfc995311 --- /dev/null +++ b/examples/angular/src/app/screens/forgot-password-auth-screen-w-handlers/forgot-password-auth-screen-w-handlers.component.ts @@ -0,0 +1,50 @@ +/** + * 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 } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { Router } from "@angular/router"; +import { ForgotPasswordAuthScreenComponent } from "@invertase/firebaseui-angular"; + +@Component({ + selector: "app-forgot-password-auth-screen-w-handlers", + standalone: true, + imports: [CommonModule, ForgotPasswordAuthScreenComponent], + template: ` + + `, + styles: [], +}) +export class ForgotPasswordAuthScreenWithHandlersComponent { + private router = inject(Router); + + goToSignIn() { + this.router.navigate(["/screens/sign-in-auth-screen"]); + } + + goToForgotPassword() { + this.router.navigate(["/screens/forgot-password-auth-screen"]); + } + + goToSignUp() { + this.router.navigate(["/screens/sign-up-auth-screen"]); + } +} + diff --git a/examples/angular/src/app/screens/forgot-password-auth-screen-w-handlers/index.ts b/examples/angular/src/app/screens/forgot-password-auth-screen-w-handlers/index.ts new file mode 100644 index 000000000..9d609ec81 --- /dev/null +++ b/examples/angular/src/app/screens/forgot-password-auth-screen-w-handlers/index.ts @@ -0,0 +1,2 @@ +export * from "./forgot-password-auth-screen-w-handlers.component"; + diff --git a/examples/angular/src/app/app.component.css b/examples/angular/src/app/screens/forgot-password-auth-screen/forgot-password-auth-screen.component.ts similarity index 55% rename from examples/angular/src/app/app.component.css rename to examples/angular/src/app/screens/forgot-password-auth-screen/forgot-password-auth-screen.component.ts index 2ef08e386..dbd874033 100644 --- a/examples/angular/src/app/app.component.css +++ b/examples/angular/src/app/screens/forgot-password-auth-screen/forgot-password-auth-screen.component.ts @@ -13,3 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +import { Component } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { ForgotPasswordAuthScreenComponent } from "@invertase/firebaseui-angular"; + +@Component({ + selector: "app-forgot-password-auth-screen", + standalone: true, + imports: [CommonModule, ForgotPasswordAuthScreenComponent], + template: ` `, + styles: [], +}) +export class ForgotPasswordAuthScreenWrapperComponent {} + diff --git a/examples/angular/src/app/screens/forgot-password-auth-screen/index.ts b/examples/angular/src/app/screens/forgot-password-auth-screen/index.ts new file mode 100644 index 000000000..30d3393c6 --- /dev/null +++ b/examples/angular/src/app/screens/forgot-password-auth-screen/index.ts @@ -0,0 +1,2 @@ +export * from "./forgot-password-auth-screen.component"; + diff --git a/examples/angular/src/app/screens/mfa-enrollment-screen/index.ts b/examples/angular/src/app/screens/mfa-enrollment-screen/index.ts new file mode 100644 index 000000000..dcb9d6f17 --- /dev/null +++ b/examples/angular/src/app/screens/mfa-enrollment-screen/index.ts @@ -0,0 +1,2 @@ +export * from "./mfa-enrollment-screen.component"; + diff --git a/examples/angular/src/app/screens/mfa-enrollment-screen/mfa-enrollment-screen.component.ts b/examples/angular/src/app/screens/mfa-enrollment-screen/mfa-enrollment-screen.component.ts new file mode 100644 index 000000000..90051f6e0 --- /dev/null +++ b/examples/angular/src/app/screens/mfa-enrollment-screen/mfa-enrollment-screen.component.ts @@ -0,0 +1,43 @@ +/** + * 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 } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { Router } from "@angular/router"; +import { MultiFactorAuthEnrollmentScreenComponent } from "@invertase/firebaseui-angular"; +import { FactorId } from "firebase/auth"; + +@Component({ + selector: "app-mfa-enrollment-screen", + standalone: true, + imports: [CommonModule, MultiFactorAuthEnrollmentScreenComponent], + template: ` + + `, + styles: [], +}) +export class MfaEnrollmentScreenComponent { + FactorId = FactorId; + private router = inject(Router); + + onEnrollment() { + this.router.navigate(["/"]); + } +} + diff --git a/examples/angular/src/app/screens/phone-auth-screen-w-oauth/index.ts b/examples/angular/src/app/screens/phone-auth-screen-w-oauth/index.ts new file mode 100644 index 000000000..bee87dc66 --- /dev/null +++ b/examples/angular/src/app/screens/phone-auth-screen-w-oauth/index.ts @@ -0,0 +1,2 @@ +export * from "./phone-auth-screen-w-oauth.component"; + diff --git a/examples/angular/src/app/screens/phone-auth-screen-w-oauth/phone-auth-screen-w-oauth.component.ts b/examples/angular/src/app/screens/phone-auth-screen-w-oauth/phone-auth-screen-w-oauth.component.ts new file mode 100644 index 000000000..c78b97fd0 --- /dev/null +++ b/examples/angular/src/app/screens/phone-auth-screen-w-oauth/phone-auth-screen-w-oauth.component.ts @@ -0,0 +1,33 @@ +/** + * 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 } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { PhoneAuthScreenComponent, GoogleSignInButtonComponent } from "@invertase/firebaseui-angular"; + +@Component({ + selector: "app-phone-auth-screen-w-oauth", + standalone: true, + imports: [CommonModule, PhoneAuthScreenComponent, GoogleSignInButtonComponent], + template: ` + + + + `, + styles: [], +}) +export class PhoneAuthScreenWithOAuthComponent {} + diff --git a/examples/angular/src/app/screens/sign-in-auth-screen-w-handlers/index.ts b/examples/angular/src/app/screens/sign-in-auth-screen-w-handlers/index.ts new file mode 100644 index 000000000..787c34acd --- /dev/null +++ b/examples/angular/src/app/screens/sign-in-auth-screen-w-handlers/index.ts @@ -0,0 +1,2 @@ +export * from "./sign-in-auth-screen-w-handlers.component"; + diff --git a/examples/angular/src/app/screens/sign-in-auth-screen-w-handlers/sign-in-auth-screen-w-handlers.component.ts b/examples/angular/src/app/screens/sign-in-auth-screen-w-handlers/sign-in-auth-screen-w-handlers.component.ts new file mode 100644 index 000000000..7fb555d1a --- /dev/null +++ b/examples/angular/src/app/screens/sign-in-auth-screen-w-handlers/sign-in-auth-screen-w-handlers.component.ts @@ -0,0 +1,51 @@ +/** + * 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 } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { Router } from "@angular/router"; +import { SignInAuthScreenComponent } from "@invertase/firebaseui-angular"; + +@Component({ + selector: "app-sign-in-auth-screen-w-handlers", + standalone: true, + imports: [CommonModule, SignInAuthScreenComponent], + template: ` + + `, + styles: [], +}) +export class SignInAuthScreenWithHandlersComponent { + private router = inject(Router); + + goToForgotPassword() { + this.router.navigate(["/screens/forgot-password-auth-screen"]); + } + + goToSignUp() { + this.router.navigate(["/screens/sign-up-auth-screen"]); + } + + onSignIn(credential: unknown) { + console.log(credential); + this.router.navigate(["/"]); + } +} + diff --git a/examples/angular/src/app/screens/sign-in-auth-screen-w-oauth/index.ts b/examples/angular/src/app/screens/sign-in-auth-screen-w-oauth/index.ts new file mode 100644 index 000000000..6ce353b14 --- /dev/null +++ b/examples/angular/src/app/screens/sign-in-auth-screen-w-oauth/index.ts @@ -0,0 +1,2 @@ +export * from "./sign-in-auth-screen-w-oauth.component"; + diff --git a/examples/angular/src/app/screens/sign-in-auth-screen-w-oauth/sign-in-auth-screen-w-oauth.component.ts b/examples/angular/src/app/screens/sign-in-auth-screen-w-oauth/sign-in-auth-screen-w-oauth.component.ts new file mode 100644 index 000000000..1925ea947 --- /dev/null +++ b/examples/angular/src/app/screens/sign-in-auth-screen-w-oauth/sign-in-auth-screen-w-oauth.component.ts @@ -0,0 +1,33 @@ +/** + * 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 } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { SignInAuthScreenComponent, GoogleSignInButtonComponent } from "@invertase/firebaseui-angular"; + +@Component({ + selector: "app-sign-in-auth-screen-w-oauth", + standalone: true, + imports: [CommonModule, SignInAuthScreenComponent, GoogleSignInButtonComponent], + template: ` + + + + `, + styles: [], +}) +export class SignInAuthScreenWithOAuthComponent {} + diff --git a/examples/angular/src/app/screens/sign-up-auth-screen-w-handlers/index.ts b/examples/angular/src/app/screens/sign-up-auth-screen-w-handlers/index.ts new file mode 100644 index 000000000..54f58ebea --- /dev/null +++ b/examples/angular/src/app/screens/sign-up-auth-screen-w-handlers/index.ts @@ -0,0 +1,2 @@ +export * from "./sign-up-auth-screen-w-handlers.component"; + diff --git a/examples/angular/src/app/screens/sign-up-auth-screen-w-handlers/sign-up-auth-screen-w-handlers.component.ts b/examples/angular/src/app/screens/sign-up-auth-screen-w-handlers/sign-up-auth-screen-w-handlers.component.ts new file mode 100644 index 000000000..93772b6a3 --- /dev/null +++ b/examples/angular/src/app/screens/sign-up-auth-screen-w-handlers/sign-up-auth-screen-w-handlers.component.ts @@ -0,0 +1,43 @@ +/** + * 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 } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { Router } from "@angular/router"; +import { SignUpAuthScreenComponent } from "@invertase/firebaseui-angular"; + +@Component({ + selector: "app-sign-up-auth-screen-w-handlers", + standalone: true, + imports: [CommonModule, SignUpAuthScreenComponent], + template: ` + + `, + styles: [], +}) +export class SignUpAuthScreenWithHandlersComponent { + private router = inject(Router); + + goToSignIn() { + this.router.navigate(["/screens/sign-in-auth-screen"]); + } + + onSignUp(credential: unknown) { + console.log(credential); + this.router.navigate(["/"]); + } +} + diff --git a/examples/angular/src/app/screens/sign-up-auth-screen-w-oauth/index.ts b/examples/angular/src/app/screens/sign-up-auth-screen-w-oauth/index.ts new file mode 100644 index 000000000..3711b7302 --- /dev/null +++ b/examples/angular/src/app/screens/sign-up-auth-screen-w-oauth/index.ts @@ -0,0 +1,2 @@ +export * from "./sign-up-auth-screen-w-oauth.component"; + diff --git a/examples/angular/src/app/screens/sign-up-auth-screen-w-oauth/sign-up-auth-screen-w-oauth.component.ts b/examples/angular/src/app/screens/sign-up-auth-screen-w-oauth/sign-up-auth-screen-w-oauth.component.ts new file mode 100644 index 000000000..3bc5a549a --- /dev/null +++ b/examples/angular/src/app/screens/sign-up-auth-screen-w-oauth/sign-up-auth-screen-w-oauth.component.ts @@ -0,0 +1,33 @@ +/** + * 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 } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { SignUpAuthScreenComponent, GoogleSignInButtonComponent } from "@invertase/firebaseui-angular"; + +@Component({ + selector: "app-sign-up-auth-screen-w-oauth", + standalone: true, + imports: [CommonModule, SignUpAuthScreenComponent, GoogleSignInButtonComponent], + template: ` + + + + `, + styles: [], +}) +export class SignUpAuthScreenWithOAuthComponent {} + diff --git a/examples/angular/src/app/components/header/index.ts b/examples/angular/src/app/services/user.service.ts similarity index 64% rename from examples/angular/src/app/components/header/index.ts rename to examples/angular/src/app/services/user.service.ts index 7df717022..5c8e639b3 100644 --- a/examples/angular/src/app/components/header/index.ts +++ b/examples/angular/src/app/services/user.service.ts @@ -14,4 +14,18 @@ * limitations under the License. */ -export * from "./header.component"; +import { Injectable, inject } from "@angular/core"; +import { Auth, type User, authState } from "@angular/fire/auth"; +import { Observable } from "rxjs"; + +@Injectable({ + providedIn: "root", +}) +export class UserService { + private auth = inject(Auth); + + getUser(): Observable { + return authState(this.auth); + } +} + diff --git a/examples/angular/src/index.html b/examples/angular/src/index.html index 781564eb6..e53fbd415 100644 --- a/examples/angular/src/index.html +++ b/examples/angular/src/index.html @@ -18,12 +18,15 @@ - AngularSsr + Firebase UI for Angular - + + diff --git a/examples/angular/src/styles.css b/examples/angular/src/styles.css index 5e23eb6d6..c0f242db4 100644 --- a/examples/angular/src/styles.css +++ b/examples/angular/src/styles.css @@ -16,4 +16,5 @@ /* You can add global styles to this file, and also import other style files */ @import "tailwindcss"; +@custom-variant dark (&:where(.dark, .dark *)); @import "@invertase/firebaseui-styles/tailwind"; diff --git a/packages/angular/package.json b/packages/angular/package.json index 9d95db2ab..770160ba2 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -5,13 +5,13 @@ "dist" ], "type": "module", - "main": "./dist/fesm2022/firebase-ui-angular.mjs", - "module": "./dist/fesm2022/firebase-ui-angular.mjs", + "main": "./dist/fesm2022/invertase-firebaseui-angular.mjs", + "module": "./dist/fesm2022/invertase-firebaseui-angular.mjs", "typings": "./dist/index.d.ts", "exports": { ".": { "types": "./dist/index.d.ts", - "default": "./dist/fesm2022/firebase-ui-angular.mjs" + "default": "./dist/fesm2022/invertase-firebaseui-angular.mjs" } }, "scripts": { diff --git a/packages/angular/src/lib/components/card.spec.ts b/packages/angular/src/lib/components/card.spec.ts index bd2eac6a5..7096581fa 100644 --- a/packages/angular/src/lib/components/card.spec.ts +++ b/packages/angular/src/lib/components/card.spec.ts @@ -30,10 +30,9 @@ describe("", () => { imports: [CardComponent, CardContentComponent], }); const card = screen.getByTestId("test-card"); - const cardDiv = card.querySelector(".fui-card"); - expect(cardDiv).toHaveClass("fui-card"); - expect(cardDiv).toHaveTextContent("Card content"); + expect(card).toHaveClass("fui-card"); + expect(card).toHaveTextContent("Card content"); }); it("applies custom class", async () => { @@ -42,9 +41,8 @@ describe("", () => { { imports: [CardComponent, CardContentComponent] } ); const card = screen.getByTestId("test-card"); - const cardDiv = card.querySelector(".fui-card"); - expect(cardDiv).toHaveClass("fui-card"); + expect(card).toHaveClass("fui-card"); expect(card).toHaveClass("custom-class"); }); @@ -54,9 +52,8 @@ describe("", () => { { imports: [CardComponent, CardContentComponent] } ); const card = screen.getByTestId("test-card"); - const cardDiv = card.querySelector(".fui-card"); - expect(cardDiv).toHaveClass("fui-card"); + expect(card).toHaveClass("fui-card"); expect(card).toHaveAttribute("aria-label", "card"); }); @@ -65,8 +62,8 @@ describe("", () => { ` - Card Title - Card Subtitle + Card Title + Card Subtitle
Card Body Content
@@ -80,14 +77,16 @@ describe("", () => { const card = screen.getByTestId("complete-card"); const header = screen.getByTestId("complete-header"); + const titleHost = screen.getByTestId("complete-title"); + const subtitleHost = screen.getByTestId("complete-subtitle"); const title = screen.getByRole("heading", { name: "Card Title" }); const subtitle = screen.getByText("Card Subtitle"); const content = screen.getByText("Card Body Content"); - expect(card.querySelector(".fui-card")).toHaveClass("fui-card"); - expect(title).toHaveClass("fui-card__title"); - expect(subtitle).toHaveClass("fui-card__subtitle"); - expect(header.querySelector(".fui-card__header")).toHaveClass("fui-card__header"); + expect(card).toHaveClass("fui-card"); + expect(titleHost).toHaveClass("fui-card__title"); + expect(subtitleHost).toHaveClass("fui-card__subtitle"); + expect(header).toHaveClass("fui-card__header"); expect(content).toBeTruthy(); expect(header).toContainElement(title); @@ -103,10 +102,9 @@ describe("", () => { { imports: [CardHeaderComponent, CardTitleComponent] } ); const header = screen.getByTestId("test-header"); - const headerDiv = header.querySelector(".fui-card__header"); - expect(headerDiv).toHaveClass("fui-card__header"); - expect(headerDiv).toHaveTextContent("Header content"); + expect(header).toHaveClass("fui-card__header"); + expect(header).toHaveTextContent("Header content"); }); it("applies custom className", async () => { @@ -115,19 +113,19 @@ describe("", () => { { imports: [CardHeaderComponent, CardTitleComponent] } ); const header = screen.getByTestId("test-header"); - const headerDiv = header.querySelector(".fui-card__header"); - expect(headerDiv).toHaveClass("fui-card__header"); + expect(header).toHaveClass("fui-card__header"); expect(header).toHaveClass("custom-header"); }); }); describe("", () => { it("renders a card title with children", async () => { - await render(`Title content`, { imports: [CardTitleComponent] }); + await render(`Title content`, { imports: [CardTitleComponent] }); + const titleHost = screen.getByTestId("title-host"); const title = screen.getByRole("heading", { name: "Title content" }); - expect(title).toHaveClass("fui-card__title"); + expect(titleHost).toHaveClass("fui-card__title"); expect(title.tagName).toBe("H2"); }); @@ -135,20 +133,20 @@ describe("", () => { await render(`Title content`, { imports: [CardTitleComponent], }); - const title = screen.getByRole("heading", { name: "Title content" }); const titleHost = screen.getByTestId("title-host"); - expect(title).toHaveClass("fui-card__title"); + expect(titleHost).toHaveClass("fui-card__title"); expect(titleHost).toHaveClass("custom-title"); }); }); describe("", () => { it("renders a card subtitle with children", async () => { - await render(`Subtitle content`, { imports: [CardSubtitleComponent] }); + await render(`Subtitle content`, { imports: [CardSubtitleComponent] }); + const subtitleHost = screen.getByTestId("subtitle-host"); const subtitle = screen.getByText("Subtitle content"); - expect(subtitle).toHaveClass("fui-card__subtitle"); + expect(subtitleHost).toHaveClass("fui-card__subtitle"); expect(subtitle.tagName).toBe("P"); }); @@ -157,31 +155,28 @@ describe("", () => { `Subtitle content`, { imports: [CardSubtitleComponent] } ); - const subtitle = screen.getByText("Subtitle content"); const subtitleHost = screen.getByTestId("subtitle-host"); - expect(subtitle).toHaveClass("fui-card__subtitle"); + expect(subtitleHost).toHaveClass("fui-card__subtitle"); expect(subtitleHost).toHaveClass("custom-subtitle"); }); }); describe("", () => { it("renders a card content with children", async () => { - await render(`Content content`, { imports: [CardContentComponent] }); - const content = screen.getByText("Content content"); + await render(`Content content`, { imports: [CardContentComponent] }); + const content = screen.getByTestId("test-content"); expect(content).toHaveClass("fui-card__content"); - expect(content.tagName).toBe("DIV"); }); it("applies custom className", async () => { await render(`Content`, { imports: [CardContentComponent], }); - const content = screen.getByText("Content"); const contentHost = screen.getByTestId("content-host"); - expect(content).toHaveClass("fui-card__content"); + expect(contentHost).toHaveClass("fui-card__content"); expect(contentHost).toHaveClass("custom-content"); }); }); diff --git a/packages/angular/src/lib/components/card.ts b/packages/angular/src/lib/components/card.ts index 0e5cf2933..e5221ad3c 100644 --- a/packages/angular/src/lib/components/card.ts +++ b/packages/angular/src/lib/components/card.ts @@ -21,11 +21,13 @@ import { CommonModule } from "@angular/common"; selector: "fui-card", standalone: true, imports: [], + host: { + class: "fui-card", + style: "display: block;", + }, template: ` -
- - -
+ + `, }) export class CardComponent {} @@ -34,11 +36,13 @@ export class CardComponent {} selector: "fui-card-header", standalone: true, imports: [CommonModule], + host: { + class: "fui-card__header", + style: "display: block;", + }, template: ` -
- - -
+ + `, }) export class CardHeaderComponent {} @@ -47,8 +51,12 @@ export class CardHeaderComponent {} selector: "fui-card-title", standalone: true, imports: [CommonModule], + host: { + class: "fui-card__title", + style: "display: block;", + }, template: ` -

+

`, @@ -59,8 +67,12 @@ export class CardTitleComponent {} selector: "fui-card-subtitle", standalone: true, imports: [CommonModule], + host: { + class: "fui-card__subtitle", + style: "display: block;", + }, template: ` -

+

`, @@ -71,10 +83,12 @@ export class CardSubtitleComponent {} selector: "fui-card-content", standalone: true, imports: [CommonModule], + host: { + class: "fui-card__content", + style: "display: block;", + }, template: ` -
- -
+ `, }) export class CardContentComponent {} diff --git a/packages/angular/src/lib/components/policies.spec.ts b/packages/angular/src/lib/components/policies.spec.ts index 6a68df1be..bcdfb48ef 100644 --- a/packages/angular/src/lib/components/policies.spec.ts +++ b/packages/angular/src/lib/components/policies.spec.ts @@ -165,6 +165,7 @@ describe("", () => { const policiesContainer = container.querySelector(".fui-policies"); expect(policiesContainer).toBeTruthy(); + expect(policiesContainer).toHaveClass("fui-policies"); const tosLink = container.querySelector('a[href="https://example.com/terms"]'); expect(tosLink).toBeTruthy(); @@ -191,7 +192,12 @@ describe("", () => { const { container } = await render(TestPoliciesWithNoUrlsHostComponent); const policiesContainer = container.querySelector(".fui-policies"); - expect(policiesContainer).toBeFalsy(); + // Host element is always rendered, but should be hidden and have no content + expect(policiesContainer).toBeTruthy(); + expect(policiesContainer).toHaveClass("fui-policies"); + expect(policiesContainer).toHaveStyle({ display: "none" }); + expect(policiesContainer?.textContent?.trim()).toBe(""); + expect(policiesContainer?.querySelectorAll("a").length).toBe(0); }); it("renders with tosUrl when privacyPolicyUrl is not provided", async () => { @@ -205,6 +211,7 @@ describe("", () => { const policiesContainer = container.querySelector(".fui-policies"); expect(policiesContainer).toBeTruthy(); + expect(policiesContainer).toHaveClass("fui-policies"); const tosLink = container.querySelector('a[href="https://example.com/terms"]'); expect(tosLink).toBeTruthy(); @@ -225,6 +232,7 @@ describe("", () => { const policiesContainer = container.querySelector(".fui-policies"); expect(policiesContainer).toBeTruthy(); + expect(policiesContainer).toHaveClass("fui-policies"); const tosLink = container.querySelector('a[href="https://example.com/terms"]'); expect(tosLink).toBeFalsy(); @@ -253,6 +261,7 @@ describe("", () => { const policiesContainer = container.querySelector(".fui-policies"); expect(policiesContainer).toBeTruthy(); + expect(policiesContainer).toHaveClass("fui-policies"); const textContent = policiesContainer?.textContent; expect(textContent).toContain("Custom template with"); diff --git a/packages/angular/src/lib/components/policies.ts b/packages/angular/src/lib/components/policies.ts index feaef45f6..b8675b7e1 100644 --- a/packages/angular/src/lib/components/policies.ts +++ b/packages/angular/src/lib/components/policies.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Component, computed, Signal } from "@angular/core"; +import { Component, computed, HostBinding, Signal } from "@angular/core"; import { CommonModule } from "@angular/common"; import { injectPolicies, injectTranslation } from "../provider"; @@ -25,25 +25,26 @@ type PolicyPart = @Component({ selector: "fui-policies", + host: { + class: "fui-policies", + }, standalone: true, imports: [CommonModule], template: ` @if (shouldShow()) { -
- @for (part of policyParts(); track $index) { - @if (part.type === "tos") { - - {{ part.text }} - - } @else if (part.type === "privacy") { - - {{ part.text }} - - } @else { - {{ part.content }} - } + @for (part of policyParts(); track $index) { + @if (part.type === "tos") { + + {{ part.text }} + + } @else if (part.type === "privacy") { + + {{ part.text }} + + } @else { + {{ part.content }} } -
+ } } `, }) @@ -59,6 +60,11 @@ export class PoliciesComponent { readonly shouldShow = computed(() => this.policies !== null); + @HostBinding("style.display") + get displayStyle(): string { + return this.shouldShow() ? "block" : "none"; + } + readonly policyParts: Signal = computed(() => { if (!this.shouldShow()) { return []; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 633ef4be1..ac69d9050 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -180,17 +180,17 @@ importers: specifier: ^20.2.2 version: 20.3.7(64ca8375dbaf48ae24b53908d91cad2b) '@invertase/firebaseui-angular': - specifier: 0.0.3 - version: 0.0.3(@angular/common@20.3.7(@angular/core@20.3.7(@angular/compiler@20.3.7)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.7(@angular/compiler@20.3.7)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/fire@20.0.1(567573b864ff578578a14734fda777a6))(firebase@11.10.0)(typescript@5.9.3) + specifier: workspace:* + version: link:../../packages/angular '@invertase/firebaseui-core': - specifier: 0.0.3 - version: 0.0.3(firebase@11.10.0) + specifier: workspace:* + version: link:../../packages/core '@invertase/firebaseui-styles': - specifier: 0.0.7 - version: 0.0.7(typescript@5.9.3) + specifier: workspace:* + version: link:../../packages/styles '@invertase/firebaseui-translations': - specifier: 0.0.4 - version: 0.0.4 + specifier: workspace:* + version: link:../../packages/translations '@tailwindcss/postcss': specifier: ^4.0.6 version: 4.1.16 @@ -2929,21 +2929,11 @@ packages: '@types/node': optional: true - '@invertase/firebaseui-angular@0.0.3': - resolution: {integrity: sha512-ppFfGQl4hw3oh4Nb8irmR5bitNp5VKvmwdoxYFVpRA6WKvmFhgpDnownBA+wrrF0w1fLud2NrzLperEbGf9pJw==} - peerDependencies: - '@angular/fire': ^20 - '@invertase/firebaseui-core@0.0.10': resolution: {integrity: sha512-Qu6BFSikBJhWkBUy5uYEX61mdxiPH/1pI6i/g5iYnAIDCg8llo9h0QfnFozuFwtjjTyIWThEvk3e/Y7L/oYA2A==} peerDependencies: firebase: ^11 || ^12 - '@invertase/firebaseui-core@0.0.3': - resolution: {integrity: sha512-3omygaTrAKV8KaN/TWNzzdwvQHAGHZGZzzm7nFo2FVVSZp6Ri6poUXUmseGC5ionRW1Cnx7h8dXLbtsH+XG40g==} - peerDependencies: - firebase: ^11 || ^12 - '@invertase/firebaseui-react@0.0.9': resolution: {integrity: sha512-ydB4itzvbMK3jYIMzlN+UJN9fY1OniErEz+I21701wqQDd9BKCEteajaj7oRn/IsLGmflrVdNGW2W/sY2MJUDQ==} peerDependencies: @@ -2954,15 +2944,6 @@ packages: '@invertase/firebaseui-styles@0.0.12': resolution: {integrity: sha512-uRzI+GNiOBw5bqoI9frRqDTy6iyC+M7pBe6uzrOAXnNJFXWJnZ9oCIjzGW5pQEoyG2bBjQ616KRdEYfZpxVEwQ==} - '@invertase/firebaseui-styles@0.0.3': - resolution: {integrity: sha512-0obT/2OZfoayTE2GTw3botbOKYCHVmeozjvb4xNs1NCd5MZyc9nmN8y1PkgZ7vTsTIi6LBmqteFXk5yCIxp4MA==} - - '@invertase/firebaseui-styles@0.0.7': - resolution: {integrity: sha512-NbtwZAnIjjqQZt4KxXyhfW28/ABNZ7KoldgPgQ0mOIE35JXbCmTBIrIoyIMkwYtH2KTeHMMLnsPLY0b8MVCmRQ==} - - '@invertase/firebaseui-translations@0.0.4': - resolution: {integrity: sha512-sG8DToRfe7phgF5PXL02hZ9fwQcnwLdJvkfVaz4HuCBne94EH0dBgHhrF0oElRxLae93S2endufa94Yy7IoKsA==} - '@invertase/firebaseui-translations@0.0.7': resolution: {integrity: sha512-r1miz4ur82U1aw+l264OO9oLXjXb9lF208uZ+eABsFxaChgUg3TbnDeXPMVHf/eM34vx5yT8lrMDNS0qQJjg+g==} @@ -12408,20 +12389,6 @@ snapshots: optionalDependencies: '@types/node': 24.9.2 - '@invertase/firebaseui-angular@0.0.3(@angular/common@20.3.7(@angular/core@20.3.7(@angular/compiler@20.3.7)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.7(@angular/compiler@20.3.7)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/fire@20.0.1(567573b864ff578578a14734fda777a6))(firebase@11.10.0)(typescript@5.9.3)': - dependencies: - '@angular/fire': 20.0.1(567573b864ff578578a14734fda777a6) - '@invertase/firebaseui-core': 0.0.3(firebase@11.10.0) - '@invertase/firebaseui-styles': 0.0.3(typescript@5.9.3) - '@tanstack/angular-form': 1.23.8(@angular/common@20.3.7(@angular/core@20.3.7(@angular/compiler@20.3.7)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.7(@angular/compiler@20.3.7)(rxjs@7.8.2)(zone.js@0.15.1)) - nanostores: 1.0.1 - tslib: 2.8.1 - transitivePeerDependencies: - - '@angular/common' - - '@angular/core' - - firebase - - typescript - '@invertase/firebaseui-core@0.0.10(firebase@11.10.0)': dependencies: '@invertase/firebaseui-translations': 0.0.7 @@ -12431,15 +12398,6 @@ snapshots: qrcode-generator: 2.0.4 zod: 4.1.12 - '@invertase/firebaseui-core@0.0.3(firebase@11.10.0)': - dependencies: - '@invertase/firebaseui-translations': 0.0.4 - firebase: 11.10.0 - libphonenumber-js: 1.12.25 - nanostores: 1.0.1 - qrcode-generator: 2.0.4 - zod: 4.1.12 - '@invertase/firebaseui-react@0.0.9(@types/react@19.1.16)(firebase@11.10.0)(nanostores@1.0.1)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.9.3)': dependencies: '@invertase/firebaseui-core': 0.0.10(firebase@11.10.0) @@ -12465,20 +12423,6 @@ snapshots: transitivePeerDependencies: - typescript - '@invertase/firebaseui-styles@0.0.3(typescript@5.9.3)': - dependencies: - cva: 1.0.0-beta.4(typescript@5.9.3) - transitivePeerDependencies: - - typescript - - '@invertase/firebaseui-styles@0.0.7(typescript@5.9.3)': - dependencies: - cva: 1.0.0-beta.4(typescript@5.9.3) - transitivePeerDependencies: - - typescript - - '@invertase/firebaseui-translations@0.0.4': {} - '@invertase/firebaseui-translations@0.0.7': {} '@isaacs/balanced-match@4.0.1': {} From 02ceaa4effec764caf795bd200cac7b8c61ef7f0 Mon Sep 17 00:00:00 2001 From: Elliot Hesp Date: Sat, 8 Nov 2025 19:12:27 +0000 Subject: [PATCH 25/32] fix(angular): Forms use input event emitter to handle conditional rendering --- examples/angular/src/app/app.component.ts | 35 ++---- .../pirate-toggle/pirate-toggle.component.ts | 1 - .../screen-route-layout.component.ts | 1 - .../theme-toggle/theme-toggle.component.ts | 3 +- examples/angular/src/app/pirate.ts | 1 - examples/angular/src/app/routes.ts | 7 +- ...mail-link-auth-screen-w-oauth.component.ts | 1 - .../email-link-auth-screen-w-oauth/index.ts | 1 - ...ssword-auth-screen-w-handlers.component.ts | 1 - .../index.ts | 1 - .../forgot-password-auth-screen.component.ts | 1 - .../forgot-password-auth-screen/index.ts | 1 - .../screens/mfa-enrollment-screen/index.ts | 1 - .../mfa-enrollment-screen.component.ts | 1 - .../phone-auth-screen-w-oauth/index.ts | 1 - .../phone-auth-screen-w-oauth.component.ts | 1 - .../sign-in-auth-screen-w-handlers/index.ts | 1 - ...ign-in-auth-screen-w-handlers.component.ts | 1 - .../sign-in-auth-screen-w-oauth/index.ts | 1 - .../sign-in-auth-screen-w-oauth.component.ts | 1 - .../sign-up-auth-screen-w-handlers/index.ts | 1 - ...ign-up-auth-screen-w-handlers.component.ts | 5 +- .../sign-up-auth-screen-w-oauth/index.ts | 1 - .../sign-up-auth-screen-w-oauth.component.ts | 1 - .../angular/src/app/services/user.service.ts | 1 - .../lib/auth/forms/email-link-auth-form.ts | 10 +- .../forms/forgot-password-auth-form.spec.ts | 23 +++- .../auth/forms/forgot-password-auth-form.ts | 13 ++- .../mfa/sms-multi-factor-assertion-form.ts | 8 +- .../mfa/sms-multi-factor-enrollment-form.ts | 4 +- .../mfa/totp-multi-factor-assertion-form.ts | 4 +- .../mfa/totp-multi-factor-enrollment-form.ts | 8 +- .../forms/multi-factor-auth-assertion-form.ts | 4 +- .../multi-factor-auth-enrollment-form.ts | 4 +- .../src/lib/auth/forms/phone-auth-form.ts | 8 +- .../lib/auth/forms/sign-in-auth-form.spec.ts | 103 +++++++++++++++++- .../src/lib/auth/forms/sign-in-auth-form.ts | 17 +-- .../lib/auth/forms/sign-up-auth-form.spec.ts | 20 +++- .../src/lib/auth/forms/sign-up-auth-form.ts | 11 +- .../auth/screens/email-link-auth-screen.ts | 6 +- .../screens/forgot-password-auth-screen.ts | 8 +- .../multi-factor-auth-assertion-screen.ts | 4 +- .../multi-factor-auth-enrollment-screen.ts | 4 +- .../src/lib/auth/screens/oauth-screen.ts | 4 +- .../src/lib/auth/screens/phone-auth-screen.ts | 4 +- .../lib/auth/screens/sign-in-auth-screen.ts | 14 +-- .../lib/auth/screens/sign-up-auth-screen.ts | 8 +- .../angular/src/lib/components/card.spec.ts | 12 +- packages/angular/src/lib/components/card.ts | 4 +- 49 files changed, 227 insertions(+), 149 deletions(-) diff --git a/examples/angular/src/app/app.component.ts b/examples/angular/src/app/app.component.ts index 3716f4c9d..acd98aab5 100644 --- a/examples/angular/src/app/app.component.ts +++ b/examples/angular/src/app/app.component.ts @@ -34,11 +34,7 @@ import { injectUI } from "@invertase/firebaseui-angular"; } @else {
- + Firebase UI

Welcome to Firebase UI, choose an example screen below to get started! @@ -80,17 +76,12 @@ export class UnauthenticatedAppComponent { template: `

-

- Welcome, {{ user().displayName || user().email || user().phoneNumber }} -

+

Welcome, {{ user().displayName || user().email || user().phoneNumber }}

@if (user().email) { @if (user().emailVerified) {
Email verified
} @else { - } @@ -98,20 +89,13 @@ export class UnauthenticatedAppComponent {

Multi-factor Authentication

@for (factor of mfaFactors(); track factor.factorId) { -
- {{ factor.factorId }} - {{ factor.displayName }} -
+
{{ factor.factorId }} - {{ factor.displayName }}
} -
- +
`, @@ -148,12 +132,7 @@ export class AuthenticatedAppComponent { @Component({ selector: "app-root", standalone: true, - imports: [ - CommonModule, - RouterModule, - ThemeToggleComponent, - PirateToggleComponent, - ], + imports: [CommonModule, RouterModule, ThemeToggleComponent, PirateToggleComponent], templateUrl: "./app.component.html", }) export class AppComponent {} diff --git a/examples/angular/src/app/components/pirate-toggle/pirate-toggle.component.ts b/examples/angular/src/app/components/pirate-toggle/pirate-toggle.component.ts index 8fc95d0b9..77401b370 100644 --- a/examples/angular/src/app/components/pirate-toggle/pirate-toggle.component.ts +++ b/examples/angular/src/app/components/pirate-toggle/pirate-toggle.component.ts @@ -49,4 +49,3 @@ export class PirateToggleComponent { } } } - diff --git a/examples/angular/src/app/components/screen-route-layout/screen-route-layout.component.ts b/examples/angular/src/app/components/screen-route-layout/screen-route-layout.component.ts index 55c5ad7d0..ec6b361b1 100644 --- a/examples/angular/src/app/components/screen-route-layout/screen-route-layout.component.ts +++ b/examples/angular/src/app/components/screen-route-layout/screen-route-layout.component.ts @@ -38,4 +38,3 @@ import { RouterModule } from "@angular/router"; styles: [], }) export class ScreenRouteLayoutComponent {} - diff --git a/examples/angular/src/app/components/theme-toggle/theme-toggle.component.ts b/examples/angular/src/app/components/theme-toggle/theme-toggle.component.ts index ede44c227..dbfe6ed8e 100644 --- a/examples/angular/src/app/components/theme-toggle/theme-toggle.component.ts +++ b/examples/angular/src/app/components/theme-toggle/theme-toggle.component.ts @@ -57,7 +57,6 @@ export class ThemeToggleComponent { const htmlElement = document.documentElement; const isDark = htmlElement.classList.contains("dark"); htmlElement.classList.toggle("dark", !isDark); - localStorage['theme'] = htmlElement.classList.contains("dark") ? "dark" : "light"; + localStorage["theme"] = htmlElement.classList.contains("dark") ? "dark" : "light"; } } - diff --git a/examples/angular/src/app/pirate.ts b/examples/angular/src/app/pirate.ts index f4feecae3..aa92433ce 100644 --- a/examples/angular/src/app/pirate.ts +++ b/examples/angular/src/app/pirate.ts @@ -93,4 +93,3 @@ export const pirate = registerLocale("pirate", { mfaTotpEnrollmentVerificationPrompt: "Add the code generated by yer authenticator app, arrr!", }, }); - diff --git a/examples/angular/src/app/routes.ts b/examples/angular/src/app/routes.ts index 37a989b6f..fcafffd4a 100644 --- a/examples/angular/src/app/routes.ts +++ b/examples/angular/src/app/routes.ts @@ -68,8 +68,7 @@ export const routes: RouteConfig[] = [ name: "Email Link Auth Screen", description: "A screen allowing a user to send an email link for sign in.", path: "/screens/email-link-auth-screen", - loadComponent: () => - import("./screens/email-link-auth-screen").then((m) => m.EmailLinkAuthScreenWrapperComponent), + loadComponent: () => import("./screens/email-link-auth-screen").then((m) => m.EmailLinkAuthScreenWrapperComponent), }, { name: "Email Link Auth Screen (with OAuth)", @@ -110,8 +109,7 @@ export const routes: RouteConfig[] = [ name: "Phone Auth Screen (with OAuth)", description: "A screen allowing a user to sign in with a phone number, with oAuth buttons.", path: "/screens/phone-auth-screen-w-oauth", - loadComponent: () => - import("./screens/phone-auth-screen-w-oauth").then((m) => m.PhoneAuthScreenWithOAuthComponent), + loadComponent: () => import("./screens/phone-auth-screen-w-oauth").then((m) => m.PhoneAuthScreenWithOAuthComponent), }, ] as const; @@ -123,4 +121,3 @@ export const hiddenRoutes: RouteConfig[] = [ loadComponent: () => import("./screens/mfa-enrollment-screen").then((m) => m.MfaEnrollmentScreenComponent), }, ] as const; - diff --git a/examples/angular/src/app/screens/email-link-auth-screen-w-oauth/email-link-auth-screen-w-oauth.component.ts b/examples/angular/src/app/screens/email-link-auth-screen-w-oauth/email-link-auth-screen-w-oauth.component.ts index 9e8ad65fc..a0921dedd 100644 --- a/examples/angular/src/app/screens/email-link-auth-screen-w-oauth/email-link-auth-screen-w-oauth.component.ts +++ b/examples/angular/src/app/screens/email-link-auth-screen-w-oauth/email-link-auth-screen-w-oauth.component.ts @@ -30,4 +30,3 @@ import { EmailLinkAuthScreenComponent, GoogleSignInButtonComponent } from "@inve styles: [], }) export class EmailLinkAuthScreenWithOAuthComponent {} - diff --git a/examples/angular/src/app/screens/email-link-auth-screen-w-oauth/index.ts b/examples/angular/src/app/screens/email-link-auth-screen-w-oauth/index.ts index 6a1ddb82b..13f2e186d 100644 --- a/examples/angular/src/app/screens/email-link-auth-screen-w-oauth/index.ts +++ b/examples/angular/src/app/screens/email-link-auth-screen-w-oauth/index.ts @@ -1,2 +1 @@ export * from "./email-link-auth-screen-w-oauth.component"; - diff --git a/examples/angular/src/app/screens/forgot-password-auth-screen-w-handlers/forgot-password-auth-screen-w-handlers.component.ts b/examples/angular/src/app/screens/forgot-password-auth-screen-w-handlers/forgot-password-auth-screen-w-handlers.component.ts index dfc995311..e8c09a3ca 100644 --- a/examples/angular/src/app/screens/forgot-password-auth-screen-w-handlers/forgot-password-auth-screen-w-handlers.component.ts +++ b/examples/angular/src/app/screens/forgot-password-auth-screen-w-handlers/forgot-password-auth-screen-w-handlers.component.ts @@ -47,4 +47,3 @@ export class ForgotPasswordAuthScreenWithHandlersComponent { this.router.navigate(["/screens/sign-up-auth-screen"]); } } - diff --git a/examples/angular/src/app/screens/forgot-password-auth-screen-w-handlers/index.ts b/examples/angular/src/app/screens/forgot-password-auth-screen-w-handlers/index.ts index 9d609ec81..227203450 100644 --- a/examples/angular/src/app/screens/forgot-password-auth-screen-w-handlers/index.ts +++ b/examples/angular/src/app/screens/forgot-password-auth-screen-w-handlers/index.ts @@ -1,2 +1 @@ export * from "./forgot-password-auth-screen-w-handlers.component"; - diff --git a/examples/angular/src/app/screens/forgot-password-auth-screen/forgot-password-auth-screen.component.ts b/examples/angular/src/app/screens/forgot-password-auth-screen/forgot-password-auth-screen.component.ts index dbd874033..6f26e4ec1 100644 --- a/examples/angular/src/app/screens/forgot-password-auth-screen/forgot-password-auth-screen.component.ts +++ b/examples/angular/src/app/screens/forgot-password-auth-screen/forgot-password-auth-screen.component.ts @@ -26,4 +26,3 @@ import { ForgotPasswordAuthScreenComponent } from "@invertase/firebaseui-angular styles: [], }) export class ForgotPasswordAuthScreenWrapperComponent {} - diff --git a/examples/angular/src/app/screens/forgot-password-auth-screen/index.ts b/examples/angular/src/app/screens/forgot-password-auth-screen/index.ts index 30d3393c6..6cc32654d 100644 --- a/examples/angular/src/app/screens/forgot-password-auth-screen/index.ts +++ b/examples/angular/src/app/screens/forgot-password-auth-screen/index.ts @@ -1,2 +1 @@ export * from "./forgot-password-auth-screen.component"; - diff --git a/examples/angular/src/app/screens/mfa-enrollment-screen/index.ts b/examples/angular/src/app/screens/mfa-enrollment-screen/index.ts index dcb9d6f17..1402c2ecd 100644 --- a/examples/angular/src/app/screens/mfa-enrollment-screen/index.ts +++ b/examples/angular/src/app/screens/mfa-enrollment-screen/index.ts @@ -1,2 +1 @@ export * from "./mfa-enrollment-screen.component"; - diff --git a/examples/angular/src/app/screens/mfa-enrollment-screen/mfa-enrollment-screen.component.ts b/examples/angular/src/app/screens/mfa-enrollment-screen/mfa-enrollment-screen.component.ts index 90051f6e0..4e7c7fd8e 100644 --- a/examples/angular/src/app/screens/mfa-enrollment-screen/mfa-enrollment-screen.component.ts +++ b/examples/angular/src/app/screens/mfa-enrollment-screen/mfa-enrollment-screen.component.ts @@ -40,4 +40,3 @@ export class MfaEnrollmentScreenComponent { this.router.navigate(["/"]); } } - diff --git a/examples/angular/src/app/screens/phone-auth-screen-w-oauth/index.ts b/examples/angular/src/app/screens/phone-auth-screen-w-oauth/index.ts index bee87dc66..3d0a30e0d 100644 --- a/examples/angular/src/app/screens/phone-auth-screen-w-oauth/index.ts +++ b/examples/angular/src/app/screens/phone-auth-screen-w-oauth/index.ts @@ -1,2 +1 @@ export * from "./phone-auth-screen-w-oauth.component"; - diff --git a/examples/angular/src/app/screens/phone-auth-screen-w-oauth/phone-auth-screen-w-oauth.component.ts b/examples/angular/src/app/screens/phone-auth-screen-w-oauth/phone-auth-screen-w-oauth.component.ts index c78b97fd0..a33e091ca 100644 --- a/examples/angular/src/app/screens/phone-auth-screen-w-oauth/phone-auth-screen-w-oauth.component.ts +++ b/examples/angular/src/app/screens/phone-auth-screen-w-oauth/phone-auth-screen-w-oauth.component.ts @@ -30,4 +30,3 @@ import { PhoneAuthScreenComponent, GoogleSignInButtonComponent } from "@invertas styles: [], }) export class PhoneAuthScreenWithOAuthComponent {} - diff --git a/examples/angular/src/app/screens/sign-in-auth-screen-w-handlers/index.ts b/examples/angular/src/app/screens/sign-in-auth-screen-w-handlers/index.ts index 787c34acd..d498e8f0b 100644 --- a/examples/angular/src/app/screens/sign-in-auth-screen-w-handlers/index.ts +++ b/examples/angular/src/app/screens/sign-in-auth-screen-w-handlers/index.ts @@ -1,2 +1 @@ export * from "./sign-in-auth-screen-w-handlers.component"; - diff --git a/examples/angular/src/app/screens/sign-in-auth-screen-w-handlers/sign-in-auth-screen-w-handlers.component.ts b/examples/angular/src/app/screens/sign-in-auth-screen-w-handlers/sign-in-auth-screen-w-handlers.component.ts index 7fb555d1a..ce7a7d296 100644 --- a/examples/angular/src/app/screens/sign-in-auth-screen-w-handlers/sign-in-auth-screen-w-handlers.component.ts +++ b/examples/angular/src/app/screens/sign-in-auth-screen-w-handlers/sign-in-auth-screen-w-handlers.component.ts @@ -48,4 +48,3 @@ export class SignInAuthScreenWithHandlersComponent { this.router.navigate(["/"]); } } - diff --git a/examples/angular/src/app/screens/sign-in-auth-screen-w-oauth/index.ts b/examples/angular/src/app/screens/sign-in-auth-screen-w-oauth/index.ts index 6ce353b14..2697e1510 100644 --- a/examples/angular/src/app/screens/sign-in-auth-screen-w-oauth/index.ts +++ b/examples/angular/src/app/screens/sign-in-auth-screen-w-oauth/index.ts @@ -1,2 +1 @@ export * from "./sign-in-auth-screen-w-oauth.component"; - diff --git a/examples/angular/src/app/screens/sign-in-auth-screen-w-oauth/sign-in-auth-screen-w-oauth.component.ts b/examples/angular/src/app/screens/sign-in-auth-screen-w-oauth/sign-in-auth-screen-w-oauth.component.ts index 1925ea947..35f2faf7d 100644 --- a/examples/angular/src/app/screens/sign-in-auth-screen-w-oauth/sign-in-auth-screen-w-oauth.component.ts +++ b/examples/angular/src/app/screens/sign-in-auth-screen-w-oauth/sign-in-auth-screen-w-oauth.component.ts @@ -30,4 +30,3 @@ import { SignInAuthScreenComponent, GoogleSignInButtonComponent } from "@inverta styles: [], }) export class SignInAuthScreenWithOAuthComponent {} - diff --git a/examples/angular/src/app/screens/sign-up-auth-screen-w-handlers/index.ts b/examples/angular/src/app/screens/sign-up-auth-screen-w-handlers/index.ts index 54f58ebea..64db728c0 100644 --- a/examples/angular/src/app/screens/sign-up-auth-screen-w-handlers/index.ts +++ b/examples/angular/src/app/screens/sign-up-auth-screen-w-handlers/index.ts @@ -1,2 +1 @@ export * from "./sign-up-auth-screen-w-handlers.component"; - diff --git a/examples/angular/src/app/screens/sign-up-auth-screen-w-handlers/sign-up-auth-screen-w-handlers.component.ts b/examples/angular/src/app/screens/sign-up-auth-screen-w-handlers/sign-up-auth-screen-w-handlers.component.ts index 93772b6a3..8aa94af46 100644 --- a/examples/angular/src/app/screens/sign-up-auth-screen-w-handlers/sign-up-auth-screen-w-handlers.component.ts +++ b/examples/angular/src/app/screens/sign-up-auth-screen-w-handlers/sign-up-auth-screen-w-handlers.component.ts @@ -23,9 +23,7 @@ import { SignUpAuthScreenComponent } from "@invertase/firebaseui-angular"; selector: "app-sign-up-auth-screen-w-handlers", standalone: true, imports: [CommonModule, SignUpAuthScreenComponent], - template: ` - - `, + template: ` `, styles: [], }) export class SignUpAuthScreenWithHandlersComponent { @@ -40,4 +38,3 @@ export class SignUpAuthScreenWithHandlersComponent { this.router.navigate(["/"]); } } - diff --git a/examples/angular/src/app/screens/sign-up-auth-screen-w-oauth/index.ts b/examples/angular/src/app/screens/sign-up-auth-screen-w-oauth/index.ts index 3711b7302..cc1567d01 100644 --- a/examples/angular/src/app/screens/sign-up-auth-screen-w-oauth/index.ts +++ b/examples/angular/src/app/screens/sign-up-auth-screen-w-oauth/index.ts @@ -1,2 +1 @@ export * from "./sign-up-auth-screen-w-oauth.component"; - diff --git a/examples/angular/src/app/screens/sign-up-auth-screen-w-oauth/sign-up-auth-screen-w-oauth.component.ts b/examples/angular/src/app/screens/sign-up-auth-screen-w-oauth/sign-up-auth-screen-w-oauth.component.ts index 3bc5a549a..1f5db65de 100644 --- a/examples/angular/src/app/screens/sign-up-auth-screen-w-oauth/sign-up-auth-screen-w-oauth.component.ts +++ b/examples/angular/src/app/screens/sign-up-auth-screen-w-oauth/sign-up-auth-screen-w-oauth.component.ts @@ -30,4 +30,3 @@ import { SignUpAuthScreenComponent, GoogleSignInButtonComponent } from "@inverta styles: [], }) export class SignUpAuthScreenWithOAuthComponent {} - diff --git a/examples/angular/src/app/services/user.service.ts b/examples/angular/src/app/services/user.service.ts index 5c8e639b3..7fb342a4e 100644 --- a/examples/angular/src/app/services/user.service.ts +++ b/examples/angular/src/app/services/user.service.ts @@ -28,4 +28,3 @@ export class UserService { return authState(this.auth); } } - diff --git a/packages/angular/src/lib/auth/forms/email-link-auth-form.ts b/packages/angular/src/lib/auth/forms/email-link-auth-form.ts index 13ff011c2..1412f3344 100644 --- a/packages/angular/src/lib/auth/forms/email-link-auth-form.ts +++ b/packages/angular/src/lib/auth/forms/email-link-auth-form.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Component, effect, output, signal } from "@angular/core"; +import { Component, effect, Output, EventEmitter, signal } from "@angular/core"; import { CommonModule } from "@angular/common"; import { injectForm, injectStore, TanStackAppField, TanStackField } from "@tanstack/angular-form"; import { UserCredential } from "@angular/fire/auth"; @@ -72,8 +72,8 @@ export class EmailLinkAuthFormComponent { emailSentMessage = injectTranslation("messages", "signInLinkSent"); unknownErrorLabel = injectTranslation("errors", "unknownError"); - emailSent = output(); - signIn = output(); + @Output() emailSent = new EventEmitter(); + @Output() signIn = new EventEmitter(); form = injectForm({ defaultValues: { @@ -100,7 +100,7 @@ export class EmailLinkAuthFormComponent { try { await sendSignInLinkToEmail(this.ui(), value.email); this.emailSentState.set(true); - this.emailSent?.emit(); + this.emailSent.emit(); return; } catch (error) { if (error instanceof FirebaseUIError) { @@ -120,7 +120,7 @@ export class EmailLinkAuthFormComponent { const credential = await completeEmailLinkSignIn(this.ui(), window.location.href); if (credential) { - this.signIn?.emit(credential); + this.signIn.emit(credential); } } } diff --git a/packages/angular/src/lib/auth/forms/forgot-password-auth-form.spec.ts b/packages/angular/src/lib/auth/forms/forgot-password-auth-form.spec.ts index 048b0860d..71517c7b0 100644 --- a/packages/angular/src/lib/auth/forms/forgot-password-auth-form.spec.ts +++ b/packages/angular/src/lib/auth/forms/forgot-password-auth-form.spec.ts @@ -16,6 +16,7 @@ import { render, screen, fireEvent, waitFor } from "@testing-library/angular"; import { CommonModule } from "@angular/common"; +import { EventEmitter } from "@angular/core"; import { TanStackField, TanStackAppField } from "@tanstack/angular-form"; import { ForgotPasswordAuthFormComponent } from "./forgot-password-auth-form"; import { @@ -73,7 +74,10 @@ describe("", () => { }); it("should render the form initially", async () => { - await render(ForgotPasswordAuthFormComponent, { + const backToSignInEmitter = new EventEmitter(); + backToSignInEmitter.subscribe(() => {}); + + const { fixture } = await render(ForgotPasswordAuthFormComponent, { imports: [ CommonModule, ForgotPasswordAuthFormComponent, @@ -85,7 +89,11 @@ describe("", () => { FormActionComponent, PoliciesComponent, ], + componentInputs: { + backToSignIn: backToSignInEmitter, + }, }); + fixture.detectChanges(); expect(screen.getByLabelText("Email Address")).toBeInTheDocument(); expect(screen.getByRole("button", { name: "Reset Password" })).toBeInTheDocument(); @@ -153,6 +161,9 @@ describe("", () => { }); it("should emit backToSignIn when back button is clicked", async () => { + const backToSignInEmitter = new EventEmitter(); + backToSignInEmitter.subscribe(() => {}); + const { fixture } = await render(ForgotPasswordAuthFormComponent, { imports: [ CommonModule, @@ -165,9 +176,12 @@ describe("", () => { FormActionComponent, PoliciesComponent, ], + componentInputs: { + backToSignIn: backToSignInEmitter, + }, }); - const component = fixture.componentInstance; - const backToSignInSpy = jest.spyOn(component.backToSignIn, "emit"); + fixture.detectChanges(); + const backToSignInSpy = jest.spyOn(backToSignInEmitter, "emit"); const backButton = screen.getByRole("button", { name: "Back to Sign In →" }); fireEvent.click(backButton); @@ -226,6 +240,9 @@ describe("", () => { }); const component = fixture.componentInstance; + // Access the getter to initialize EventEmitter (simulating template binding) + component.passwordSent.subscribe(() => {}); + fixture.detectChanges(); const passwordSentSpy = jest.spyOn(component.passwordSent, "emit"); const mockUI = { app: {}, auth: {} }; diff --git a/packages/angular/src/lib/auth/forms/forgot-password-auth-form.ts b/packages/angular/src/lib/auth/forms/forgot-password-auth-form.ts index af79f4cf0..ea78a3e4b 100644 --- a/packages/angular/src/lib/auth/forms/forgot-password-auth-form.ts +++ b/packages/angular/src/lib/auth/forms/forgot-password-auth-form.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Component, effect, output, signal } from "@angular/core"; +import { Component, effect, Output, EventEmitter, input, signal } from "@angular/core"; import { CommonModule } from "@angular/common"; import { injectForm, injectStore, TanStackAppField, TanStackField } from "@tanstack/angular-form"; import { FirebaseUIError, sendPasswordResetEmail } from "@invertase/firebaseui-core"; @@ -63,8 +63,8 @@ import { injectForgotPasswordAuthFormSchema, injectTranslation, injectUI } from
- @if (backToSignIn) { - + @if (backToSignIn()?.observed) { + } } @@ -82,8 +82,9 @@ export class ForgotPasswordAuthFormComponent { checkEmailForResetMessage = injectTranslation("messages", "checkEmailForReset"); unknownErrorLabel = injectTranslation("errors", "unknownError"); - passwordSent = output(); - backToSignIn = output(); + backToSignIn = input>(); + + @Output() passwordSent = new EventEmitter(); form = injectForm({ defaultValues: { @@ -108,7 +109,7 @@ export class ForgotPasswordAuthFormComponent { try { await sendPasswordResetEmail(this.ui(), value.email); this.emailSent.set(true); - this.passwordSent?.emit(); + this.passwordSent.emit(); return; } catch (error) { if (error instanceof FirebaseUIError) { diff --git a/packages/angular/src/lib/auth/forms/mfa/sms-multi-factor-assertion-form.ts b/packages/angular/src/lib/auth/forms/mfa/sms-multi-factor-assertion-form.ts index 0dba04799..8799fdfd3 100644 --- a/packages/angular/src/lib/auth/forms/mfa/sms-multi-factor-assertion-form.ts +++ b/packages/angular/src/lib/auth/forms/mfa/sms-multi-factor-assertion-form.ts @@ -13,7 +13,7 @@ * limitations under the License. */ -import { Component, ElementRef, effect, input, signal, output, computed, viewChild } from "@angular/core"; +import { Component, ElementRef, effect, input, signal, Output, EventEmitter, computed, viewChild } from "@angular/core"; import { CommonModule } from "@angular/common"; import { injectForm, injectStore, TanStackAppField, TanStackField } from "@tanstack/angular-form"; import { @@ -64,7 +64,7 @@ export class SmsMultiFactorAssertionPhoneFormComponent { private ui = injectUI(); hint = input.required(); - onSubmit = output(); + @Output() onSubmit = new EventEmitter(); sendCodeLabel = injectTranslation("labels", "sendCode"); @@ -163,7 +163,7 @@ export class SmsMultiFactorAssertionVerifyFormComponent { private formSchema = injectMultiFactorPhoneAuthVerifyFormSchema(); verificationId = input.required(); - onSuccess = output(); + @Output() onSuccess = new EventEmitter(); verificationCodeLabel = injectTranslation("labels", "verificationCode"); verifyCodeLabel = injectTranslation("labels", "verifyCode"); @@ -231,7 +231,7 @@ export class SmsMultiFactorAssertionVerifyFormComponent { }) export class SmsMultiFactorAssertionFormComponent { hint = input.required(); - onSuccess = output(); + @Output() onSuccess = new EventEmitter(); verification = signal<{ verificationId: string } | null>(null); diff --git a/packages/angular/src/lib/auth/forms/mfa/sms-multi-factor-enrollment-form.ts b/packages/angular/src/lib/auth/forms/mfa/sms-multi-factor-enrollment-form.ts index 784b61558..cda69d130 100644 --- a/packages/angular/src/lib/auth/forms/mfa/sms-multi-factor-enrollment-form.ts +++ b/packages/angular/src/lib/auth/forms/mfa/sms-multi-factor-enrollment-form.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Component, signal, effect, viewChild, computed, output } from "@angular/core"; +import { Component, signal, effect, viewChild, computed, Output, EventEmitter } from "@angular/core"; import { CommonModule } from "@angular/common"; import { TanStackField, TanStackAppField, injectForm, injectStore } from "@tanstack/angular-form"; import { ElementRef } from "@angular/core"; @@ -124,7 +124,7 @@ export class SmsMultiFactorEnrollmentFormComponent { verifyCodeLabel = injectTranslation("labels", "verifyCode"); smsVerificationPrompt = injectTranslation("prompts", "smsVerificationPrompt"); - onEnrollment = output(); + @Output() onEnrollment = new EventEmitter(); recaptchaContainer = viewChild.required>("recaptchaContainer"); diff --git a/packages/angular/src/lib/auth/forms/mfa/totp-multi-factor-assertion-form.ts b/packages/angular/src/lib/auth/forms/mfa/totp-multi-factor-assertion-form.ts index fea336044..4ede34b48 100644 --- a/packages/angular/src/lib/auth/forms/mfa/totp-multi-factor-assertion-form.ts +++ b/packages/angular/src/lib/auth/forms/mfa/totp-multi-factor-assertion-form.ts @@ -13,7 +13,7 @@ * limitations under the License. */ -import { Component, effect, input, output } from "@angular/core"; +import { Component, effect, input, Output, EventEmitter } from "@angular/core"; import { CommonModule } from "@angular/common"; import { injectForm, injectStore, TanStackAppField, TanStackField } from "@tanstack/angular-form"; import { injectMultiFactorTotpAuthVerifyFormSchema, injectTranslation, injectUI } from "../../../provider"; @@ -60,7 +60,7 @@ export class TotpMultiFactorAssertionFormComponent { private formSchema = injectMultiFactorTotpAuthVerifyFormSchema(); hint = input.required(); - onSuccess = output(); + @Output() onSuccess = new EventEmitter(); verificationCodeLabel = injectTranslation("labels", "verificationCode"); verifyCodeLabel = injectTranslation("labels", "verifyCode"); diff --git a/packages/angular/src/lib/auth/forms/mfa/totp-multi-factor-enrollment-form.ts b/packages/angular/src/lib/auth/forms/mfa/totp-multi-factor-enrollment-form.ts index e16dd022a..66dfc158f 100644 --- a/packages/angular/src/lib/auth/forms/mfa/totp-multi-factor-enrollment-form.ts +++ b/packages/angular/src/lib/auth/forms/mfa/totp-multi-factor-enrollment-form.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Component, signal, effect, output, computed, input } from "@angular/core"; +import { Component, signal, effect, Output, EventEmitter, computed, input } from "@angular/core"; import { CommonModule } from "@angular/common"; import { TanStackField, TanStackAppField, injectForm, injectStore } from "@tanstack/angular-form"; import { TotpMultiFactorGenerator, type TotpSecret } from "firebase/auth"; @@ -67,7 +67,7 @@ export class TotpMultiFactorSecretGenerationFormComponent { private ui = injectUI(); private formSchema = injectMultiFactorTotpAuthNumberFormSchema(); - onSubmit = output<{ secret: TotpSecret; displayName: string }>(); + @Output() onSubmit = new EventEmitter<{ secret: TotpSecret; displayName: string }>(); displayNameLabel = injectTranslation("labels", "displayName"); generateQrCodeLabel = injectTranslation("labels", "generateQrCode"); @@ -150,7 +150,7 @@ export class TotpMultiFactorVerificationFormComponent { secret = input.required(); displayName = input.required(); - onEnrollment = output(); + @Output() onEnrollment = new EventEmitter(); verificationCodeLabel = injectTranslation("labels", "verificationCode"); verifyCodeLabel = injectTranslation("labels", "verifyCode"); @@ -219,7 +219,7 @@ export class TotpMultiFactorEnrollmentFormComponent { private ui = injectUI(); enrollment = signal<{ secret: TotpSecret; displayName: string } | null>(null); - onEnrollment = output(); + @Output() onEnrollment = new EventEmitter(); constructor() { if (!this.ui().auth.currentUser) { diff --git a/packages/angular/src/lib/auth/forms/multi-factor-auth-assertion-form.ts b/packages/angular/src/lib/auth/forms/multi-factor-auth-assertion-form.ts index 656ed76f2..dc7c25ac6 100644 --- a/packages/angular/src/lib/auth/forms/multi-factor-auth-assertion-form.ts +++ b/packages/angular/src/lib/auth/forms/multi-factor-auth-assertion-form.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Component, computed, effect, output, signal } from "@angular/core"; +import { Component, computed, effect, Output, EventEmitter, signal } from "@angular/core"; import { CommonModule } from "@angular/common"; import { injectUI, injectTranslation } from "../../provider"; import { @@ -68,7 +68,7 @@ export class MultiFactorAuthAssertionFormComponent { }); } - onSuccess = output(); + @Output() onSuccess = new EventEmitter(); resolver = computed(() => { const resolver = this.ui().multiFactorResolver; diff --git a/packages/angular/src/lib/auth/forms/multi-factor-auth-enrollment-form.ts b/packages/angular/src/lib/auth/forms/multi-factor-auth-enrollment-form.ts index dcee1bb0f..ab7e7b561 100644 --- a/packages/angular/src/lib/auth/forms/multi-factor-auth-enrollment-form.ts +++ b/packages/angular/src/lib/auth/forms/multi-factor-auth-enrollment-form.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Component, signal, input, output, OnInit, computed } from "@angular/core"; +import { Component, signal, input, Output, EventEmitter, OnInit, computed } from "@angular/core"; import { CommonModule } from "@angular/common"; import { FactorId } from "firebase/auth"; import { injectTranslation } from "../../provider"; @@ -59,7 +59,7 @@ type Hint = (typeof FactorId)[keyof typeof FactorId]; }) export class MultiFactorAuthEnrollmentFormComponent implements OnInit { hints = input([FactorId.TOTP, FactorId.PHONE]); - onEnrollment = output(); + @Output() onEnrollment = new EventEmitter(); selectedHint = signal(undefined); diff --git a/packages/angular/src/lib/auth/forms/phone-auth-form.ts b/packages/angular/src/lib/auth/forms/phone-auth-form.ts index e7cdb938f..cec32b8e7 100644 --- a/packages/angular/src/lib/auth/forms/phone-auth-form.ts +++ b/packages/angular/src/lib/auth/forms/phone-auth-form.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Component, ElementRef, effect, input, signal, output, computed, viewChild } from "@angular/core"; +import { Component, ElementRef, effect, input, signal, Output, EventEmitter, computed, viewChild } from "@angular/core"; import { CommonModule } from "@angular/common"; import { injectForm, injectStore, TanStackAppField, TanStackField } from "@tanstack/angular-form"; import { @@ -79,7 +79,7 @@ export class PhoneNumberFormComponent { private ui = injectUI(); private formSchema = injectPhoneAuthFormSchema(); - onSubmit = output<{ verificationId: string; phoneNumber: string }>(); + @Output() onSubmit = new EventEmitter<{ verificationId: string; phoneNumber: string }>(); country = signal(countryData[0].code); phoneNumberLabel = injectTranslation("labels", "phoneNumber"); @@ -185,7 +185,7 @@ export class VerificationFormComponent { private formSchema = injectPhoneAuthVerifyFormSchema(); verificationId = input.required(); - signIn = output(); + @Output() signIn = new EventEmitter(); verificationCodeLabel = injectTranslation("labels", "verificationCode"); verifyCodeLabel = injectTranslation("labels", "verifyCode"); @@ -251,7 +251,7 @@ export class VerificationFormComponent { }) export class PhoneAuthFormComponent { verificationId = signal(null); - signIn = output(); + @Output() signIn = new EventEmitter(); handlePhoneSubmit(data: { verificationId: string; phoneNumber: string }) { this.verificationId.set(data.verificationId); diff --git a/packages/angular/src/lib/auth/forms/sign-in-auth-form.spec.ts b/packages/angular/src/lib/auth/forms/sign-in-auth-form.spec.ts index 98ff2fa23..616bb4cc1 100644 --- a/packages/angular/src/lib/auth/forms/sign-in-auth-form.spec.ts +++ b/packages/angular/src/lib/auth/forms/sign-in-auth-form.spec.ts @@ -16,6 +16,7 @@ import { render, screen, fireEvent } from "@testing-library/angular"; import { CommonModule } from "@angular/common"; +import { EventEmitter } from "@angular/core"; import { TanStackField, TanStackAppField } from "@tanstack/angular-form"; import { SignInAuthFormComponent } from "./sign-in-auth-form"; import { @@ -59,7 +60,12 @@ describe("", () => { }); it("should render the form initially", async () => { - await render(SignInAuthFormComponent, { + const forgotPasswordEmitter = new EventEmitter(); + const signUpEmitter = new EventEmitter(); + forgotPasswordEmitter.subscribe(() => {}); + signUpEmitter.subscribe(() => {}); + + const { fixture } = await render(SignInAuthFormComponent, { imports: [ CommonModule, SignInAuthFormComponent, @@ -71,7 +77,12 @@ describe("", () => { FormActionComponent, PoliciesComponent, ], + componentInputs: { + forgotPassword: forgotPasswordEmitter, + signUp: signUpEmitter, + }, }); + fixture.detectChanges(); expect(screen.getByLabelText("Email Address")).toBeInTheDocument(); expect(screen.getByLabelText("Password", { selector: "input" })).toBeInTheDocument(); @@ -81,6 +92,76 @@ describe("", () => { expect(screen.getByRole("button", { name: "Don't have an account? Sign Up" })).toBeInTheDocument(); }); + it("should not render forgot password button when output is not bound", async () => { + const { fixture } = await render(SignInAuthFormComponent, { + imports: [ + CommonModule, + SignInAuthFormComponent, + TanStackField, + TanStackAppField, + FormInputComponent, + FormSubmitComponent, + FormErrorMessageComponent, + FormActionComponent, + PoliciesComponent, + ], + }); + fixture.detectChanges(); + + expect(screen.getByLabelText("Email Address")).toBeInTheDocument(); + expect(screen.getByLabelText("Password", { selector: "input" })).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Sign In" })).toBeInTheDocument(); + expect(screen.queryByRole("button", { name: "Forgot Password" })).not.toBeInTheDocument(); + }); + + it("should not render sign up button when output is not bound", async () => { + const { fixture } = await render(SignInAuthFormComponent, { + imports: [ + CommonModule, + SignInAuthFormComponent, + TanStackField, + TanStackAppField, + FormInputComponent, + FormSubmitComponent, + FormErrorMessageComponent, + FormActionComponent, + PoliciesComponent, + ], + }); + fixture.detectChanges(); + + expect(screen.getByLabelText("Email Address")).toBeInTheDocument(); + expect(screen.getByLabelText("Password", { selector: "input" })).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Sign In" })).toBeInTheDocument(); + expect(screen.queryByRole("button", { name: "Don't have an account? Sign Up" })).not.toBeInTheDocument(); + }); + + it("should conditionally render buttons based on which outputs are bound", async () => { + const forgotPasswordEmitter = new EventEmitter(); + forgotPasswordEmitter.subscribe(() => {}); + + const { fixture } = await render(SignInAuthFormComponent, { + imports: [ + CommonModule, + SignInAuthFormComponent, + TanStackField, + TanStackAppField, + FormInputComponent, + FormSubmitComponent, + FormErrorMessageComponent, + FormActionComponent, + PoliciesComponent, + ], + componentInputs: { + forgotPassword: forgotPasswordEmitter, + }, + }); + fixture.detectChanges(); + + expect(screen.getByRole("button", { name: "Forgot Password" })).toBeInTheDocument(); + expect(screen.queryByRole("button", { name: "Don't have an account? Sign Up" })).not.toBeInTheDocument(); + }); + it("should have correct translation labels", async () => { const { fixture } = await render(SignInAuthFormComponent, { imports: [ @@ -126,6 +207,9 @@ describe("", () => { }); it("should emit forgotPassword when forgot password button is clicked", async () => { + const forgotPasswordEmitter = new EventEmitter(); + forgotPasswordEmitter.subscribe(() => {}); + const { fixture } = await render(SignInAuthFormComponent, { imports: [ CommonModule, @@ -138,9 +222,12 @@ describe("", () => { FormActionComponent, PoliciesComponent, ], + componentInputs: { + forgotPassword: forgotPasswordEmitter, + }, }); - const component = fixture.componentInstance; - const forgotPasswordSpy = jest.spyOn(component.forgotPassword, "emit"); + fixture.detectChanges(); + const forgotPasswordSpy = jest.spyOn(forgotPasswordEmitter, "emit"); const forgotPasswordButton = screen.getByRole("button", { name: "Forgot Password" }); fireEvent.click(forgotPasswordButton); @@ -148,6 +235,9 @@ describe("", () => { }); it("should emit signUp when sign up button is clicked", async () => { + const signUpEmitter = new EventEmitter(); + signUpEmitter.subscribe(() => {}); + const { fixture } = await render(SignInAuthFormComponent, { imports: [ CommonModule, @@ -160,9 +250,12 @@ describe("", () => { FormActionComponent, PoliciesComponent, ], + componentInputs: { + signUp: signUpEmitter, + }, }); - const component = fixture.componentInstance; - const signUpSpy = jest.spyOn(component.signUp, "emit"); + fixture.detectChanges(); + const signUpSpy = jest.spyOn(signUpEmitter, "emit"); const signUpButton = screen.getByRole("button", { name: "Don't have an account? Sign Up" }); fireEvent.click(signUpButton); diff --git a/packages/angular/src/lib/auth/forms/sign-in-auth-form.ts b/packages/angular/src/lib/auth/forms/sign-in-auth-form.ts index 820c06d69..88833f015 100644 --- a/packages/angular/src/lib/auth/forms/sign-in-auth-form.ts +++ b/packages/angular/src/lib/auth/forms/sign-in-auth-form.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Component, output, effect } from "@angular/core"; +import { Component, Output, EventEmitter, input, effect } from "@angular/core"; import { CommonModule } from "@angular/common"; import { UserCredential } from "@angular/fire/auth"; import { injectForm, TanStackField, TanStackAppField, injectStore } from "@tanstack/angular-form"; @@ -61,8 +61,8 @@ import { [label]="passwordLabel()" type="password" > - @if (forgotPassword) { - } @@ -78,8 +78,8 @@ import {
- @if (signUp) { - + @if (signUp()?.observed) { + } `, @@ -96,9 +96,10 @@ export class SignInAuthFormComponent { signUpLabel = injectTranslation("labels", "signUp"); unknownErrorLabel = injectTranslation("errors", "unknownError"); - forgotPassword = output(); - signUp = output(); - signIn = output(); + forgotPassword = input>(); + signUp = input>(); + + @Output() signIn = new EventEmitter(); form = injectForm({ defaultValues: { diff --git a/packages/angular/src/lib/auth/forms/sign-up-auth-form.spec.ts b/packages/angular/src/lib/auth/forms/sign-up-auth-form.spec.ts index 2e6ccec6e..57161b17c 100644 --- a/packages/angular/src/lib/auth/forms/sign-up-auth-form.spec.ts +++ b/packages/angular/src/lib/auth/forms/sign-up-auth-form.spec.ts @@ -16,6 +16,7 @@ import { render, screen, fireEvent } from "@testing-library/angular"; import { CommonModule } from "@angular/common"; +import { EventEmitter } from "@angular/core"; import { TanStackField, TanStackAppField } from "@tanstack/angular-form"; import { SignUpAuthFormComponent } from "./sign-up-auth-form"; import { @@ -64,7 +65,10 @@ describe("", () => { }); it("should render the form initially without display name field", async () => { - await render(SignUpAuthFormComponent, { + const signInEmitter = new EventEmitter(); + signInEmitter.subscribe(() => {}); + + const { fixture } = await render(SignUpAuthFormComponent, { imports: [ CommonModule, SignUpAuthFormComponent, @@ -76,7 +80,11 @@ describe("", () => { FormActionComponent, PoliciesComponent, ], + componentInputs: { + signIn: signInEmitter, + }, }); + fixture.detectChanges(); expect(screen.getByLabelText("Email Address")).toBeInTheDocument(); expect(screen.getByLabelText("Password")).toBeInTheDocument(); @@ -155,6 +163,9 @@ describe("", () => { }); it("should emit signIn when sign in button is clicked", async () => { + const signInEmitter = new EventEmitter(); + signInEmitter.subscribe(() => {}); + const { fixture } = await render(SignUpAuthFormComponent, { imports: [ CommonModule, @@ -167,9 +178,12 @@ describe("", () => { FormActionComponent, PoliciesComponent, ], + componentInputs: { + signIn: signInEmitter, + }, }); - const component = fixture.componentInstance; - const signInSpy = jest.spyOn(component.signIn, "emit"); + fixture.detectChanges(); + const signInSpy = jest.spyOn(signInEmitter, "emit"); const signInButton = screen.getByRole("button", { name: "Already have an account? Sign In" }); fireEvent.click(signInButton); diff --git a/packages/angular/src/lib/auth/forms/sign-up-auth-form.ts b/packages/angular/src/lib/auth/forms/sign-up-auth-form.ts index a1f4386cb..9ec46666e 100644 --- a/packages/angular/src/lib/auth/forms/sign-up-auth-form.ts +++ b/packages/angular/src/lib/auth/forms/sign-up-auth-form.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Component, output, effect, computed } from "@angular/core"; +import { Component, Output, EventEmitter, input, effect, computed } from "@angular/core"; import { CommonModule } from "@angular/common"; import { injectForm, injectStore, TanStackAppField, TanStackField } from "@tanstack/angular-form"; import { FirebaseUIError, createUserWithEmailAndPassword, hasBehavior } from "@invertase/firebaseui-core"; @@ -69,8 +69,8 @@ import {
- @if (signIn) { - + @if (signIn()?.observed) { + } `, @@ -91,8 +91,9 @@ export class SignUpAuthFormComponent { signInLabel = injectTranslation("labels", "signIn"); unknownErrorLabel = injectTranslation("errors", "unknownError"); - signUp = output(); - signIn = output(); + signIn = input>(); + + @Output() signUp = new EventEmitter(); form = injectForm({ defaultValues: { 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 e4492fa43..38507aee1 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, computed } from "@angular/core"; +import { Component, Output, EventEmitter, computed } from "@angular/core"; import { CommonModule } from "@angular/common"; import { CardComponent, @@ -71,6 +71,6 @@ export class EmailLinkAuthScreenComponent { titleText = injectTranslation("labels", "signIn"); subtitleText = injectTranslation("prompts", "signInToAccount"); - emailSent = output(); - signIn = output(); + @Output() emailSent = new EventEmitter(); + @Output() signIn = new EventEmitter(); } diff --git a/packages/angular/src/lib/auth/screens/forgot-password-auth-screen.ts b/packages/angular/src/lib/auth/screens/forgot-password-auth-screen.ts index d8c8f4d23..de2b16ce9 100644 --- a/packages/angular/src/lib/auth/screens/forgot-password-auth-screen.ts +++ b/packages/angular/src/lib/auth/screens/forgot-password-auth-screen.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Component, output } from "@angular/core"; +import { Component, Output, EventEmitter } from "@angular/core"; import { CommonModule } from "@angular/common"; import { CardComponent, @@ -46,7 +46,7 @@ import { ForgotPasswordAuthFormComponent } from "../forms/forgot-password-auth-f {{ subtitleText() }} - +
@@ -56,6 +56,6 @@ export class ForgotPasswordAuthScreenComponent { titleText = injectTranslation("labels", "resetPassword"); subtitleText = injectTranslation("prompts", "enterEmailToReset"); - passwordSent = output(); - backToSignIn = output(); + @Output() passwordSent = new EventEmitter(); + @Output() backToSignIn = new EventEmitter(); } diff --git a/packages/angular/src/lib/auth/screens/multi-factor-auth-assertion-screen.ts b/packages/angular/src/lib/auth/screens/multi-factor-auth-assertion-screen.ts index 0aec82ebf..4dac522d7 100644 --- a/packages/angular/src/lib/auth/screens/multi-factor-auth-assertion-screen.ts +++ b/packages/angular/src/lib/auth/screens/multi-factor-auth-assertion-screen.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Component, output } from "@angular/core"; +import { Component, Output, EventEmitter } from "@angular/core"; import { CommonModule } from "@angular/common"; import { UserCredential } from "@angular/fire/auth"; import { injectTranslation } from "../../provider"; @@ -54,7 +54,7 @@ import { `, }) export class MultiFactorAuthAssertionScreenComponent { - onSuccess = output(); + @Output() onSuccess = new EventEmitter(); titleText = injectTranslation("labels", "multiFactorAssertion"); subtitleText = injectTranslation("prompts", "mfaAssertionPrompt"); diff --git a/packages/angular/src/lib/auth/screens/multi-factor-auth-enrollment-screen.ts b/packages/angular/src/lib/auth/screens/multi-factor-auth-enrollment-screen.ts index ed82f15c6..adcd7e160 100644 --- a/packages/angular/src/lib/auth/screens/multi-factor-auth-enrollment-screen.ts +++ b/packages/angular/src/lib/auth/screens/multi-factor-auth-enrollment-screen.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Component, output, input } from "@angular/core"; +import { Component, Output, EventEmitter, input } from "@angular/core"; import { CommonModule } from "@angular/common"; import { FactorId } from "firebase/auth"; import { injectTranslation } from "../../provider"; @@ -57,7 +57,7 @@ type Hint = (typeof FactorId)[keyof typeof FactorId]; }) export class MultiFactorAuthEnrollmentScreenComponent { hints = input([FactorId.TOTP, FactorId.PHONE]); - onEnrollment = output(); + @Output() onEnrollment = new EventEmitter(); titleText = injectTranslation("labels", "multiFactorEnrollment"); subtitleText = injectTranslation("prompts", "mfaEnrollmentPrompt"); diff --git a/packages/angular/src/lib/auth/screens/oauth-screen.ts b/packages/angular/src/lib/auth/screens/oauth-screen.ts index 916bb7736..9fda7ba99 100644 --- a/packages/angular/src/lib/auth/screens/oauth-screen.ts +++ b/packages/angular/src/lib/auth/screens/oauth-screen.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Component, computed, output } from "@angular/core"; +import { Component, computed, Output, EventEmitter } from "@angular/core"; import { CommonModule } from "@angular/common"; import { CardComponent, @@ -75,5 +75,5 @@ export class OAuthScreenComponent { titleText = injectTranslation("labels", "signIn"); subtitleText = injectTranslation("prompts", "signInToAccount"); - onSignIn = output(); + @Output() onSignIn = new EventEmitter(); } diff --git a/packages/angular/src/lib/auth/screens/phone-auth-screen.ts b/packages/angular/src/lib/auth/screens/phone-auth-screen.ts index ae523e4bc..d4ae5c7bc 100644 --- a/packages/angular/src/lib/auth/screens/phone-auth-screen.ts +++ b/packages/angular/src/lib/auth/screens/phone-auth-screen.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Component, input, output, computed } from "@angular/core"; +import { Component, input, Output, EventEmitter, computed } from "@angular/core"; import { CommonModule } from "@angular/common"; import { CardComponent, @@ -71,5 +71,5 @@ export class PhoneAuthScreenComponent { titleText = injectTranslation("labels", "signIn"); subtitleText = injectTranslation("prompts", "signInToAccount"); - signIn = output(); + @Output() signIn = new EventEmitter(); } diff --git a/packages/angular/src/lib/auth/screens/sign-in-auth-screen.ts b/packages/angular/src/lib/auth/screens/sign-in-auth-screen.ts index 9b1ef4b9b..431c4ff53 100644 --- a/packages/angular/src/lib/auth/screens/sign-in-auth-screen.ts +++ b/packages/angular/src/lib/auth/screens/sign-in-auth-screen.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Component, output, computed } from "@angular/core"; +import { Component, Output, EventEmitter, computed } from "@angular/core"; import { CommonModule } from "@angular/common"; import { injectTranslation, injectUI } from "../../provider"; @@ -54,11 +54,7 @@ import { UserCredential } from "@angular/fire/auth"; {{ subtitleText() }} - + @@ -75,7 +71,7 @@ export class SignInAuthScreenComponent { titleText = injectTranslation("labels", "signIn"); subtitleText = injectTranslation("prompts", "signInToAccount"); - forgotPassword = output(); - signUp = output(); - signIn = output(); + @Output() forgotPassword = new EventEmitter(); + @Output() signUp = new EventEmitter(); + @Output() signIn = new EventEmitter(); } diff --git a/packages/angular/src/lib/auth/screens/sign-up-auth-screen.ts b/packages/angular/src/lib/auth/screens/sign-up-auth-screen.ts index 260f2f447..98514213b 100644 --- a/packages/angular/src/lib/auth/screens/sign-up-auth-screen.ts +++ b/packages/angular/src/lib/auth/screens/sign-up-auth-screen.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Component, output, computed } from "@angular/core"; +import { Component, Output, EventEmitter, computed } from "@angular/core"; import { CommonModule } from "@angular/common"; import { UserCredential } from "@angular/fire/auth"; @@ -55,7 +55,7 @@ import { {{ subtitleText() }} - + @@ -72,6 +72,6 @@ export class SignUpAuthScreenComponent { titleText = injectTranslation("labels", "signUp"); subtitleText = injectTranslation("prompts", "enterDetailsToCreate"); - signUp = output(); - signIn = output(); + @Output() signUp = new EventEmitter(); + @Output() signIn = new EventEmitter(); } diff --git a/packages/angular/src/lib/components/card.spec.ts b/packages/angular/src/lib/components/card.spec.ts index 7096581fa..022b230f5 100644 --- a/packages/angular/src/lib/components/card.spec.ts +++ b/packages/angular/src/lib/components/card.spec.ts @@ -121,7 +121,9 @@ describe("", () => { describe("", () => { it("renders a card title with children", async () => { - await render(`Title content`, { imports: [CardTitleComponent] }); + await render(`Title content`, { + imports: [CardTitleComponent], + }); const titleHost = screen.getByTestId("title-host"); const title = screen.getByRole("heading", { name: "Title content" }); @@ -142,7 +144,9 @@ describe("", () => { describe("", () => { it("renders a card subtitle with children", async () => { - await render(`Subtitle content`, { imports: [CardSubtitleComponent] }); + await render(`Subtitle content`, { + imports: [CardSubtitleComponent], + }); const subtitleHost = screen.getByTestId("subtitle-host"); const subtitle = screen.getByText("Subtitle content"); @@ -164,7 +168,9 @@ describe("", () => { describe("", () => { it("renders a card content with children", async () => { - await render(`Content content`, { imports: [CardContentComponent] }); + await render(`Content content`, { + imports: [CardContentComponent], + }); const content = screen.getByTestId("test-content"); expect(content).toHaveClass("fui-card__content"); diff --git a/packages/angular/src/lib/components/card.ts b/packages/angular/src/lib/components/card.ts index e5221ad3c..48fe36cac 100644 --- a/packages/angular/src/lib/components/card.ts +++ b/packages/angular/src/lib/components/card.ts @@ -87,8 +87,6 @@ export class CardSubtitleComponent {} class: "fui-card__content", style: "display: block;", }, - template: ` - - `, + template: ` `, }) export class CardContentComponent {} From 052c495b7ae73a5c32067a15b8bf707d2c2d2444 Mon Sep 17 00:00:00 2001 From: Elliot Hesp Date: Sat, 8 Nov 2025 19:38:14 +0000 Subject: [PATCH 26/32] refactor(angular): Set components to host block by default --- .../oauth-screen/oauth-screen.component.ts | 44 ++++++++++++++++--- .../sign-in-auth-screen-w-oauth.component.ts | 32 ++++++++++++-- .../sign-up-auth-screen-w-oauth.component.ts | 32 ++++++++++++-- .../lib/auth/forms/email-link-auth-form.ts | 3 ++ .../auth/forms/forgot-password-auth-form.ts | 3 ++ .../mfa/sms-multi-factor-assertion-form.ts | 9 ++++ .../mfa/sms-multi-factor-enrollment-form.ts | 3 ++ .../mfa/totp-multi-factor-assertion-form.ts | 3 ++ .../mfa/totp-multi-factor-enrollment-form.ts | 9 ++++ .../forms/multi-factor-auth-assertion-form.ts | 3 ++ .../multi-factor-auth-enrollment-form.ts | 3 ++ .../src/lib/auth/forms/phone-auth-form.ts | 9 ++++ .../src/lib/auth/forms/sign-in-auth-form.ts | 3 ++ .../src/lib/auth/forms/sign-up-auth-form.ts | 3 ++ .../lib/auth/oauth/apple-sign-in-button.ts | 8 +++- .../lib/auth/oauth/facebook-sign-in-button.ts | 8 +++- .../auth/oauth/github-sign-in-button.spec.ts | 6 +-- .../lib/auth/oauth/github-sign-in-button.ts | 12 +++-- .../lib/auth/oauth/google-sign-in-button.ts | 8 +++- .../auth/oauth/microsoft-sign-in-button.ts | 8 +++- .../src/lib/auth/oauth/oauth-button.ts | 3 ++ .../lib/auth/oauth/twitter-sign-in-button.ts | 8 +++- .../auth/screens/email-link-auth-screen.ts | 3 ++ .../screens/forgot-password-auth-screen.ts | 3 ++ .../multi-factor-auth-assertion-screen.ts | 3 ++ .../multi-factor-auth-enrollment-screen.ts | 3 ++ .../src/lib/auth/screens/oauth-screen.ts | 13 +++--- .../src/lib/auth/screens/phone-auth-screen.ts | 3 ++ .../lib/auth/screens/sign-in-auth-screen.ts | 3 ++ .../lib/auth/screens/sign-up-auth-screen.ts | 3 ++ .../angular/src/lib/components/content.ts | 3 ++ .../src/lib/components/country-selector.ts | 3 ++ .../angular/src/lib/components/divider.ts | 3 ++ packages/angular/src/lib/components/form.ts | 10 +++++ .../src/lib/components/logos/facebook.ts | 2 +- .../src/lib/components/redirect-error.ts | 3 ++ packages/angular/src/public-api.ts | 2 +- packages/core/brands/facebook/logo.svg | 2 +- packages/styles/src/base.css | 2 +- 39 files changed, 246 insertions(+), 38 deletions(-) diff --git a/examples/angular/src/app/screens/oauth-screen/oauth-screen.component.ts b/examples/angular/src/app/screens/oauth-screen/oauth-screen.component.ts index a952ec62f..b878f91bf 100644 --- a/examples/angular/src/app/screens/oauth-screen/oauth-screen.component.ts +++ b/examples/angular/src/app/screens/oauth-screen/oauth-screen.component.ts @@ -14,15 +14,49 @@ * limitations under the License. */ -import { Component } from "@angular/core"; +import { Component, signal } from "@angular/core"; import { CommonModule } from "@angular/common"; -import { OAuthScreenComponent } from "@invertase/firebaseui-angular"; +import { + OAuthScreenComponent, + GoogleSignInButtonComponent, + FacebookSignInButtonComponent, + AppleSignInButtonComponent, + GitHubSignInButtonComponent, + MicrosoftSignInButtonComponent, + TwitterSignInButtonComponent, +} from "@invertase/firebaseui-angular"; @Component({ selector: "app-oauth-screen", standalone: true, - imports: [CommonModule, OAuthScreenComponent], - template: ` `, + imports: [ + CommonModule, + OAuthScreenComponent, + GoogleSignInButtonComponent, + FacebookSignInButtonComponent, + AppleSignInButtonComponent, + GitHubSignInButtonComponent, + MicrosoftSignInButtonComponent, + TwitterSignInButtonComponent, + ], + template: ` + + + + + + + + +
+ +
+ `, styles: [], }) -export class OAuthScreenWrapperComponent {} +export class OAuthScreenWrapperComponent { + themed = signal(false); +} diff --git a/examples/angular/src/app/screens/sign-in-auth-screen-w-oauth/sign-in-auth-screen-w-oauth.component.ts b/examples/angular/src/app/screens/sign-in-auth-screen-w-oauth/sign-in-auth-screen-w-oauth.component.ts index 35f2faf7d..4c17c1d70 100644 --- a/examples/angular/src/app/screens/sign-in-auth-screen-w-oauth/sign-in-auth-screen-w-oauth.component.ts +++ b/examples/angular/src/app/screens/sign-in-auth-screen-w-oauth/sign-in-auth-screen-w-oauth.component.ts @@ -16,15 +16,41 @@ import { Component } from "@angular/core"; import { CommonModule } from "@angular/common"; -import { SignInAuthScreenComponent, GoogleSignInButtonComponent } from "@invertase/firebaseui-angular"; +import { + SignInAuthScreenComponent, + ContentComponent, + GoogleSignInButtonComponent, + FacebookSignInButtonComponent, + AppleSignInButtonComponent, + GitHubSignInButtonComponent, + MicrosoftSignInButtonComponent, + TwitterSignInButtonComponent, +} from "@invertase/firebaseui-angular"; @Component({ selector: "app-sign-in-auth-screen-w-oauth", standalone: true, - imports: [CommonModule, SignInAuthScreenComponent, GoogleSignInButtonComponent], + imports: [ + CommonModule, + SignInAuthScreenComponent, + ContentComponent, + GoogleSignInButtonComponent, + FacebookSignInButtonComponent, + AppleSignInButtonComponent, + GitHubSignInButtonComponent, + MicrosoftSignInButtonComponent, + TwitterSignInButtonComponent, + ], template: ` - + + + + + + + + `, styles: [], diff --git a/examples/angular/src/app/screens/sign-up-auth-screen-w-oauth/sign-up-auth-screen-w-oauth.component.ts b/examples/angular/src/app/screens/sign-up-auth-screen-w-oauth/sign-up-auth-screen-w-oauth.component.ts index 1f5db65de..0640259b7 100644 --- a/examples/angular/src/app/screens/sign-up-auth-screen-w-oauth/sign-up-auth-screen-w-oauth.component.ts +++ b/examples/angular/src/app/screens/sign-up-auth-screen-w-oauth/sign-up-auth-screen-w-oauth.component.ts @@ -16,15 +16,41 @@ import { Component } from "@angular/core"; import { CommonModule } from "@angular/common"; -import { SignUpAuthScreenComponent, GoogleSignInButtonComponent } from "@invertase/firebaseui-angular"; +import { + SignUpAuthScreenComponent, + ContentComponent, + GoogleSignInButtonComponent, + FacebookSignInButtonComponent, + AppleSignInButtonComponent, + GitHubSignInButtonComponent, + MicrosoftSignInButtonComponent, + TwitterSignInButtonComponent, +} from "@invertase/firebaseui-angular"; @Component({ selector: "app-sign-up-auth-screen-w-oauth", standalone: true, - imports: [CommonModule, SignUpAuthScreenComponent, GoogleSignInButtonComponent], + imports: [ + CommonModule, + SignUpAuthScreenComponent, + ContentComponent, + GoogleSignInButtonComponent, + FacebookSignInButtonComponent, + AppleSignInButtonComponent, + GitHubSignInButtonComponent, + MicrosoftSignInButtonComponent, + TwitterSignInButtonComponent, + ], template: ` - + + + + + + + + `, styles: [], diff --git a/packages/angular/src/lib/auth/forms/email-link-auth-form.ts b/packages/angular/src/lib/auth/forms/email-link-auth-form.ts index 1412f3344..8be54e5c9 100644 --- a/packages/angular/src/lib/auth/forms/email-link-auth-form.ts +++ b/packages/angular/src/lib/auth/forms/email-link-auth-form.ts @@ -27,6 +27,9 @@ import { injectEmailLinkAuthFormSchema, injectTranslation, injectUI } from "../. @Component({ selector: "fui-email-link-auth-form", standalone: true, + host: { + style: "display: block;", + }, imports: [ CommonModule, TanStackField, diff --git a/packages/angular/src/lib/auth/forms/forgot-password-auth-form.ts b/packages/angular/src/lib/auth/forms/forgot-password-auth-form.ts index ea78a3e4b..fa72689eb 100644 --- a/packages/angular/src/lib/auth/forms/forgot-password-auth-form.ts +++ b/packages/angular/src/lib/auth/forms/forgot-password-auth-form.ts @@ -31,6 +31,9 @@ import { injectForgotPasswordAuthFormSchema, injectTranslation, injectUI } from @Component({ selector: "fui-forgot-password-auth-form", standalone: true, + host: { + style: "display: block;", + }, imports: [ CommonModule, TanStackField, diff --git a/packages/angular/src/lib/auth/forms/mfa/sms-multi-factor-assertion-form.ts b/packages/angular/src/lib/auth/forms/mfa/sms-multi-factor-assertion-form.ts index 8799fdfd3..353b816c7 100644 --- a/packages/angular/src/lib/auth/forms/mfa/sms-multi-factor-assertion-form.ts +++ b/packages/angular/src/lib/auth/forms/mfa/sms-multi-factor-assertion-form.ts @@ -39,6 +39,9 @@ type PhoneMultiFactorInfo = MultiFactorInfo & { selector: "fui-sms-multi-factor-assertion-phone-form", standalone: true, imports: [CommonModule, FormSubmitComponent, FormErrorMessageComponent], + host: { + style: "display: block;", + }, template: `
@@ -129,6 +132,9 @@ export class SmsMultiFactorAssertionPhoneFormComponent { @Component({ selector: "fui-sms-multi-factor-assertion-verify-form", standalone: true, + host: { + style: "display: block;", + }, imports: [ CommonModule, TanStackField, @@ -216,6 +222,9 @@ export class SmsMultiFactorAssertionVerifyFormComponent { selector: "fui-sms-multi-factor-assertion-form", standalone: true, imports: [CommonModule, SmsMultiFactorAssertionPhoneFormComponent, SmsMultiFactorAssertionVerifyFormComponent], + host: { + style: "display: block;", + }, template: `
@if (verification()) { diff --git a/packages/angular/src/lib/auth/forms/mfa/sms-multi-factor-enrollment-form.ts b/packages/angular/src/lib/auth/forms/mfa/sms-multi-factor-enrollment-form.ts index cda69d130..de3c8e637 100644 --- a/packages/angular/src/lib/auth/forms/mfa/sms-multi-factor-enrollment-form.ts +++ b/packages/angular/src/lib/auth/forms/mfa/sms-multi-factor-enrollment-form.ts @@ -42,6 +42,9 @@ import { @Component({ selector: "fui-sms-multi-factor-enrollment-form", standalone: true, + host: { + style: "display: block;", + }, imports: [ CommonModule, TanStackField, diff --git a/packages/angular/src/lib/auth/forms/mfa/totp-multi-factor-assertion-form.ts b/packages/angular/src/lib/auth/forms/mfa/totp-multi-factor-assertion-form.ts index 4ede34b48..4471e66da 100644 --- a/packages/angular/src/lib/auth/forms/mfa/totp-multi-factor-assertion-form.ts +++ b/packages/angular/src/lib/auth/forms/mfa/totp-multi-factor-assertion-form.ts @@ -24,6 +24,9 @@ import { TotpMultiFactorGenerator, type MultiFactorInfo, type UserCredential } f @Component({ selector: "fui-totp-multi-factor-assertion-form", standalone: true, + host: { + style: "display: block;", + }, imports: [ CommonModule, TanStackField, diff --git a/packages/angular/src/lib/auth/forms/mfa/totp-multi-factor-enrollment-form.ts b/packages/angular/src/lib/auth/forms/mfa/totp-multi-factor-enrollment-form.ts index 66dfc158f..e96732791 100644 --- a/packages/angular/src/lib/auth/forms/mfa/totp-multi-factor-enrollment-form.ts +++ b/packages/angular/src/lib/auth/forms/mfa/totp-multi-factor-enrollment-form.ts @@ -35,6 +35,9 @@ import { @Component({ selector: "fui-totp-multi-factor-secret-generation-form", standalone: true, + host: { + style: "display: block;", + }, imports: [ CommonModule, TanStackField, @@ -110,6 +113,9 @@ export class TotpMultiFactorSecretGenerationFormComponent { @Component({ selector: "fui-totp-multi-factor-verification-form", standalone: true, + host: { + style: "display: block;", + }, imports: [ CommonModule, TanStackField, @@ -201,6 +207,9 @@ export class TotpMultiFactorVerificationFormComponent { selector: "fui-totp-multi-factor-enrollment-form", standalone: true, imports: [CommonModule, TotpMultiFactorSecretGenerationFormComponent, TotpMultiFactorVerificationFormComponent], + host: { + style: "display: block;", + }, template: `
@if (!enrollment()) { diff --git a/packages/angular/src/lib/auth/forms/multi-factor-auth-assertion-form.ts b/packages/angular/src/lib/auth/forms/multi-factor-auth-assertion-form.ts index dc7c25ac6..55fcc1b33 100644 --- a/packages/angular/src/lib/auth/forms/multi-factor-auth-assertion-form.ts +++ b/packages/angular/src/lib/auth/forms/multi-factor-auth-assertion-form.ts @@ -31,6 +31,9 @@ import { ButtonComponent } from "../../components/button"; selector: "fui-multi-factor-auth-assertion-form", standalone: true, imports: [CommonModule, SmsMultiFactorAssertionFormComponent, TotpMultiFactorAssertionFormComponent, ButtonComponent], + host: { + style: "display: block;", + }, template: `
@if (selectedHint()) { diff --git a/packages/angular/src/lib/auth/forms/multi-factor-auth-enrollment-form.ts b/packages/angular/src/lib/auth/forms/multi-factor-auth-enrollment-form.ts index ab7e7b561..30ee40402 100644 --- a/packages/angular/src/lib/auth/forms/multi-factor-auth-enrollment-form.ts +++ b/packages/angular/src/lib/auth/forms/multi-factor-auth-enrollment-form.ts @@ -27,6 +27,9 @@ type Hint = (typeof FactorId)[keyof typeof FactorId]; @Component({ selector: "fui-multi-factor-auth-enrollment-form", standalone: true, + host: { + style: "display: block;", + }, imports: [ CommonModule, SmsMultiFactorEnrollmentFormComponent, diff --git a/packages/angular/src/lib/auth/forms/phone-auth-form.ts b/packages/angular/src/lib/auth/forms/phone-auth-form.ts index cec32b8e7..e3ac7165d 100644 --- a/packages/angular/src/lib/auth/forms/phone-auth-form.ts +++ b/packages/angular/src/lib/auth/forms/phone-auth-form.ts @@ -40,6 +40,9 @@ import { @Component({ selector: "fui-phone-number-form", standalone: true, + host: { + style: "display: block;", + }, imports: [ CommonModule, TanStackField, @@ -147,6 +150,9 @@ export class PhoneNumberFormComponent { @Component({ selector: "fui-verification-form", standalone: true, + host: { + style: "display: block;", + }, imports: [ CommonModule, TanStackField, @@ -239,6 +245,9 @@ export class VerificationFormComponent { selector: "fui-phone-auth-form", standalone: true, imports: [CommonModule, PhoneNumberFormComponent, VerificationFormComponent], + host: { + style: "display: block;", + }, template: `
@if (verificationId()) { diff --git a/packages/angular/src/lib/auth/forms/sign-in-auth-form.ts b/packages/angular/src/lib/auth/forms/sign-in-auth-form.ts index 88833f015..94ba33aaa 100644 --- a/packages/angular/src/lib/auth/forms/sign-in-auth-form.ts +++ b/packages/angular/src/lib/auth/forms/sign-in-auth-form.ts @@ -32,6 +32,9 @@ import { @Component({ selector: "fui-sign-in-auth-form", standalone: true, + host: { + style: "display: block;", + }, imports: [ CommonModule, TanStackField, diff --git a/packages/angular/src/lib/auth/forms/sign-up-auth-form.ts b/packages/angular/src/lib/auth/forms/sign-up-auth-form.ts index 9ec46666e..5c3a9d35a 100644 --- a/packages/angular/src/lib/auth/forms/sign-up-auth-form.ts +++ b/packages/angular/src/lib/auth/forms/sign-up-auth-form.ts @@ -32,6 +32,9 @@ import { @Component({ selector: "fui-sign-up-auth-form", standalone: true, + host: { + style: "display: block;", + }, imports: [ CommonModule, TanStackField, diff --git a/packages/angular/src/lib/auth/oauth/apple-sign-in-button.ts b/packages/angular/src/lib/auth/oauth/apple-sign-in-button.ts index a9e358f6e..3ef5b5158 100644 --- a/packages/angular/src/lib/auth/oauth/apple-sign-in-button.ts +++ b/packages/angular/src/lib/auth/oauth/apple-sign-in-button.ts @@ -25,8 +25,11 @@ import { AppleLogoComponent } from "../../components/logos/apple"; selector: "fui-apple-sign-in-button", standalone: true, imports: [CommonModule, OAuthButtonComponent, AppleLogoComponent], + host: { + style: "display: block;", + }, template: ` - + {{ signInWithAppleLabel() }} @@ -35,7 +38,8 @@ import { AppleLogoComponent } from "../../components/logos/apple"; export class AppleSignInButtonComponent { ui = injectUI(); signInWithAppleLabel = injectTranslation("labels", "signInWithApple"); - + themed = input(false); + private defaultProvider = new OAuthProvider("apple.com"); provider = input(); diff --git a/packages/angular/src/lib/auth/oauth/facebook-sign-in-button.ts b/packages/angular/src/lib/auth/oauth/facebook-sign-in-button.ts index 8a1be2f17..d9c84fb15 100644 --- a/packages/angular/src/lib/auth/oauth/facebook-sign-in-button.ts +++ b/packages/angular/src/lib/auth/oauth/facebook-sign-in-button.ts @@ -25,8 +25,11 @@ import { FacebookLogoComponent } from "../../components/logos/facebook"; selector: "fui-facebook-sign-in-button", standalone: true, imports: [CommonModule, OAuthButtonComponent, FacebookLogoComponent], + host: { + style: "display: block;", + }, template: ` - + {{ signInWithFacebookLabel() }} @@ -35,7 +38,8 @@ import { FacebookLogoComponent } from "../../components/logos/facebook"; export class FacebookSignInButtonComponent { ui = injectUI(); signInWithFacebookLabel = injectTranslation("labels", "signInWithFacebook"); - + themed = input(false); + private defaultProvider = new FacebookAuthProvider(); provider = input(); diff --git a/packages/angular/src/lib/auth/oauth/github-sign-in-button.spec.ts b/packages/angular/src/lib/auth/oauth/github-sign-in-button.spec.ts index 955d06f68..6686fcf59 100644 --- a/packages/angular/src/lib/auth/oauth/github-sign-in-button.spec.ts +++ b/packages/angular/src/lib/auth/oauth/github-sign-in-button.spec.ts @@ -17,19 +17,19 @@ import { render, screen } from "@testing-library/angular"; import { Component } from "@angular/core"; -import { GithubSignInButtonComponent } from "./github-sign-in-button"; +import { GitHubSignInButtonComponent } from "./github-sign-in-button"; @Component({ template: ``, standalone: true, - imports: [GithubSignInButtonComponent], + imports: [GitHubSignInButtonComponent], }) class TestGithubSignInButtonHostComponent {} @Component({ template: ``, standalone: true, - imports: [GithubSignInButtonComponent], + imports: [GitHubSignInButtonComponent], }) class TestGithubSignInButtonWithCustomProviderHostComponent { customProvider = { providerId: "custom.github.com" }; diff --git a/packages/angular/src/lib/auth/oauth/github-sign-in-button.ts b/packages/angular/src/lib/auth/oauth/github-sign-in-button.ts index d76cf686e..6e253e175 100644 --- a/packages/angular/src/lib/auth/oauth/github-sign-in-button.ts +++ b/packages/angular/src/lib/auth/oauth/github-sign-in-button.ts @@ -25,15 +25,19 @@ import { GithubLogoComponent } from "../../components/logos/github"; selector: "fui-github-sign-in-button", standalone: true, imports: [CommonModule, OAuthButtonComponent, GithubLogoComponent], + host: { + style: "display: block;", + }, template: ` - + - {{ signInWithGithubLabel() }} + {{ signInWithGitHubLabel() }} `, }) -export class GithubSignInButtonComponent { - signInWithGithubLabel = injectTranslation("labels", "signInWithGithub"); +export class GitHubSignInButtonComponent { + signInWithGitHubLabel = injectTranslation("labels", "signInWithGitHub"); + themed = input(false); private defaultProvider = new GithubAuthProvider(); diff --git a/packages/angular/src/lib/auth/oauth/google-sign-in-button.ts b/packages/angular/src/lib/auth/oauth/google-sign-in-button.ts index cece30b49..93c5959ef 100644 --- a/packages/angular/src/lib/auth/oauth/google-sign-in-button.ts +++ b/packages/angular/src/lib/auth/oauth/google-sign-in-button.ts @@ -25,8 +25,11 @@ import { GoogleLogoComponent } from "../../components/logos/google"; selector: "fui-google-sign-in-button", standalone: true, imports: [CommonModule, OAuthButtonComponent, GoogleLogoComponent], + host: { + style: "display: block;", + }, template: ` - + {{ signInWithGoogleLabel() }} @@ -35,7 +38,8 @@ import { GoogleLogoComponent } from "../../components/logos/google"; export class GoogleSignInButtonComponent { ui = injectUI(); signInWithGoogleLabel = injectTranslation("labels", "signInWithGoogle"); - + themed = input(false); + private defaultProvider = new GoogleAuthProvider(); provider = input(); diff --git a/packages/angular/src/lib/auth/oauth/microsoft-sign-in-button.ts b/packages/angular/src/lib/auth/oauth/microsoft-sign-in-button.ts index e2b05039e..5acc7f25b 100644 --- a/packages/angular/src/lib/auth/oauth/microsoft-sign-in-button.ts +++ b/packages/angular/src/lib/auth/oauth/microsoft-sign-in-button.ts @@ -25,8 +25,11 @@ import { MicrosoftLogoComponent } from "../../components/logos/microsoft"; selector: "fui-microsoft-sign-in-button", standalone: true, imports: [CommonModule, OAuthButtonComponent, MicrosoftLogoComponent], + host: { + style: "display: block;", + }, template: ` - + {{ signInWithMicrosoftLabel() }} @@ -34,7 +37,8 @@ import { MicrosoftLogoComponent } from "../../components/logos/microsoft"; }) export class MicrosoftSignInButtonComponent { signInWithMicrosoftLabel = injectTranslation("labels", "signInWithMicrosoft"); - + themed = input(false); + private defaultProvider = new OAuthProvider("microsoft.com"); provider = input(); diff --git a/packages/angular/src/lib/auth/oauth/oauth-button.ts b/packages/angular/src/lib/auth/oauth/oauth-button.ts index a4cc149cc..727c2c5f0 100644 --- a/packages/angular/src/lib/auth/oauth/oauth-button.ts +++ b/packages/angular/src/lib/auth/oauth/oauth-button.ts @@ -25,6 +25,9 @@ import { FirebaseUIError, signInWithProvider, getTranslation } from "@invertase/ selector: "fui-oauth-button", standalone: true, imports: [CommonModule, ButtonComponent], + host: { + style: "display: block;", + }, template: `
diff --git a/packages/angular/src/lib/auth/screens/phone-auth-screen.ts b/packages/angular/src/lib/auth/screens/phone-auth-screen.ts index d4ae5c7bc..64ed89364 100644 --- a/packages/angular/src/lib/auth/screens/phone-auth-screen.ts +++ b/packages/angular/src/lib/auth/screens/phone-auth-screen.ts @@ -32,6 +32,9 @@ import { UserCredential } from "@angular/fire/auth"; @Component({ selector: "fui-phone-auth-screen", standalone: true, + host: { + style: "display: block;", + }, imports: [ CommonModule, CardComponent, diff --git a/packages/angular/src/lib/auth/screens/sign-in-auth-screen.ts b/packages/angular/src/lib/auth/screens/sign-in-auth-screen.ts index 431c4ff53..02efd57b2 100644 --- a/packages/angular/src/lib/auth/screens/sign-in-auth-screen.ts +++ b/packages/angular/src/lib/auth/screens/sign-in-auth-screen.ts @@ -32,6 +32,9 @@ import { UserCredential } from "@angular/fire/auth"; @Component({ selector: "fui-sign-in-auth-screen", standalone: true, + host: { + style: "display: block;", + }, imports: [ CommonModule, CardComponent, diff --git a/packages/angular/src/lib/auth/screens/sign-up-auth-screen.ts b/packages/angular/src/lib/auth/screens/sign-up-auth-screen.ts index 98514213b..48e16a14e 100644 --- a/packages/angular/src/lib/auth/screens/sign-up-auth-screen.ts +++ b/packages/angular/src/lib/auth/screens/sign-up-auth-screen.ts @@ -33,6 +33,9 @@ import { @Component({ selector: "fui-sign-up-auth-screen", standalone: true, + host: { + style: "display: block;", + }, imports: [ CommonModule, CardComponent, diff --git a/packages/angular/src/lib/components/content.ts b/packages/angular/src/lib/components/content.ts index 0b9d1d0f2..ce3ddc4b9 100644 --- a/packages/angular/src/lib/components/content.ts +++ b/packages/angular/src/lib/components/content.ts @@ -22,6 +22,9 @@ import { injectTranslation } from "../provider"; selector: "fui-content", standalone: true, imports: [DividerComponent], + host: { + style: "display: block;", + }, template: `
diff --git a/packages/angular/src/lib/components/country-selector.ts b/packages/angular/src/lib/components/country-selector.ts index 8146461d9..12e597b81 100644 --- a/packages/angular/src/lib/components/country-selector.ts +++ b/packages/angular/src/lib/components/country-selector.ts @@ -24,6 +24,9 @@ import { injectCountries, injectDefaultCountry } from "../provider"; selector: "fui-country-selector", standalone: true, imports: [CommonModule, FormsModule], + host: { + style: "display: block;", + }, template: `
diff --git a/packages/angular/src/lib/components/divider.ts b/packages/angular/src/lib/components/divider.ts index 0bd736276..40b3e43db 100644 --- a/packages/angular/src/lib/components/divider.ts +++ b/packages/angular/src/lib/components/divider.ts @@ -21,6 +21,9 @@ import { CommonModule } from "@angular/common"; selector: "fui-divider", standalone: true, imports: [CommonModule], + host: { + style: "display: block;", + }, template: `
diff --git a/packages/angular/src/lib/components/form.ts b/packages/angular/src/lib/components/form.ts index 77a68a9d7..034cb860c 100644 --- a/packages/angular/src/lib/components/form.ts +++ b/packages/angular/src/lib/components/form.ts @@ -5,6 +5,9 @@ import { ButtonComponent } from "./button"; @Component({ selector: "fui-form-metadata", standalone: true, + host: { + style: "display: block;", + }, template: ` @if (field().state.meta.isTouched && errors().length > 0) {
@@ -28,6 +31,9 @@ export class FormMetadataComponent { selector: "fui-form-input", standalone: true, imports: [FormMetadataComponent], + host: { + style: "display: block;", + }, template: `
- + > + +
diff --git a/packages/angular/src/lib/auth/forms/phone-auth-form.ts b/packages/angular/src/lib/auth/forms/phone-auth-form.ts index e3ac7165d..acd988c2c 100644 --- a/packages/angular/src/lib/auth/forms/phone-auth-form.ts +++ b/packages/angular/src/lib/auth/forms/phone-auth-form.ts @@ -56,14 +56,15 @@ import { template: `
- + > + +
From 919515b4b740964cf2f3c12a770f5f5a66390e5d Mon Sep 17 00:00:00 2001 From: Elliot Hesp Date: Mon, 10 Nov 2025 11:52:47 +0000 Subject: [PATCH 30/32] chore(angular): Add redirect handlers to example app --- ...mail-link-auth-screen-w-oauth.component.ts | 19 ++++++++++++++++--- .../email-link-auth-screen.component.ts | 19 ++++++++++++++++--- .../forgot-password-auth-screen.component.ts | 8 ++++++-- .../oauth-screen/oauth-screen.component.ts | 12 ++++++++++-- .../phone-auth-screen-w-oauth.component.ts | 15 ++++++++++++--- .../phone-auth-screen.component.ts | 15 ++++++++++++--- .../sign-in-auth-screen-w-oauth.component.ts | 15 ++++++++++++--- .../sign-in-auth-screen.component.ts | 15 ++++++++++++--- .../sign-up-auth-screen-w-oauth.component.ts | 15 ++++++++++++--- .../sign-up-auth-screen.component.ts | 15 ++++++++++++--- .../lib/auth/oauth/apple-sign-in-button.ts | 2 +- .../lib/auth/oauth/facebook-sign-in-button.ts | 2 +- .../lib/auth/oauth/google-sign-in-button.ts | 4 ++-- .../auth/oauth/microsoft-sign-in-button.ts | 2 +- .../lib/auth/oauth/twitter-sign-in-button.ts | 2 +- .../src/lib/components/country-selector.ts | 2 +- 16 files changed, 127 insertions(+), 35 deletions(-) diff --git a/examples/angular/src/app/screens/email-link-auth-screen-w-oauth/email-link-auth-screen-w-oauth.component.ts b/examples/angular/src/app/screens/email-link-auth-screen-w-oauth/email-link-auth-screen-w-oauth.component.ts index a0921dedd..4755c4916 100644 --- a/examples/angular/src/app/screens/email-link-auth-screen-w-oauth/email-link-auth-screen-w-oauth.component.ts +++ b/examples/angular/src/app/screens/email-link-auth-screen-w-oauth/email-link-auth-screen-w-oauth.component.ts @@ -14,19 +14,32 @@ * limitations under the License. */ -import { Component } from "@angular/core"; +import { Component, inject } from "@angular/core"; import { CommonModule } from "@angular/common"; import { EmailLinkAuthScreenComponent, GoogleSignInButtonComponent } from "@invertase/firebaseui-angular"; +import { UserCredential } from "firebase/auth"; +import { Router } from "@angular/router"; @Component({ selector: "app-email-link-auth-screen-w-oauth", standalone: true, imports: [CommonModule, EmailLinkAuthScreenComponent, GoogleSignInButtonComponent], template: ` - + `, styles: [], }) -export class EmailLinkAuthScreenWithOAuthComponent {} +export class EmailLinkAuthScreenWithOAuthComponent { + private router = inject(Router); + + onEmailSent() { + alert("email sent - please check your email"); + } + + onSignIn(credential: UserCredential) { + console.log("sign in", credential); + this.router.navigate(["/"]); + } +} diff --git a/examples/angular/src/app/screens/email-link-auth-screen/email-link-auth-screen.component.ts b/examples/angular/src/app/screens/email-link-auth-screen/email-link-auth-screen.component.ts index 6489ce99e..d17d77aa1 100644 --- a/examples/angular/src/app/screens/email-link-auth-screen/email-link-auth-screen.component.ts +++ b/examples/angular/src/app/screens/email-link-auth-screen/email-link-auth-screen.component.ts @@ -14,15 +14,28 @@ * limitations under the License. */ -import { Component } from "@angular/core"; +import { Component, inject } from "@angular/core"; import { CommonModule } from "@angular/common"; import { EmailLinkAuthScreenComponent } from "@invertase/firebaseui-angular"; +import { UserCredential } from "firebase/auth"; +import { Router } from "@angular/router"; @Component({ selector: "app-email-link-auth-screen", standalone: true, imports: [CommonModule, EmailLinkAuthScreenComponent], - template: ` `, + template: ` `, styles: [], }) -export class EmailLinkAuthScreenWrapperComponent {} +export class EmailLinkAuthScreenWrapperComponent { + private router = inject(Router); + + onEmailSent() { + alert("email sent - please check your email"); + } + + onSignIn(credential: UserCredential) { + console.log("sign in", credential); + this.router.navigate(["/"]); + } +} diff --git a/examples/angular/src/app/screens/forgot-password-auth-screen/forgot-password-auth-screen.component.ts b/examples/angular/src/app/screens/forgot-password-auth-screen/forgot-password-auth-screen.component.ts index 6f26e4ec1..ceb20bc35 100644 --- a/examples/angular/src/app/screens/forgot-password-auth-screen/forgot-password-auth-screen.component.ts +++ b/examples/angular/src/app/screens/forgot-password-auth-screen/forgot-password-auth-screen.component.ts @@ -22,7 +22,11 @@ import { ForgotPasswordAuthScreenComponent } from "@invertase/firebaseui-angular selector: "app-forgot-password-auth-screen", standalone: true, imports: [CommonModule, ForgotPasswordAuthScreenComponent], - template: ` `, + template: ` `, styles: [], }) -export class ForgotPasswordAuthScreenWrapperComponent {} +export class ForgotPasswordAuthScreenWrapperComponent { + onPasswordSent() { + alert("password reset email sent - please check your email"); + } +} diff --git a/examples/angular/src/app/screens/oauth-screen/oauth-screen.component.ts b/examples/angular/src/app/screens/oauth-screen/oauth-screen.component.ts index b878f91bf..8834e82f7 100644 --- a/examples/angular/src/app/screens/oauth-screen/oauth-screen.component.ts +++ b/examples/angular/src/app/screens/oauth-screen/oauth-screen.component.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Component, signal } from "@angular/core"; +import { Component, inject, signal } from "@angular/core"; import { CommonModule } from "@angular/common"; import { OAuthScreenComponent, @@ -25,6 +25,8 @@ import { MicrosoftSignInButtonComponent, TwitterSignInButtonComponent, } from "@invertase/firebaseui-angular"; +import { UserCredential } from "firebase/auth"; +import { Router } from "@angular/router"; @Component({ selector: "app-oauth-screen", @@ -40,7 +42,7 @@ import { TwitterSignInButtonComponent, ], template: ` - + @@ -59,4 +61,10 @@ import { }) export class OAuthScreenWrapperComponent { themed = signal(false); + private router = inject(Router); + + onSignIn(credential: UserCredential) { + console.log("sign in", credential); + this.router.navigate(["/"]); + } } diff --git a/examples/angular/src/app/screens/phone-auth-screen-w-oauth/phone-auth-screen-w-oauth.component.ts b/examples/angular/src/app/screens/phone-auth-screen-w-oauth/phone-auth-screen-w-oauth.component.ts index 3a76fa9ff..34e957ce5 100644 --- a/examples/angular/src/app/screens/phone-auth-screen-w-oauth/phone-auth-screen-w-oauth.component.ts +++ b/examples/angular/src/app/screens/phone-auth-screen-w-oauth/phone-auth-screen-w-oauth.component.ts @@ -14,16 +14,18 @@ * limitations under the License. */ -import { Component } from "@angular/core"; +import { Component, inject } from "@angular/core"; import { CommonModule } from "@angular/common"; import { PhoneAuthScreenComponent, GoogleSignInButtonComponent, ContentComponent } from "@invertase/firebaseui-angular"; +import { UserCredential } from "firebase/auth"; +import { Router } from "@angular/router"; @Component({ selector: "app-phone-auth-screen-w-oauth", standalone: true, imports: [CommonModule, PhoneAuthScreenComponent, GoogleSignInButtonComponent, ContentComponent], template: ` - + @@ -31,4 +33,11 @@ import { PhoneAuthScreenComponent, GoogleSignInButtonComponent, ContentComponent `, styles: [], }) -export class PhoneAuthScreenWithOAuthComponent {} +export class PhoneAuthScreenWithOAuthComponent { + private router = inject(Router); + + onSignIn(credential: UserCredential) { + console.log("sign in", credential); + this.router.navigate(["/"]); + } +} diff --git a/examples/angular/src/app/screens/phone-auth-screen/phone-auth-screen.component.ts b/examples/angular/src/app/screens/phone-auth-screen/phone-auth-screen.component.ts index 2dd4fab2c..8ef3c8f4c 100644 --- a/examples/angular/src/app/screens/phone-auth-screen/phone-auth-screen.component.ts +++ b/examples/angular/src/app/screens/phone-auth-screen/phone-auth-screen.component.ts @@ -14,15 +14,24 @@ * limitations under the License. */ -import { Component } from "@angular/core"; +import { Component, inject } from "@angular/core"; import { CommonModule } from "@angular/common"; import { PhoneAuthScreenComponent } from "@invertase/firebaseui-angular"; +import { UserCredential } from "firebase/auth"; +import { Router } from "@angular/router"; @Component({ selector: "app-phone-auth-screen", standalone: true, imports: [CommonModule, PhoneAuthScreenComponent], - template: ` `, + template: ` `, styles: [], }) -export class PhoneAuthScreenWrapperComponent {} +export class PhoneAuthScreenWrapperComponent { + private router = inject(Router); + + onSignIn(credential: UserCredential) { + console.log("sign in", credential); + this.router.navigate(["/"]); + } +} diff --git a/examples/angular/src/app/screens/sign-in-auth-screen-w-oauth/sign-in-auth-screen-w-oauth.component.ts b/examples/angular/src/app/screens/sign-in-auth-screen-w-oauth/sign-in-auth-screen-w-oauth.component.ts index 4c17c1d70..21aa5a077 100644 --- a/examples/angular/src/app/screens/sign-in-auth-screen-w-oauth/sign-in-auth-screen-w-oauth.component.ts +++ b/examples/angular/src/app/screens/sign-in-auth-screen-w-oauth/sign-in-auth-screen-w-oauth.component.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Component } from "@angular/core"; +import { Component, inject } from "@angular/core"; import { CommonModule } from "@angular/common"; import { SignInAuthScreenComponent, @@ -26,6 +26,8 @@ import { MicrosoftSignInButtonComponent, TwitterSignInButtonComponent, } from "@invertase/firebaseui-angular"; +import { UserCredential } from "firebase/auth"; +import { Router } from "@angular/router"; @Component({ selector: "app-sign-in-auth-screen-w-oauth", @@ -42,7 +44,7 @@ import { TwitterSignInButtonComponent, ], template: ` - + @@ -55,4 +57,11 @@ import { `, styles: [], }) -export class SignInAuthScreenWithOAuthComponent {} +export class SignInAuthScreenWithOAuthComponent { + private router = inject(Router); + + onSignIn(credential: UserCredential) { + console.log("sign in", credential); + this.router.navigate(["/"]); + } +} diff --git a/examples/angular/src/app/screens/sign-in-auth-screen/sign-in-auth-screen.component.ts b/examples/angular/src/app/screens/sign-in-auth-screen/sign-in-auth-screen.component.ts index 8b1e7d458..11d4985e3 100644 --- a/examples/angular/src/app/screens/sign-in-auth-screen/sign-in-auth-screen.component.ts +++ b/examples/angular/src/app/screens/sign-in-auth-screen/sign-in-auth-screen.component.ts @@ -14,15 +14,24 @@ * limitations under the License. */ -import { Component } from "@angular/core"; +import { Component, inject } from "@angular/core"; import { CommonModule } from "@angular/common"; import { SignInAuthScreenComponent } from "@invertase/firebaseui-angular"; +import { UserCredential } from "firebase/auth"; +import { Router } from "@angular/router"; @Component({ selector: "app-sign-in-auth-screen", standalone: true, imports: [CommonModule, SignInAuthScreenComponent], - template: ` `, + template: ` `, styles: [], }) -export class SignInAuthScreenWrapperComponent {} +export class SignInAuthScreenWrapperComponent { + private router = inject(Router); + + onSignIn(credential: UserCredential) { + console.log("sign in", credential); + this.router.navigate(["/"]); + } +} diff --git a/examples/angular/src/app/screens/sign-up-auth-screen-w-oauth/sign-up-auth-screen-w-oauth.component.ts b/examples/angular/src/app/screens/sign-up-auth-screen-w-oauth/sign-up-auth-screen-w-oauth.component.ts index 0640259b7..67087e1da 100644 --- a/examples/angular/src/app/screens/sign-up-auth-screen-w-oauth/sign-up-auth-screen-w-oauth.component.ts +++ b/examples/angular/src/app/screens/sign-up-auth-screen-w-oauth/sign-up-auth-screen-w-oauth.component.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Component } from "@angular/core"; +import { Component, inject } from "@angular/core"; import { CommonModule } from "@angular/common"; import { SignUpAuthScreenComponent, @@ -26,6 +26,8 @@ import { MicrosoftSignInButtonComponent, TwitterSignInButtonComponent, } from "@invertase/firebaseui-angular"; +import { UserCredential } from "firebase/auth"; +import { Router } from "@angular/router"; @Component({ selector: "app-sign-up-auth-screen-w-oauth", @@ -42,7 +44,7 @@ import { TwitterSignInButtonComponent, ], template: ` - + @@ -55,4 +57,11 @@ import { `, styles: [], }) -export class SignUpAuthScreenWithOAuthComponent {} +export class SignUpAuthScreenWithOAuthComponent { + private router = inject(Router); + + onSignUp(credential: UserCredential) { + console.log("sign up", credential); + this.router.navigate(["/"]); + } +} diff --git a/examples/angular/src/app/screens/sign-up-auth-screen/sign-up-auth-screen.component.ts b/examples/angular/src/app/screens/sign-up-auth-screen/sign-up-auth-screen.component.ts index 4f9b43493..72db02e3f 100644 --- a/examples/angular/src/app/screens/sign-up-auth-screen/sign-up-auth-screen.component.ts +++ b/examples/angular/src/app/screens/sign-up-auth-screen/sign-up-auth-screen.component.ts @@ -14,15 +14,24 @@ * limitations under the License. */ -import { Component } from "@angular/core"; +import { Component, inject } from "@angular/core"; import { CommonModule } from "@angular/common"; import { SignUpAuthScreenComponent } from "@invertase/firebaseui-angular"; +import { Router } from "@angular/router"; +import { UserCredential } from "firebase/auth"; @Component({ selector: "app-sign-up-auth-screen", standalone: true, imports: [CommonModule, SignUpAuthScreenComponent], - template: ` `, + template: ` `, styles: [], }) -export class SignUpAuthScreenWrapperComponent {} +export class SignUpAuthScreenWrapperComponent { + private router = inject(Router); + + onSignUp(credential: UserCredential) { + console.log("sign up", credential); + this.router.navigate(["/"]); + } +} diff --git a/packages/angular/src/lib/auth/oauth/apple-sign-in-button.ts b/packages/angular/src/lib/auth/oauth/apple-sign-in-button.ts index 3ef5b5158..8681b5765 100644 --- a/packages/angular/src/lib/auth/oauth/apple-sign-in-button.ts +++ b/packages/angular/src/lib/auth/oauth/apple-sign-in-button.ts @@ -39,7 +39,7 @@ export class AppleSignInButtonComponent { ui = injectUI(); signInWithAppleLabel = injectTranslation("labels", "signInWithApple"); themed = input(false); - + private defaultProvider = new OAuthProvider("apple.com"); provider = input(); diff --git a/packages/angular/src/lib/auth/oauth/facebook-sign-in-button.ts b/packages/angular/src/lib/auth/oauth/facebook-sign-in-button.ts index d9c84fb15..06443abe2 100644 --- a/packages/angular/src/lib/auth/oauth/facebook-sign-in-button.ts +++ b/packages/angular/src/lib/auth/oauth/facebook-sign-in-button.ts @@ -39,7 +39,7 @@ export class FacebookSignInButtonComponent { ui = injectUI(); signInWithFacebookLabel = injectTranslation("labels", "signInWithFacebook"); themed = input(false); - + private defaultProvider = new FacebookAuthProvider(); provider = input(); diff --git a/packages/angular/src/lib/auth/oauth/google-sign-in-button.ts b/packages/angular/src/lib/auth/oauth/google-sign-in-button.ts index 93c5959ef..a8201b0a6 100644 --- a/packages/angular/src/lib/auth/oauth/google-sign-in-button.ts +++ b/packages/angular/src/lib/auth/oauth/google-sign-in-button.ts @@ -38,8 +38,8 @@ import { GoogleLogoComponent } from "../../components/logos/google"; export class GoogleSignInButtonComponent { ui = injectUI(); signInWithGoogleLabel = injectTranslation("labels", "signInWithGoogle"); - themed = input(false); - + themed = input(false); + private defaultProvider = new GoogleAuthProvider(); provider = input(); diff --git a/packages/angular/src/lib/auth/oauth/microsoft-sign-in-button.ts b/packages/angular/src/lib/auth/oauth/microsoft-sign-in-button.ts index 5acc7f25b..641aeeb98 100644 --- a/packages/angular/src/lib/auth/oauth/microsoft-sign-in-button.ts +++ b/packages/angular/src/lib/auth/oauth/microsoft-sign-in-button.ts @@ -38,7 +38,7 @@ import { MicrosoftLogoComponent } from "../../components/logos/microsoft"; export class MicrosoftSignInButtonComponent { signInWithMicrosoftLabel = injectTranslation("labels", "signInWithMicrosoft"); themed = input(false); - + private defaultProvider = new OAuthProvider("microsoft.com"); provider = input(); diff --git a/packages/angular/src/lib/auth/oauth/twitter-sign-in-button.ts b/packages/angular/src/lib/auth/oauth/twitter-sign-in-button.ts index d9f81ae7e..db486aef9 100644 --- a/packages/angular/src/lib/auth/oauth/twitter-sign-in-button.ts +++ b/packages/angular/src/lib/auth/oauth/twitter-sign-in-button.ts @@ -38,7 +38,7 @@ import { TwitterLogoComponent } from "../../components/logos/twitter"; export class TwitterSignInButtonComponent { signInWithTwitterLabel = injectTranslation("labels", "signInWithTwitter"); themed = input(false); - + private defaultProvider = new TwitterAuthProvider(); provider = input(); diff --git a/packages/angular/src/lib/components/country-selector.ts b/packages/angular/src/lib/components/country-selector.ts index f4b9b1583..4ac72704c 100644 --- a/packages/angular/src/lib/components/country-selector.ts +++ b/packages/angular/src/lib/components/country-selector.ts @@ -38,7 +38,7 @@ import { injectCountries, injectDefaultCountry } from "../provider"; [ngModel]="selected().code" (ngModelChange)="handleCountryChange($event)" > - @for (country of countries(); track $index;) { + @for (country of countries(); track $index) { } From 510bde73f3f38546c40e54533f12d9744cb53ff2 Mon Sep 17 00:00:00 2001 From: Elliot Hesp Date: Mon, 10 Nov 2025 11:57:23 +0000 Subject: [PATCH 31/32] chore: Formatting --- .../email-link-auth-screen-w-oauth.component.ts | 2 +- .../email-link-auth-screen/email-link-auth-screen.component.ts | 2 +- .../src/app/screens/oauth-screen/oauth-screen.component.ts | 2 +- .../phone-auth-screen-w-oauth.component.ts | 2 +- .../screens/phone-auth-screen/phone-auth-screen.component.ts | 2 +- .../sign-in-auth-screen-w-oauth.component.ts | 2 +- .../sign-in-auth-screen/sign-in-auth-screen.component.ts | 2 +- .../sign-up-auth-screen-w-oauth.component.ts | 2 +- .../sign-up-auth-screen/sign-up-auth-screen.component.ts | 2 +- examples/angular/src/app/services/user.service.ts | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/examples/angular/src/app/screens/email-link-auth-screen-w-oauth/email-link-auth-screen-w-oauth.component.ts b/examples/angular/src/app/screens/email-link-auth-screen-w-oauth/email-link-auth-screen-w-oauth.component.ts index 4755c4916..331551c38 100644 --- a/examples/angular/src/app/screens/email-link-auth-screen-w-oauth/email-link-auth-screen-w-oauth.component.ts +++ b/examples/angular/src/app/screens/email-link-auth-screen-w-oauth/email-link-auth-screen-w-oauth.component.ts @@ -17,7 +17,7 @@ import { Component, inject } from "@angular/core"; import { CommonModule } from "@angular/common"; import { EmailLinkAuthScreenComponent, GoogleSignInButtonComponent } from "@invertase/firebaseui-angular"; -import { UserCredential } from "firebase/auth"; +import type { UserCredential } from "firebase/auth"; import { Router } from "@angular/router"; @Component({ diff --git a/examples/angular/src/app/screens/email-link-auth-screen/email-link-auth-screen.component.ts b/examples/angular/src/app/screens/email-link-auth-screen/email-link-auth-screen.component.ts index d17d77aa1..55dcefb66 100644 --- a/examples/angular/src/app/screens/email-link-auth-screen/email-link-auth-screen.component.ts +++ b/examples/angular/src/app/screens/email-link-auth-screen/email-link-auth-screen.component.ts @@ -17,7 +17,7 @@ import { Component, inject } from "@angular/core"; import { CommonModule } from "@angular/common"; import { EmailLinkAuthScreenComponent } from "@invertase/firebaseui-angular"; -import { UserCredential } from "firebase/auth"; +import type { UserCredential } from "firebase/auth"; import { Router } from "@angular/router"; @Component({ diff --git a/examples/angular/src/app/screens/oauth-screen/oauth-screen.component.ts b/examples/angular/src/app/screens/oauth-screen/oauth-screen.component.ts index 8834e82f7..80690c2dd 100644 --- a/examples/angular/src/app/screens/oauth-screen/oauth-screen.component.ts +++ b/examples/angular/src/app/screens/oauth-screen/oauth-screen.component.ts @@ -25,7 +25,7 @@ import { MicrosoftSignInButtonComponent, TwitterSignInButtonComponent, } from "@invertase/firebaseui-angular"; -import { UserCredential } from "firebase/auth"; +import type { UserCredential } from "firebase/auth"; import { Router } from "@angular/router"; @Component({ diff --git a/examples/angular/src/app/screens/phone-auth-screen-w-oauth/phone-auth-screen-w-oauth.component.ts b/examples/angular/src/app/screens/phone-auth-screen-w-oauth/phone-auth-screen-w-oauth.component.ts index 34e957ce5..9fac00203 100644 --- a/examples/angular/src/app/screens/phone-auth-screen-w-oauth/phone-auth-screen-w-oauth.component.ts +++ b/examples/angular/src/app/screens/phone-auth-screen-w-oauth/phone-auth-screen-w-oauth.component.ts @@ -17,7 +17,7 @@ import { Component, inject } from "@angular/core"; import { CommonModule } from "@angular/common"; import { PhoneAuthScreenComponent, GoogleSignInButtonComponent, ContentComponent } from "@invertase/firebaseui-angular"; -import { UserCredential } from "firebase/auth"; +import type { UserCredential } from "firebase/auth"; import { Router } from "@angular/router"; @Component({ diff --git a/examples/angular/src/app/screens/phone-auth-screen/phone-auth-screen.component.ts b/examples/angular/src/app/screens/phone-auth-screen/phone-auth-screen.component.ts index 8ef3c8f4c..add17ca3f 100644 --- a/examples/angular/src/app/screens/phone-auth-screen/phone-auth-screen.component.ts +++ b/examples/angular/src/app/screens/phone-auth-screen/phone-auth-screen.component.ts @@ -17,7 +17,7 @@ import { Component, inject } from "@angular/core"; import { CommonModule } from "@angular/common"; import { PhoneAuthScreenComponent } from "@invertase/firebaseui-angular"; -import { UserCredential } from "firebase/auth"; +import type { UserCredential } from "firebase/auth"; import { Router } from "@angular/router"; @Component({ diff --git a/examples/angular/src/app/screens/sign-in-auth-screen-w-oauth/sign-in-auth-screen-w-oauth.component.ts b/examples/angular/src/app/screens/sign-in-auth-screen-w-oauth/sign-in-auth-screen-w-oauth.component.ts index 21aa5a077..dd0048828 100644 --- a/examples/angular/src/app/screens/sign-in-auth-screen-w-oauth/sign-in-auth-screen-w-oauth.component.ts +++ b/examples/angular/src/app/screens/sign-in-auth-screen-w-oauth/sign-in-auth-screen-w-oauth.component.ts @@ -26,7 +26,7 @@ import { MicrosoftSignInButtonComponent, TwitterSignInButtonComponent, } from "@invertase/firebaseui-angular"; -import { UserCredential } from "firebase/auth"; +import type { UserCredential } from "firebase/auth"; import { Router } from "@angular/router"; @Component({ diff --git a/examples/angular/src/app/screens/sign-in-auth-screen/sign-in-auth-screen.component.ts b/examples/angular/src/app/screens/sign-in-auth-screen/sign-in-auth-screen.component.ts index 11d4985e3..2475549dd 100644 --- a/examples/angular/src/app/screens/sign-in-auth-screen/sign-in-auth-screen.component.ts +++ b/examples/angular/src/app/screens/sign-in-auth-screen/sign-in-auth-screen.component.ts @@ -17,7 +17,7 @@ import { Component, inject } from "@angular/core"; import { CommonModule } from "@angular/common"; import { SignInAuthScreenComponent } from "@invertase/firebaseui-angular"; -import { UserCredential } from "firebase/auth"; +import type { UserCredential } from "firebase/auth"; import { Router } from "@angular/router"; @Component({ diff --git a/examples/angular/src/app/screens/sign-up-auth-screen-w-oauth/sign-up-auth-screen-w-oauth.component.ts b/examples/angular/src/app/screens/sign-up-auth-screen-w-oauth/sign-up-auth-screen-w-oauth.component.ts index 67087e1da..c172ead67 100644 --- a/examples/angular/src/app/screens/sign-up-auth-screen-w-oauth/sign-up-auth-screen-w-oauth.component.ts +++ b/examples/angular/src/app/screens/sign-up-auth-screen-w-oauth/sign-up-auth-screen-w-oauth.component.ts @@ -26,7 +26,7 @@ import { MicrosoftSignInButtonComponent, TwitterSignInButtonComponent, } from "@invertase/firebaseui-angular"; -import { UserCredential } from "firebase/auth"; +import type { UserCredential } from "firebase/auth"; import { Router } from "@angular/router"; @Component({ diff --git a/examples/angular/src/app/screens/sign-up-auth-screen/sign-up-auth-screen.component.ts b/examples/angular/src/app/screens/sign-up-auth-screen/sign-up-auth-screen.component.ts index 72db02e3f..6671ecba7 100644 --- a/examples/angular/src/app/screens/sign-up-auth-screen/sign-up-auth-screen.component.ts +++ b/examples/angular/src/app/screens/sign-up-auth-screen/sign-up-auth-screen.component.ts @@ -18,7 +18,7 @@ import { Component, inject } from "@angular/core"; import { CommonModule } from "@angular/common"; import { SignUpAuthScreenComponent } from "@invertase/firebaseui-angular"; import { Router } from "@angular/router"; -import { UserCredential } from "firebase/auth"; +import type { UserCredential } from "firebase/auth"; @Component({ selector: "app-sign-up-auth-screen", diff --git a/examples/angular/src/app/services/user.service.ts b/examples/angular/src/app/services/user.service.ts index 7fb342a4e..50548a8b1 100644 --- a/examples/angular/src/app/services/user.service.ts +++ b/examples/angular/src/app/services/user.service.ts @@ -16,7 +16,7 @@ import { Injectable, inject } from "@angular/core"; import { Auth, type User, authState } from "@angular/fire/auth"; -import { Observable } from "rxjs"; +import type { Observable } from "rxjs"; @Injectable({ providedIn: "root", From 824ca2319fd5de243de9646a1eef84721ae9da8f Mon Sep 17 00:00:00 2001 From: Elliot Hesp Date: Mon, 10 Nov 2025 12:21:21 +0000 Subject: [PATCH 32/32] fix(angular): Update tests with latest changes --- .../forms/mfa/sms-multi-factor-enrollment-form.spec.ts | 4 ++-- .../auth/forms/multi-factor-auth-enrollment-form.spec.ts | 8 ++++---- .../angular/src/lib/auth/forms/phone-auth-form.spec.ts | 8 ++++---- .../src/lib/auth/oauth/github-sign-in-button.spec.ts | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/angular/src/lib/auth/forms/mfa/sms-multi-factor-enrollment-form.spec.ts b/packages/angular/src/lib/auth/forms/mfa/sms-multi-factor-enrollment-form.spec.ts index c710bf450..5819cbd47 100644 --- a/packages/angular/src/lib/auth/forms/mfa/sms-multi-factor-enrollment-form.spec.ts +++ b/packages/angular/src/lib/auth/forms/mfa/sms-multi-factor-enrollment-form.spec.ts @@ -153,7 +153,7 @@ describe("", () => { }); it("should render phone number form initially", async () => { - await render(SmsMultiFactorEnrollmentFormComponent, { + const { container } = await render(SmsMultiFactorEnrollmentFormComponent, { imports: [ CommonModule, SmsMultiFactorEnrollmentFormComponent, @@ -167,7 +167,7 @@ describe("", () => { }); expect(screen.getByLabelText("Display Name")).toBeInTheDocument(); - expect(screen.getByLabelText("Phone Number")).toBeInTheDocument(); + expect(container.querySelector('input[name="phoneNumber"]')).toBeInTheDocument(); expect(screen.getByRole("button", { name: "Send Verification Code" })).toBeInTheDocument(); }); diff --git a/packages/angular/src/lib/auth/forms/multi-factor-auth-enrollment-form.spec.ts b/packages/angular/src/lib/auth/forms/multi-factor-auth-enrollment-form.spec.ts index 3161adc7a..6b2f32292 100644 --- a/packages/angular/src/lib/auth/forms/multi-factor-auth-enrollment-form.spec.ts +++ b/packages/angular/src/lib/auth/forms/multi-factor-auth-enrollment-form.spec.ts @@ -66,7 +66,7 @@ describe("", () => { }); it("should auto-select single hint when only one is provided", async () => { - await render(MultiFactorAuthEnrollmentFormComponent, { + const { container } = await render(MultiFactorAuthEnrollmentFormComponent, { imports: [ CommonModule, MultiFactorAuthEnrollmentFormComponent, @@ -83,11 +83,11 @@ describe("", () => { expect(screen.queryByRole("button", { name: "TOTP Verification" })).not.toBeInTheDocument(); expect(screen.getByLabelText("Display Name")).toBeInTheDocument(); - expect(screen.getByLabelText("Phone Number")).toBeInTheDocument(); + expect(container.querySelector('input[name="phoneNumber"]')).toBeInTheDocument(); }); it("should show SMS form when SMS hint is selected", async () => { - const { fixture } = await render(MultiFactorAuthEnrollmentFormComponent, { + const { fixture, container } = await render(MultiFactorAuthEnrollmentFormComponent, { imports: [ CommonModule, MultiFactorAuthEnrollmentFormComponent, @@ -105,7 +105,7 @@ describe("", () => { fixture.detectChanges(); expect(screen.getByLabelText("Display Name")).toBeInTheDocument(); - expect(screen.getByLabelText("Phone Number")).toBeInTheDocument(); + expect(container.querySelector('input[name="phoneNumber"]')).toBeInTheDocument(); expect(screen.getByRole("button", { name: "Send Verification Code" })).toBeInTheDocument(); }); diff --git a/packages/angular/src/lib/auth/forms/phone-auth-form.spec.ts b/packages/angular/src/lib/auth/forms/phone-auth-form.spec.ts index 1d55fe19f..0f0d15c1e 100644 --- a/packages/angular/src/lib/auth/forms/phone-auth-form.spec.ts +++ b/packages/angular/src/lib/auth/forms/phone-auth-form.spec.ts @@ -91,7 +91,7 @@ describe("", () => { }); it("should render phone number form initially", async () => { - await render(PhoneAuthFormComponent, { + const { container } = await render(PhoneAuthFormComponent, { imports: [ CommonModule, PhoneAuthFormComponent, @@ -104,7 +104,7 @@ describe("", () => { ], }); - expect(screen.getByLabelText("Phone Number")).toBeInTheDocument(); + expect(container.querySelector('input[name="phoneNumber"]')).toBeInTheDocument(); expect(screen.getByRole("button", { name: "Send Verification Code" })).toBeInTheDocument(); }); @@ -301,7 +301,7 @@ describe("", () => { }); it("should reset form when going back to phone number step", async () => { - const { fixture } = await render(PhoneAuthFormComponent, { + const { fixture, container } = await render(PhoneAuthFormComponent, { imports: [ CommonModule, PhoneAuthFormComponent, @@ -321,7 +321,7 @@ describe("", () => { component.verificationId.set(null); fixture.detectChanges(); - expect(screen.getByLabelText("Phone Number")).toBeInTheDocument(); + expect(container.querySelector('input[name="phoneNumber"]')).toBeInTheDocument(); expect(screen.queryByLabelText("Verification Code")).toBeNull(); }); }); diff --git a/packages/angular/src/lib/auth/oauth/github-sign-in-button.spec.ts b/packages/angular/src/lib/auth/oauth/github-sign-in-button.spec.ts index 6686fcf59..061e15e9a 100644 --- a/packages/angular/src/lib/auth/oauth/github-sign-in-button.spec.ts +++ b/packages/angular/src/lib/auth/oauth/github-sign-in-button.spec.ts @@ -43,7 +43,7 @@ describe("", () => { injectTranslation.mockImplementation((category: string, key: string) => { const mockTranslations: Record> = { labels: { - signInWithGithub: "Sign in with GitHub", + signInWithGitHub: "Sign in with GitHub", }, }; return () => mockTranslations[category]?.[key] || `${category}.${key}`;