Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 58 additions & 46 deletions packages/core/src/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import {
signInWithEmailAndPassword,
createUserWithEmailAndPassword,
signInWithPhoneNumber,
verifyPhoneNumber,
confirmPhoneNumber,
sendPasswordResetEmail,
sendSignInLinkToEmail,
Expand All @@ -16,7 +16,6 @@ import {
vi.mock("firebase/auth", () => ({
signInWithCredential: vi.fn(),
createUserWithEmailAndPassword: vi.fn(),
signInWithPhoneNumber: vi.fn(),
sendPasswordResetEmail: vi.fn(),
sendSignInLinkToEmail: vi.fn(),
signInAnonymously: vi.fn(),
Expand All @@ -26,9 +25,9 @@ vi.mock("firebase/auth", () => ({
credential: vi.fn(),
credentialWithLink: vi.fn(),
},
PhoneAuthProvider: {
PhoneAuthProvider: Object.assign(vi.fn(), {
credential: vi.fn(),
},
}),
linkWithCredential: vi.fn(),
}));

Expand All @@ -47,15 +46,12 @@ import {
EmailAuthProvider,
PhoneAuthProvider,
createUserWithEmailAndPassword as _createUserWithEmailAndPassword,
signInWithPhoneNumber as _signInWithPhoneNumber,
sendPasswordResetEmail as _sendPasswordResetEmail,
sendSignInLinkToEmail as _sendSignInLinkToEmail,
signInAnonymously as _signInAnonymously,
signInWithRedirect,
isSignInWithEmailLink as _isSignInWithEmailLink,
UserCredential,
Auth,
ConfirmationResult,
AuthProvider,
TotpSecret,
} from "firebase/auth";
Expand Down Expand Up @@ -195,7 +191,7 @@ describe("createUserWithEmailAndPassword", () => {
const password = "password123";

const credential = EmailAuthProvider.credential(email, password);
vi.mocked(hasBehavior).mockImplementation((ui, behavior) => {
vi.mocked(hasBehavior).mockImplementation((_, behavior) => {
if (behavior === "autoUpgradeAnonymousCredential") return true;
if (behavior === "requireDisplayName") return false;
return false;
Expand All @@ -221,7 +217,7 @@ describe("createUserWithEmailAndPassword", () => {
const password = "password123";

const credential = EmailAuthProvider.credential(email, password);
vi.mocked(hasBehavior).mockImplementation((ui, behavior) => {
vi.mocked(hasBehavior).mockImplementation((_, behavior) => {
if (behavior === "autoUpgradeAnonymousCredential") return true;
if (behavior === "requireDisplayName") return false;
return false;
Expand Down Expand Up @@ -380,44 +376,54 @@ describe("createUserWithEmailAndPassword", () => {
});
});

describe("signInWithPhoneNumber", () => {
describe("verifyPhoneNumber", () => {
beforeEach(() => {
vi.clearAllMocks();
});

it("should update state and call signInWithPhoneNumber successfully", async () => {
it("should update state and call PhoneAuthProvider.verifyPhoneNumber successfully", async () => {
const mockUI = createMockUI();
const phoneNumber = "+1234567890";
const mockRecaptchaVerifier = {} as any;
const mockConfirmationResult = {
verificationId: "test-verification-id",
confirm: vi.fn(),
} as any;

vi.mocked(_signInWithPhoneNumber).mockResolvedValue(mockConfirmationResult);
const mockAppVerifier = {} as any;
const mockVerificationId = "test-verification-id";

const mockVerifyPhoneNumber = vi.fn().mockResolvedValue(mockVerificationId);
vi.mocked(PhoneAuthProvider).mockImplementation(
() =>
({
verifyPhoneNumber: mockVerifyPhoneNumber,
}) as any
);

const result = await signInWithPhoneNumber(mockUI, phoneNumber, mockRecaptchaVerifier);
const result = await verifyPhoneNumber(mockUI, phoneNumber, mockAppVerifier);

// Verify state management
expect(vi.mocked(mockUI.setState).mock.calls).toEqual([["pending"], ["idle"]]);

// Verify the Firebase function was called with correct parameters
expect(_signInWithPhoneNumber).toHaveBeenCalledWith(mockUI.auth, phoneNumber, mockRecaptchaVerifier);
expect(_signInWithPhoneNumber).toHaveBeenCalledTimes(1);
// Verify the PhoneAuthProvider was created and verifyPhoneNumber was called
expect(PhoneAuthProvider).toHaveBeenCalledWith(mockUI.auth);
expect(mockVerifyPhoneNumber).toHaveBeenCalledWith(phoneNumber, mockAppVerifier);
expect(mockVerifyPhoneNumber).toHaveBeenCalledTimes(1);

// Verify the result
expect(result).toEqual(mockConfirmationResult);
expect(result).toEqual(mockVerificationId);
});

it("should call handleFirebaseError if an error is thrown", async () => {
const mockUI = createMockUI();
const phoneNumber = "+1234567890";
const mockRecaptchaVerifier = {} as any;
const mockAppVerifier = {} as any;
const error = new FirebaseError("auth/invalid-phone-number", "Invalid phone number");

vi.mocked(_signInWithPhoneNumber).mockRejectedValue(error);
const mockVerifyPhoneNumber = vi.fn().mockRejectedValue(error);
vi.mocked(PhoneAuthProvider).mockImplementation(
() =>
({
verifyPhoneNumber: mockVerifyPhoneNumber,
}) as any
);

await signInWithPhoneNumber(mockUI, phoneNumber, mockRecaptchaVerifier);
await verifyPhoneNumber(mockUI, phoneNumber, mockAppVerifier);

// Verify error handling
expect(handleFirebaseError).toHaveBeenCalledWith(mockUI, error);
Expand All @@ -429,12 +435,18 @@ describe("signInWithPhoneNumber", () => {
it("should handle recaptcha verification errors", async () => {
const mockUI = createMockUI();
const phoneNumber = "+1234567890";
const mockRecaptchaVerifier = {} as any;
const mockAppVerifier = {} as any;
const error = new Error("reCAPTCHA verification failed");

vi.mocked(_signInWithPhoneNumber).mockRejectedValue(error);
const mockVerifyPhoneNumber = vi.fn().mockRejectedValue(error);
vi.mocked(PhoneAuthProvider).mockImplementation(
() =>
({
verifyPhoneNumber: mockVerifyPhoneNumber,
}) as any
);

await signInWithPhoneNumber(mockUI, phoneNumber, mockRecaptchaVerifier);
await verifyPhoneNumber(mockUI, phoneNumber, mockAppVerifier);

// Verify error handling
expect(handleFirebaseError).toHaveBeenCalledWith(mockUI, error);
Expand All @@ -453,15 +465,15 @@ describe("confirmPhoneNumber", () => {
const mockUI = createMockUI({
auth: { currentUser: null } as Auth,
});
const confirmationResult = { verificationId: "test-verification-id" } as ConfirmationResult;
const verificationId = "test-verification-id";
const verificationCode = "123456";

const credential = PhoneAuthProvider.credential(confirmationResult.verificationId, verificationCode);
const credential = PhoneAuthProvider.credential(verificationId, verificationCode);
vi.mocked(hasBehavior).mockReturnValue(false);
vi.mocked(PhoneAuthProvider.credential).mockReturnValue(credential);
vi.mocked(_signInWithCredential).mockResolvedValue({ providerId: "phone" } as UserCredential);

const result = await confirmPhoneNumber(mockUI, confirmationResult, verificationCode);
const result = await confirmPhoneNumber(mockUI, verificationId, verificationCode);

// Since currentUser is null, the behavior should not called.
expect(hasBehavior).toHaveBeenCalledTimes(0);
Expand All @@ -480,16 +492,16 @@ describe("confirmPhoneNumber", () => {
const mockUI = createMockUI({
auth: { currentUser: { isAnonymous: true } } as Auth,
});
const confirmationResult = { verificationId: "test-verification-id" } as ConfirmationResult;
const verificationId = "test-verification-id";
const verificationCode = "123456";

const credential = PhoneAuthProvider.credential(confirmationResult.verificationId, verificationCode);
const credential = PhoneAuthProvider.credential(verificationId, verificationCode);
vi.mocked(hasBehavior).mockReturnValue(true);
vi.mocked(PhoneAuthProvider.credential).mockReturnValue(credential);
const mockBehavior = vi.fn().mockResolvedValue({ providerId: "phone" } as UserCredential);
vi.mocked(getBehavior).mockReturnValue(mockBehavior);

const result = await confirmPhoneNumber(mockUI, confirmationResult, verificationCode);
const result = await confirmPhoneNumber(mockUI, verificationId, verificationCode);

expect(hasBehavior).toHaveBeenCalledWith(mockUI, "autoUpgradeAnonymousCredential");
expect(getBehavior).toHaveBeenCalledWith(mockUI, "autoUpgradeAnonymousCredential");
Expand All @@ -505,14 +517,14 @@ describe("confirmPhoneNumber", () => {
const mockUI = createMockUI({
auth: { currentUser: { isAnonymous: false } } as Auth,
});
const confirmationResult = { verificationId: "test-verification-id" } as ConfirmationResult;
const verificationId = "test-verification-id";
const verificationCode = "123456";

const credential = PhoneAuthProvider.credential(confirmationResult.verificationId, verificationCode);
const credential = PhoneAuthProvider.credential(verificationId, verificationCode);
vi.mocked(PhoneAuthProvider.credential).mockReturnValue(credential);
vi.mocked(_signInWithCredential).mockResolvedValue({ providerId: "phone" } as UserCredential);

const result = await confirmPhoneNumber(mockUI, confirmationResult, verificationCode);
const result = await confirmPhoneNumber(mockUI, verificationId, verificationCode);

// Behavior should not be called when user is not anonymous
expect(hasBehavior).not.toHaveBeenCalled();
Expand All @@ -527,14 +539,14 @@ describe("confirmPhoneNumber", () => {
const mockUI = createMockUI({
auth: { currentUser: null } as Auth,
});
const confirmationResult = { verificationId: "test-verification-id" } as ConfirmationResult;
const verificationId = "test-verification-id";
const verificationCode = "123456";

const credential = PhoneAuthProvider.credential(confirmationResult.verificationId, verificationCode);
const credential = PhoneAuthProvider.credential(verificationId, verificationCode);
vi.mocked(PhoneAuthProvider.credential).mockReturnValue(credential);
vi.mocked(_signInWithCredential).mockResolvedValue({ providerId: "phone" } as UserCredential);

const result = await confirmPhoneNumber(mockUI, confirmationResult, verificationCode);
const result = await confirmPhoneNumber(mockUI, verificationId, verificationCode);

// Behavior should not be called when user is null
expect(hasBehavior).not.toHaveBeenCalled();
Expand All @@ -549,16 +561,16 @@ describe("confirmPhoneNumber", () => {
const mockUI = createMockUI({
auth: { currentUser: { isAnonymous: true } } as Auth,
});
const confirmationResult = { verificationId: "test-verification-id" } as ConfirmationResult;
const verificationId = "test-verification-id";
const verificationCode = "123456";

const credential = PhoneAuthProvider.credential(confirmationResult.verificationId, verificationCode);
const credential = PhoneAuthProvider.credential(verificationId, verificationCode);
vi.mocked(hasBehavior).mockReturnValue(true);
vi.mocked(PhoneAuthProvider.credential).mockReturnValue(credential);
const mockBehavior = vi.fn().mockResolvedValue(undefined);
vi.mocked(getBehavior).mockReturnValue(mockBehavior);

await confirmPhoneNumber(mockUI, confirmationResult, verificationCode);
await confirmPhoneNumber(mockUI, verificationId, verificationCode);

expect(hasBehavior).toHaveBeenCalledWith(mockUI, "autoUpgradeAnonymousCredential");
expect(getBehavior).toHaveBeenCalledWith(mockUI, "autoUpgradeAnonymousCredential");
Expand All @@ -576,14 +588,14 @@ describe("confirmPhoneNumber", () => {
const mockUI = createMockUI({
auth: { currentUser: null } as Auth,
});
const confirmationResult = { verificationId: "test-verification-id" } as ConfirmationResult;
const verificationId = "test-verification-id";
const verificationCode = "123456";

const error = new FirebaseError("auth/invalid-verification-code", "Invalid verification code");

vi.mocked(_signInWithCredential).mockRejectedValue(error);

await confirmPhoneNumber(mockUI, confirmationResult, verificationCode);
await confirmPhoneNumber(mockUI, verificationId, verificationCode);

expect(handleFirebaseError).toHaveBeenCalledWith(mockUI, error);
expect(vi.mocked(mockUI.setState).mock.calls).toEqual([["pending"], ["idle"]]);
Expand Down
16 changes: 8 additions & 8 deletions packages/core/src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,17 @@ import {
sendPasswordResetEmail as _sendPasswordResetEmail,
sendSignInLinkToEmail as _sendSignInLinkToEmail,
signInAnonymously as _signInAnonymously,
signInWithPhoneNumber as _signInWithPhoneNumber,
signInWithCredential as _signInWithCredential,
ActionCodeSettings,
ApplicationVerifier,
AuthProvider,
ConfirmationResult,
EmailAuthProvider,
linkWithCredential,
PhoneAuthProvider,
UserCredential,
AuthCredential,
TotpSecret,
PhoneInfoOptions,
} from "firebase/auth";
import QRCode from "qrcode-generator";
import { FirebaseUIConfiguration } from "./config";
Expand Down Expand Up @@ -121,14 +120,15 @@ export async function createUserWithEmailAndPassword(
}
}

export async function signInWithPhoneNumber(
export async function verifyPhoneNumber(
ui: FirebaseUIConfiguration,
phoneNumber: string,
phoneNumber: PhoneInfoOptions | string,
appVerifier: ApplicationVerifier
): Promise<ConfirmationResult> {
): Promise<string> {
try {
ui.setState("pending");
return await _signInWithPhoneNumber(ui.auth, phoneNumber, appVerifier);
const provider = new PhoneAuthProvider(ui.auth);
return await provider.verifyPhoneNumber(phoneNumber, appVerifier);
} catch (error) {
handleFirebaseError(ui, error);
} finally {
Expand All @@ -138,13 +138,13 @@ export async function signInWithPhoneNumber(

export async function confirmPhoneNumber(
ui: FirebaseUIConfiguration,
confirmationResult: ConfirmationResult,
verificationId: string,
verificationCode: string
): Promise<UserCredential> {
try {
ui.setState("pending");
const currentUser = ui.auth.currentUser;
const credential = PhoneAuthProvider.credential(confirmationResult.verificationId, verificationCode);
const credential = PhoneAuthProvider.credential(verificationId, verificationCode);

if (currentUser?.isAnonymous && hasBehavior(ui, "autoUpgradeAnonymousCredential")) {
const result = await getBehavior(ui, "autoUpgradeAnonymousCredential")(ui, credential);
Expand Down
Loading