Skip to content

Commit d50dbc4

Browse files
authored
Merge pull request #1242 from firebase/@invertase/bb-4
2 parents 7ce8629 + d3fc8c8 commit d50dbc4

34 files changed

+974
-163
lines changed

packages/angular/src/lib/auth/forms/mfa/sms-multi-factor-assertion-form.spec.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,4 +335,28 @@ describe("<fui-sms-multi-factor-assertion-verify-form>", () => {
335335
expect(onSuccessSpy).toHaveBeenCalled();
336336
});
337337
});
338+
339+
it("emits onSuccess with credential after successful verification", async () => {
340+
const mockCredential = { user: { uid: "sms-verify-user" } };
341+
signInWithMultiFactorAssertion.mockResolvedValue(mockCredential);
342+
343+
const { fixture } = await render(SmsMultiFactorAssertionVerifyFormComponent, {
344+
componentInputs: {
345+
verificationId: "test-verification-id",
346+
},
347+
imports: [SmsMultiFactorAssertionVerifyFormComponent],
348+
});
349+
350+
const onSuccessSpy = jest.fn();
351+
fixture.componentInstance.onSuccess.subscribe(onSuccessSpy);
352+
353+
const component = fixture.componentInstance;
354+
component.form.setFieldValue("verificationCode", "123456");
355+
component.form.setFieldValue("verificationId", "test-verification-id");
356+
await component.form.handleSubmit();
357+
358+
await waitFor(() => {
359+
expect(onSuccessSpy).toHaveBeenCalledWith(mockCredential);
360+
});
361+
});
338362
});

packages/angular/src/lib/auth/forms/mfa/sms-multi-factor-assertion-form.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,9 @@ import {
2323
injectTranslation,
2424
injectUI,
2525
} from "../../../provider";
26-
import { RecaptchaVerifier } from "@angular/fire/auth";
2726
import { FormInputComponent, FormSubmitComponent, FormErrorMessageComponent } from "../../../components/form";
2827
import { FirebaseUIError, verifyPhoneNumber, signInWithMultiFactorAssertion } from "@firebase-ui/core";
29-
import { PhoneAuthProvider, PhoneMultiFactorGenerator, type MultiFactorInfo } from "firebase/auth";
28+
import { PhoneAuthProvider, PhoneMultiFactorGenerator, type UserCredential, type MultiFactorInfo } from "firebase/auth";
3029

3130
type PhoneMultiFactorInfo = MultiFactorInfo & {
3231
phoneNumber?: string;
@@ -179,7 +178,7 @@ export class SmsMultiFactorAssertionVerifyFormComponent {
179178
private formSchema = injectMultiFactorPhoneAuthVerifyFormSchema();
180179

181180
verificationId = input.required<string>();
182-
onSuccess = output<void>();
181+
onSuccess = output<UserCredential>();
183182

184183
verificationCodeLabel = injectTranslation("labels", "verificationCode");
185184
verifyCodeLabel = injectTranslation("labels", "verifyCode");
@@ -208,8 +207,8 @@ export class SmsMultiFactorAssertionVerifyFormComponent {
208207
try {
209208
const credential = PhoneAuthProvider.credential(value.verificationId, value.verificationCode);
210209
const assertion = PhoneMultiFactorGenerator.assertion(credential);
211-
await signInWithMultiFactorAssertion(this.ui(), assertion);
212-
this.onSuccess.emit();
210+
const result = await signInWithMultiFactorAssertion(this.ui(), assertion);
211+
this.onSuccess.emit(result);
213212
return;
214213
} catch (error) {
215214
if (error instanceof FirebaseUIError) {
@@ -239,7 +238,7 @@ export class SmsMultiFactorAssertionVerifyFormComponent {
239238
@if (verification()) {
240239
<fui-sms-multi-factor-assertion-verify-form
241240
[verificationId]="verification()!.verificationId"
242-
(onSuccess)="onSuccess.emit()"
241+
(onSuccess)="onSuccess.emit($event)"
243242
/>
244243
} @else {
245244
<fui-sms-multi-factor-assertion-phone-form [hint]="hint()" (onSubmit)="handlePhoneSubmit($event)" />
@@ -249,7 +248,7 @@ export class SmsMultiFactorAssertionVerifyFormComponent {
249248
})
250249
export class SmsMultiFactorAssertionFormComponent {
251250
hint = input.required<MultiFactorInfo>();
252-
onSuccess = output<void>();
251+
onSuccess = output<UserCredential>();
253252

254253
verification = signal<{ verificationId: string } | null>(null);
255254

packages/angular/src/lib/auth/forms/mfa/totp-multi-factor-assertion-form.spec.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,36 @@ describe("<fui-totp-multi-factor-assertion-form>", () => {
145145
});
146146
});
147147

148+
it("emits onSuccess with credential after successful verification", async () => {
149+
const mockHint = {
150+
factorId: TotpMultiFactorGenerator.FACTOR_ID,
151+
displayName: "TOTP",
152+
uid: "test-uid",
153+
};
154+
155+
const mockCredential = { user: { uid: "totp-verify-user" } };
156+
signInWithMultiFactorAssertion.mockResolvedValue(mockCredential);
157+
158+
const { fixture } = await render(TotpMultiFactorAssertionFormComponent, {
159+
componentInputs: {
160+
hint: mockHint,
161+
},
162+
imports: [TotpMultiFactorAssertionFormComponent],
163+
});
164+
165+
const component = fixture.componentInstance;
166+
const onSuccessSpy = jest.fn();
167+
component.onSuccess.subscribe(onSuccessSpy);
168+
169+
component.form.setFieldValue("verificationCode", "123456");
170+
fixture.detectChanges();
171+
172+
await component.form.handleSubmit();
173+
await waitFor(() => {
174+
expect(onSuccessSpy).toHaveBeenCalledWith(mockCredential);
175+
});
176+
});
177+
148178
it("calls TotpMultiFactorGenerator.assertionForSignIn with correct parameters", async () => {
149179
const mockHint = {
150180
factorId: TotpMultiFactorGenerator.FACTOR_ID,

packages/angular/src/lib/auth/forms/mfa/totp-multi-factor-assertion-form.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { injectForm, injectStore, TanStackAppField, TanStackField } from "@tanst
1919
import { injectMultiFactorTotpAuthVerifyFormSchema, injectTranslation, injectUI } from "../../../provider";
2020
import { FormInputComponent, FormSubmitComponent, FormErrorMessageComponent } from "../../../components/form";
2121
import { FirebaseUIError, signInWithMultiFactorAssertion } from "@firebase-ui/core";
22-
import { TotpMultiFactorGenerator, type MultiFactorInfo } from "firebase/auth";
22+
import { TotpMultiFactorGenerator, type UserCredential, type MultiFactorInfo } from "firebase/auth";
2323

2424
@Component({
2525
selector: "fui-totp-multi-factor-assertion-form",
@@ -59,7 +59,7 @@ export class TotpMultiFactorAssertionFormComponent {
5959
private formSchema = injectMultiFactorTotpAuthVerifyFormSchema();
6060

6161
hint = input.required<MultiFactorInfo>();
62-
onSuccess = output<void>();
62+
onSuccess = output<UserCredential>();
6363

6464
verificationCodeLabel = injectTranslation("labels", "verificationCode");
6565
verifyCodeLabel = injectTranslation("labels", "verifyCode");
@@ -82,8 +82,8 @@ export class TotpMultiFactorAssertionFormComponent {
8282
onSubmitAsync: async ({ value }) => {
8383
try {
8484
const assertion = TotpMultiFactorGenerator.assertionForSignIn(this.hint().uid, value.verificationCode);
85-
await signInWithMultiFactorAssertion(this.ui(), assertion);
86-
this.onSuccess.emit();
85+
const result = await signInWithMultiFactorAssertion(this.ui(), assertion);
86+
this.onSuccess.emit(result);
8787
return;
8888
} catch (error) {
8989
if (error instanceof FirebaseUIError) {

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,16 +136,18 @@ describe("<fui-multi-factor-auth-assertion-form>", () => {
136136
expect(screen.queryByRole("button", { name: "SMS Verification" })).not.toBeInTheDocument();
137137
});
138138

139-
it("throws error when no resolver is provided", () => {
139+
it("throws error when no resolver is provided", async () => {
140140
const { injectUI } = require("../../../provider");
141141
injectUI.mockImplementation(() => {
142142
return () => ({
143143
multiFactorResolver: null,
144144
});
145145
});
146146

147-
expect(() => {
148-
new MultiFactorAuthAssertionFormComponent();
149-
}).toThrow("MultiFactorAuthAssertionForm requires a multi-factor resolver");
147+
await expect(
148+
render(MultiFactorAuthAssertionFormComponent, {
149+
imports: [MultiFactorAuthAssertionFormComponent],
150+
})
151+
).rejects.toThrow("MultiFactorAuthAssertionForm requires a multi-factor resolver");
150152
});
151153
});

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

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

17-
import { Component, computed, signal } from "@angular/core";
17+
import { Component, computed, output, signal } from "@angular/core";
1818
import { CommonModule } from "@angular/common";
1919
import { injectUI, injectTranslation } from "../../provider";
20-
import { PhoneMultiFactorGenerator, TotpMultiFactorGenerator, type MultiFactorInfo } from "firebase/auth";
20+
import {
21+
PhoneMultiFactorGenerator,
22+
TotpMultiFactorGenerator,
23+
type UserCredential,
24+
type MultiFactorInfo,
25+
} from "firebase/auth";
2126
import { SmsMultiFactorAssertionFormComponent } from "./mfa/sms-multi-factor-assertion-form";
2227
import { TotpMultiFactorAssertionFormComponent } from "./mfa/totp-multi-factor-assertion-form";
2328
import { ButtonComponent } from "../../components/button";
@@ -30,9 +35,9 @@ import { ButtonComponent } from "../../components/button";
3035
<div class="fui-content">
3136
@if (selectedHint()) {
3237
@if (selectedHint()!.factorId === phoneFactorId()) {
33-
<fui-sms-multi-factor-assertion-form [hint]="selectedHint()!" />
38+
<fui-sms-multi-factor-assertion-form [hint]="selectedHint()!" (onSuccess)="onSuccess.emit($event)" />
3439
} @else if (selectedHint()!.factorId === totpFactorId()) {
35-
<fui-totp-multi-factor-assertion-form [hint]="selectedHint()!" />
40+
<fui-totp-multi-factor-assertion-form [hint]="selectedHint()!" (onSuccess)="onSuccess.emit($event)" />
3641
}
3742
} @else {
3843
<p>TODO: Select a multi-factor authentication method</p>
@@ -54,6 +59,8 @@ import { ButtonComponent } from "../../components/button";
5459
export class MultiFactorAuthAssertionFormComponent {
5560
private ui = injectUI();
5661

62+
onSuccess = output<UserCredential>();
63+
5764
resolver = computed(() => {
5865
const resolver = this.ui().multiFactorResolver;
5966
if (!resolver) {

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

Lines changed: 69 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*/
1616

1717
import { render, screen } from "@testing-library/angular";
18-
import { Component } from "@angular/core";
18+
import { Component, EventEmitter } from "@angular/core";
1919
import { TestBed } from "@angular/core/testing";
2020

2121
import { OAuthScreenComponent } from "./oauth-screen";
@@ -50,13 +50,6 @@ class MockPoliciesComponent {}
5050
})
5151
class MockRedirectErrorComponent {}
5252

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-
6053
@Component({
6154
template: `
6255
<fui-oauth-screen>
@@ -87,6 +80,15 @@ class TestHostWithMultipleProvidersComponent {}
8780
})
8881
class TestHostWithoutContentComponent {}
8982

83+
@Component({
84+
selector: "fui-multi-factor-auth-assertion-form",
85+
template: '<div data-testid="mfa-assertion-form">MFA Assertion Form</div>',
86+
standalone: true,
87+
})
88+
class MockMultiFactorAuthAssertionFormComponent {
89+
onSuccess = new EventEmitter();
90+
}
91+
9092
describe("<fui-oauth-screen>", () => {
9193
beforeEach(() => {
9294
const { injectTranslation, injectPolicies, injectRedirectError, injectUI } = require("../../../provider");
@@ -124,7 +126,7 @@ describe("<fui-oauth-screen>", () => {
124126
OAuthScreenComponent,
125127
MockPoliciesComponent,
126128
MockRedirectErrorComponent,
127-
MockMultiFactorAuthAssertionFormComponent,
129+
MultiFactorAuthAssertionFormComponent,
128130
CardComponent,
129131
CardHeaderComponent,
130132
CardTitleComponent,
@@ -144,7 +146,7 @@ describe("<fui-oauth-screen>", () => {
144146
OAuthScreenComponent,
145147
MockPoliciesComponent,
146148
MockRedirectErrorComponent,
147-
MockMultiFactorAuthAssertionFormComponent,
149+
MultiFactorAuthAssertionFormComponent,
148150
CardComponent,
149151
CardHeaderComponent,
150152
CardTitleComponent,
@@ -164,7 +166,7 @@ describe("<fui-oauth-screen>", () => {
164166
OAuthScreenComponent,
165167
MockPoliciesComponent,
166168
MockRedirectErrorComponent,
167-
MockMultiFactorAuthAssertionFormComponent,
169+
MultiFactorAuthAssertionFormComponent,
168170
CardComponent,
169171
CardHeaderComponent,
170172
CardTitleComponent,
@@ -185,7 +187,7 @@ describe("<fui-oauth-screen>", () => {
185187
OAuthScreenComponent,
186188
MockPoliciesComponent,
187189
MockRedirectErrorComponent,
188-
MockMultiFactorAuthAssertionFormComponent,
190+
MultiFactorAuthAssertionFormComponent,
189191
CardComponent,
190192
CardHeaderComponent,
191193
CardTitleComponent,
@@ -210,7 +212,7 @@ describe("<fui-oauth-screen>", () => {
210212
OAuthScreenComponent,
211213
MockPoliciesComponent,
212214
MockRedirectErrorComponent,
213-
MockMultiFactorAuthAssertionFormComponent,
215+
MultiFactorAuthAssertionFormComponent,
214216
CardComponent,
215217
CardHeaderComponent,
216218
CardTitleComponent,
@@ -230,7 +232,7 @@ describe("<fui-oauth-screen>", () => {
230232
OAuthScreenComponent,
231233
MockPoliciesComponent,
232234
MockRedirectErrorComponent,
233-
MockMultiFactorAuthAssertionFormComponent,
235+
MultiFactorAuthAssertionFormComponent,
234236
CardComponent,
235237
CardHeaderComponent,
236238
CardTitleComponent,
@@ -255,7 +257,7 @@ describe("<fui-oauth-screen>", () => {
255257
OAuthScreenComponent,
256258
MockPoliciesComponent,
257259
MockRedirectErrorComponent,
258-
MockMultiFactorAuthAssertionFormComponent,
260+
MultiFactorAuthAssertionFormComponent,
259261
CardComponent,
260262
CardHeaderComponent,
261263
CardTitleComponent,
@@ -288,7 +290,7 @@ describe("<fui-oauth-screen>", () => {
288290
OAuthScreenComponent,
289291
MockPoliciesComponent,
290292
MockRedirectErrorComponent,
291-
MockMultiFactorAuthAssertionFormComponent,
293+
MultiFactorAuthAssertionFormComponent,
292294
CardComponent,
293295
CardHeaderComponent,
294296
CardTitleComponent,
@@ -321,7 +323,7 @@ describe("<fui-oauth-screen>", () => {
321323
OAuthScreenComponent,
322324
MockPoliciesComponent,
323325
MockRedirectErrorComponent,
324-
MockMultiFactorAuthAssertionFormComponent,
326+
MultiFactorAuthAssertionFormComponent,
325327
CardComponent,
326328
CardHeaderComponent,
327329
CardTitleComponent,
@@ -334,4 +336,54 @@ describe("<fui-oauth-screen>", () => {
334336
expect(screen.queryByTestId("policies")).not.toBeInTheDocument();
335337
expect(screen.getByTestId("mfa-assertion-form")).toBeInTheDocument();
336338
});
339+
340+
it("emits onSignIn with credential when MFA flow succeeds", async () => {
341+
const { injectUI } = require("../../../provider");
342+
injectUI.mockImplementation(() => {
343+
return () => ({
344+
multiFactorResolver: { hints: [{ factorId: "totp", uid: "test" }] },
345+
});
346+
});
347+
348+
TestBed.overrideComponent(MultiFactorAuthAssertionFormComponent, {
349+
set: {
350+
template:
351+
'<div data-testid="mfa-assertion-form">MFA Assertion Form</div><button data-testid="mfa-on-success" (click)="onSuccess.emit({ user: { uid: \'angular-oauth-mfa-user\' } })">Trigger</button>',
352+
},
353+
});
354+
355+
const onSignInHandler = jest.fn();
356+
357+
@Component({
358+
template: `<fui-oauth-screen (onSignIn)="onSignIn($event)"></fui-oauth-screen>`,
359+
standalone: true,
360+
imports: [OAuthScreenComponent],
361+
})
362+
class HostCaptureComponent {
363+
onSignIn = onSignInHandler;
364+
}
365+
366+
await render(HostCaptureComponent, {
367+
imports: [
368+
OAuthScreenComponent,
369+
MockPoliciesComponent,
370+
MockRedirectErrorComponent,
371+
MultiFactorAuthAssertionFormComponent,
372+
CardComponent,
373+
CardHeaderComponent,
374+
CardTitleComponent,
375+
CardSubtitleComponent,
376+
CardContentComponent,
377+
ContentComponent,
378+
],
379+
});
380+
381+
const trigger = screen.getByTestId("mfa-on-success");
382+
trigger.dispatchEvent(new MouseEvent("click", { bubbles: true }));
383+
384+
expect(onSignInHandler).toHaveBeenCalled();
385+
expect(onSignInHandler).toHaveBeenCalledWith(
386+
expect.objectContaining({ user: expect.objectContaining({ uid: "angular-oauth-mfa-user" }) })
387+
);
388+
});
337389
});

0 commit comments

Comments
 (0)