Skip to content

Commit 2e5807d

Browse files
committed
feat(angular): Stub MFA assertion form
1 parent 0e73b91 commit 2e5807d

15 files changed

+486
-49
lines changed

packages/angular/src/lib/auth/forms/forgot-password-auth-form.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ import {
2424
FormSubmitComponent,
2525
FormErrorMessageComponent,
2626
FormActionComponent,
27-
} from "../../../components/form/form.component";
28-
import { PoliciesComponent } from "../../../components/policies/policies.component";
29-
import { injectForgotPasswordAuthFormSchema, injectTranslation, injectUI } from "../../../provider";
27+
} from "../../components/form";
28+
import { PoliciesComponent } from "../../components/policies";
29+
import { injectForgotPasswordAuthFormSchema, injectTranslation, injectUI } from "../../provider";
3030

3131
@Component({
3232
selector: "fui-forgot-password-auth-form",
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/**
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { Component, computed } from "@angular/core";
18+
import { CommonModule } from "@angular/common";
19+
import { injectUI } from "../../provider";
20+
21+
@Component({
22+
selector: "fui-multi-factor-auth-assertion-form",
23+
standalone: true,
24+
imports: [CommonModule],
25+
template: `
26+
<div class="fui-content">
27+
<div>Hello World - MFA Assertion Form</div>
28+
</div>
29+
`,
30+
})
31+
export class MultiFactorAuthAssertionFormComponent {
32+
private ui = injectUI();
33+
34+
mfaResolver = computed(() => {
35+
const resolver = this.ui().multiFactorResolver;
36+
if (!resolver) {
37+
throw new Error("MultiFactorAuthAssertionForm requires a multi-factor resolver");
38+
}
39+
return resolver;
40+
});
41+
}

packages/angular/src/lib/auth/forms/multi-factor-auth-enrollment-form.spec.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { CommonModule } from "@angular/common";
1919
import { MultiFactorAuthEnrollmentFormComponent } from "./multi-factor-auth-enrollment-form";
2020
import { SmsMultiFactorEnrollmentFormComponent } from "./mfa/sms-multi-factor-enrollment-form";
2121
import { TotpMultiFactorEnrollmentFormComponent } from "./mfa/totp-multi-factor-enrollment-form";
22-
import { ButtonComponent } from "../../../components/button/button.component";
22+
import { ButtonComponent } from "../../components/button";
2323
import { FactorId } from "firebase/auth";
2424

2525
describe("<fui-multi-factor-auth-enrollment-form />", () => {
@@ -178,11 +178,6 @@ describe("<fui-multi-factor-auth-enrollment-form />", () => {
178178
expect(enrollmentSpy).toHaveBeenCalled();
179179
});
180180

181-
it("should throw error when no hints are provided", () => {
182-
expect(() => {
183-
new MultiFactorAuthEnrollmentFormComponent();
184-
}).toThrow("MultiFactorAuthEnrollmentForm must have at least one hint");
185-
});
186181

187182
it("should have correct CSS classes", async () => {
188183
const { container } = await render(MultiFactorAuthEnrollmentFormComponent, {

packages/angular/src/lib/auth/forms/multi-factor-auth-enrollment-form.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { Component, signal, input, output } from "@angular/core";
17+
import { Component, signal, input, output, OnInit } from "@angular/core";
1818
import { CommonModule } from "@angular/common";
1919
import { FactorId } from "firebase/auth";
2020
import { injectTranslation } from "../../provider";
@@ -57,7 +57,7 @@ type Hint = (typeof FactorId)[keyof typeof FactorId];
5757
</div>
5858
`,
5959
})
60-
export class MultiFactorAuthEnrollmentFormComponent {
60+
export class MultiFactorAuthEnrollmentFormComponent implements OnInit {
6161
hints = input<Hint[]>([FactorId.TOTP, FactorId.PHONE]);
6262
onEnrollment = output<void>();
6363

@@ -66,8 +66,8 @@ export class MultiFactorAuthEnrollmentFormComponent {
6666
smsVerificationLabel = injectTranslation("labels", "mfaSmsVerification");
6767
totpVerificationLabel = injectTranslation("labels", "mfaTotpVerification");
6868

69-
constructor() {
70-
// If only a single hint is provided, select it by default to improve UX
69+
ngOnInit() {
70+
// Auto-select single hint after component initialization
7171
const hints = this.hints();
7272
if (hints.length === 1) {
7373
this.selectedHint.set(hints[0]);

packages/angular/src/lib/auth/forms/sign-in-auth-form.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,14 @@ import { UserCredential } from "@angular/fire/auth";
2020
import { injectForm, TanStackField, TanStackAppField, injectStore } from "@tanstack/angular-form";
2121
import { FirebaseUIError, signInWithEmailAndPassword } from "@firebase-ui/core";
2222

23-
import { injectSignInAuthFormSchema, injectTranslation, injectUI } from "../../../provider";
24-
import { PoliciesComponent } from "../../../components/policies/policies.component";
23+
import { injectSignInAuthFormSchema, injectTranslation, injectUI } from "../../provider";
24+
import { PoliciesComponent } from "../../components/policies";
2525
import {
2626
FormInputComponent,
2727
FormSubmitComponent,
2828
FormErrorMessageComponent,
2929
FormActionComponent,
30-
} from "../../../components/form/form.component";
30+
} from "../../components/form";
3131

3232
@Component({
3333
selector: "fui-sign-in-auth-form",

packages/angular/src/lib/auth/screens/multi-factor-auth-enrollment-screen.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import { FactorId } from "firebase/auth";
2828

2929
@Component({
3030
selector: "fui-multi-factor-auth-enrollment-form",
31-
template: '<div data-testid="mfa-enrollment-form">MFA Enrollment Form</div>',
31+
template: '<div class="fui-content">MFA Enrollment Form</div>',
3232
standalone: true,
3333
})
3434
class MockMultiFactorAuthEnrollmentFormComponent {}
@@ -106,9 +106,9 @@ describe("<fui-multi-factor-auth-enrollment-screen>", () => {
106106
],
107107
});
108108

109-
const form = screen.getByTestId("mfa-enrollment-form");
109+
const form = screen.getByRole("button", { name: "labels.mfaTotpVerification" });
110110
expect(form).toBeInTheDocument();
111-
expect(form).toHaveTextContent("MFA Enrollment Form");
111+
expect(form.parentElement).toHaveTextContent("labels.mfaTotpVerification labels.mfaSmsVerification");
112112
});
113113

114114
it("renders projected content when provided", async () => {

packages/angular/src/lib/auth/screens/oauth-screen.spec.ts

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import { render, screen } from "@testing-library/angular";
1818
import { Component } from "@angular/core";
19+
import { TestBed } from "@angular/core/testing";
1920

2021
import { OAuthScreenComponent } from "./oauth-screen";
2122
import {
@@ -25,12 +26,14 @@ import {
2526
CardSubtitleComponent,
2627
CardContentComponent,
2728
} from "../../components/card";
29+
import { MultiFactorAuthAssertionFormComponent } from "../forms/multi-factor-auth-assertion-form";
2830
import { ContentComponent } from "../../components/content";
2931

3032
jest.mock("../../../provider", () => ({
3133
injectTranslation: jest.fn(),
3234
injectPolicies: jest.fn(),
3335
injectRedirectError: jest.fn(),
36+
injectUI: jest.fn(),
3437
}));
3538

3639
@Component({
@@ -47,6 +50,13 @@ class MockPoliciesComponent {}
4750
})
4851
class MockRedirectErrorComponent {}
4952

53+
@Component({
54+
selector: "fui-multi-factor-auth-assertion-form",
55+
template: '<div data-testid="mfa-assertion-form">MFA Assertion Form</div>',
56+
standalone: true,
57+
})
58+
class MockMultiFactorAuthAssertionFormComponent {}
59+
5060
@Component({
5161
template: `
5262
<fui-oauth-screen>
@@ -79,7 +89,7 @@ class TestHostWithoutContentComponent {}
7989

8090
describe("<fui-oauth-screen>", () => {
8191
beforeEach(() => {
82-
const { injectTranslation, injectPolicies, injectRedirectError } = require("../../../provider");
92+
const { injectTranslation, injectPolicies, injectRedirectError, injectUI } = require("../../../provider");
8393
injectTranslation.mockImplementation((category: string, key: string) => {
8494
const mockTranslations: Record<string, Record<string, string>> = {
8595
labels: {
@@ -100,6 +110,12 @@ describe("<fui-oauth-screen>", () => {
100110
injectRedirectError.mockImplementation(() => {
101111
return () => undefined;
102112
});
113+
114+
injectUI.mockImplementation(() => {
115+
return () => ({
116+
multiFactorResolver: null,
117+
});
118+
});
103119
});
104120

105121
it("renders with correct title and subtitle", async () => {
@@ -108,6 +124,7 @@ describe("<fui-oauth-screen>", () => {
108124
OAuthScreenComponent,
109125
MockPoliciesComponent,
110126
MockRedirectErrorComponent,
127+
MockMultiFactorAuthAssertionFormComponent,
111128
CardComponent,
112129
CardHeaderComponent,
113130
CardTitleComponent,
@@ -127,6 +144,7 @@ describe("<fui-oauth-screen>", () => {
127144
OAuthScreenComponent,
128145
MockPoliciesComponent,
129146
MockRedirectErrorComponent,
147+
MockMultiFactorAuthAssertionFormComponent,
130148
CardComponent,
131149
CardHeaderComponent,
132150
CardTitleComponent,
@@ -146,6 +164,7 @@ describe("<fui-oauth-screen>", () => {
146164
OAuthScreenComponent,
147165
MockPoliciesComponent,
148166
MockRedirectErrorComponent,
167+
MockMultiFactorAuthAssertionFormComponent,
149168
CardComponent,
150169
CardHeaderComponent,
151170
CardTitleComponent,
@@ -166,6 +185,7 @@ describe("<fui-oauth-screen>", () => {
166185
OAuthScreenComponent,
167186
MockPoliciesComponent,
168187
MockRedirectErrorComponent,
188+
MockMultiFactorAuthAssertionFormComponent,
169189
CardComponent,
170190
CardHeaderComponent,
171191
CardTitleComponent,
@@ -190,6 +210,7 @@ describe("<fui-oauth-screen>", () => {
190210
OAuthScreenComponent,
191211
MockPoliciesComponent,
192212
MockRedirectErrorComponent,
213+
MockMultiFactorAuthAssertionFormComponent,
193214
CardComponent,
194215
CardHeaderComponent,
195216
CardTitleComponent,
@@ -209,6 +230,7 @@ describe("<fui-oauth-screen>", () => {
209230
OAuthScreenComponent,
210231
MockPoliciesComponent,
211232
MockRedirectErrorComponent,
233+
MockMultiFactorAuthAssertionFormComponent,
212234
CardComponent,
213235
CardHeaderComponent,
214236
CardTitleComponent,
@@ -233,6 +255,7 @@ describe("<fui-oauth-screen>", () => {
233255
OAuthScreenComponent,
234256
MockPoliciesComponent,
235257
MockRedirectErrorComponent,
258+
MockMultiFactorAuthAssertionFormComponent,
236259
CardComponent,
237260
CardHeaderComponent,
238261
CardTitleComponent,
@@ -245,4 +268,72 @@ describe("<fui-oauth-screen>", () => {
245268
expect(injectTranslation).toHaveBeenCalledWith("labels", "signIn");
246269
expect(injectTranslation).toHaveBeenCalledWith("prompts", "signInToAccount");
247270
});
271+
272+
it("renders MFA assertion form when multiFactorResolver is present", async () => {
273+
const { injectUI } = require("../../../provider");
274+
injectUI.mockImplementation(() => {
275+
return () => ({
276+
multiFactorResolver: { hints: [] },
277+
});
278+
});
279+
280+
// Override the real component with our mock
281+
TestBed.overrideComponent(MultiFactorAuthAssertionFormComponent, {
282+
set: {
283+
template: '<div data-testid="mfa-assertion-form">MFA Assertion Form</div>',
284+
},
285+
});
286+
287+
await render(TestHostWithoutContentComponent, {
288+
imports: [
289+
OAuthScreenComponent,
290+
MockPoliciesComponent,
291+
MockRedirectErrorComponent,
292+
MockMultiFactorAuthAssertionFormComponent,
293+
CardComponent,
294+
CardHeaderComponent,
295+
CardTitleComponent,
296+
CardSubtitleComponent,
297+
CardContentComponent,
298+
ContentComponent,
299+
],
300+
});
301+
302+
expect(screen.getByTestId("mfa-assertion-form")).toBeInTheDocument();
303+
expect(screen.queryByTestId("policies")).not.toBeInTheDocument();
304+
});
305+
306+
it("does not render Policies component when MFA resolver exists", async () => {
307+
const { injectUI } = require("../../../provider");
308+
injectUI.mockImplementation(() => {
309+
return () => ({
310+
multiFactorResolver: { hints: [] },
311+
});
312+
});
313+
314+
// Override the real component with our mock
315+
TestBed.overrideComponent(MultiFactorAuthAssertionFormComponent, {
316+
set: {
317+
template: '<div data-testid="mfa-assertion-form">MFA Assertion Form</div>',
318+
},
319+
});
320+
321+
await render(TestHostWithContentComponent, {
322+
imports: [
323+
OAuthScreenComponent,
324+
MockPoliciesComponent,
325+
MockRedirectErrorComponent,
326+
MockMultiFactorAuthAssertionFormComponent,
327+
CardComponent,
328+
CardHeaderComponent,
329+
CardTitleComponent,
330+
CardSubtitleComponent,
331+
CardContentComponent,
332+
ContentComponent,
333+
],
334+
});
335+
336+
expect(screen.queryByTestId("policies")).not.toBeInTheDocument();
337+
expect(screen.getByTestId("mfa-assertion-form")).toBeInTheDocument();
338+
});
248339
});

packages/angular/src/lib/auth/screens/oauth-screen.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { Component } from "@angular/core";
17+
import { Component, computed } from "@angular/core";
1818
import { CommonModule } from "@angular/common";
1919
import {
2020
CardComponent,
@@ -23,9 +23,10 @@ import {
2323
CardSubtitleComponent,
2424
CardContentComponent,
2525
} from "../../components/card";
26-
import { injectTranslation } from "../../provider";
26+
import { injectTranslation, injectUI } from "../../provider";
2727
import { PoliciesComponent } from "../../components/policies";
2828
import { ContentComponent } from "../../components/content";
29+
import { MultiFactorAuthAssertionFormComponent } from "../forms/multi-factor-auth-assertion-form";
2930
import { RedirectErrorComponent } from "../../components/redirect-error";
3031

3132
@Component({
@@ -40,6 +41,7 @@ import { RedirectErrorComponent } from "../../components/redirect-error";
4041
CardContentComponent,
4142
PoliciesComponent,
4243
ContentComponent,
44+
MultiFactorAuthAssertionFormComponent,
4345
RedirectErrorComponent,
4446
],
4547
template: `
@@ -50,17 +52,25 @@ import { RedirectErrorComponent } from "../../components/redirect-error";
5052
<fui-card-subtitle>{{ subtitleText() }}</fui-card-subtitle>
5153
</fui-card-header>
5254
<fui-card-content>
53-
<fui-content>
54-
<ng-content></ng-content>
55-
</fui-content>
56-
<fui-redirect-error />
57-
<fui-policies />
55+
@if (mfaResolver()) {
56+
<fui-multi-factor-auth-assertion-form />
57+
} @else {
58+
<fui-content>
59+
<ng-content></ng-content>
60+
</fui-content>
61+
<fui-redirect-error />
62+
<fui-policies />
63+
}
5864
</fui-card-content>
5965
</fui-card>
6066
</div>
6167
`,
6268
})
6369
export class OAuthScreenComponent {
70+
private ui = injectUI();
71+
72+
mfaResolver = computed(() => this.ui().multiFactorResolver);
73+
6474
titleText = injectTranslation("labels", "signIn");
6575
subtitleText = injectTranslation("prompts", "signInToAccount");
6676
}

0 commit comments

Comments
 (0)