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
6 changes: 3 additions & 3 deletions packages/core/src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@ import {
signInAnonymously as _signInAnonymously,
signInWithPhoneNumber as _signInWithPhoneNumber,
ActionCodeSettings,
ApplicationVerifier,
AuthProvider,
ConfirmationResult,
EmailAuthProvider,
linkWithCredential,
PhoneAuthProvider,
RecaptchaVerifier,
signInWithCredential,
signInWithRedirect,
UserCredential,
Expand Down Expand Up @@ -108,11 +108,11 @@ export async function createUserWithEmailAndPassword(
export async function signInWithPhoneNumber(
ui: FirebaseUIConfiguration,
phoneNumber: string,
recaptchaVerifier: RecaptchaVerifier
appVerifier: ApplicationVerifier
): Promise<ConfirmationResult> {
try {
ui.setState("pending");
return await _signInWithPhoneNumber(ui.auth, phoneNumber, recaptchaVerifier);
return await _signInWithPhoneNumber(ui.auth, phoneNumber, appVerifier);
} catch (error) {
handleFirebaseError(ui, error);
} finally {
Expand Down
106 changes: 104 additions & 2 deletions packages/core/src/behaviors.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { createMockUI } from "~/tests/utils";
import { autoAnonymousLogin, autoUpgradeAnonymousUsers, getBehavior, hasBehavior } from "./behaviors";
import { Auth, signInAnonymously, User, UserCredential, linkWithCredential, linkWithRedirect, AuthCredential, AuthProvider } from "firebase/auth";
import { autoAnonymousLogin, autoUpgradeAnonymousUsers, getBehavior, hasBehavior, recaptchaVerification } from "./behaviors";
import { Auth, signInAnonymously, User, UserCredential, linkWithCredential, linkWithRedirect, AuthCredential, AuthProvider, RecaptchaVerifier } from "firebase/auth";

vi.mock("firebase/auth", () => ({
signInAnonymously: vi.fn(),
linkWithCredential: vi.fn(),
linkWithRedirect: vi.fn(),
RecaptchaVerifier: vi.fn(),
}));

describe("hasBehavior", () => {
Expand Down Expand Up @@ -218,3 +219,104 @@ describe("autoUpgradeAnonymousUsers", () => {
});
});
});

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

it("should create a RecaptchaVerifier with default options", () => {
const mockRecaptchaVerifier = { render: vi.fn() };
vi.mocked(RecaptchaVerifier).mockImplementation(() => mockRecaptchaVerifier as any);

const mockElement = document.createElement("div");
const mockUI = createMockUI();

const behavior = recaptchaVerification();
const result = behavior.recaptchaVerification(mockUI, mockElement);

expect(RecaptchaVerifier).toHaveBeenCalledWith(mockUI.auth, mockElement, {
size: "invisible",
theme: "light",
tabindex: 0,
});
expect(result).toBe(mockRecaptchaVerifier);
});

it("should create a RecaptchaVerifier with custom options", () => {
const mockRecaptchaVerifier = { render: vi.fn() };
vi.mocked(RecaptchaVerifier).mockImplementation(() => mockRecaptchaVerifier as any);

const mockElement = document.createElement("div");
const mockUI = createMockUI();
const customOptions = {
size: "normal" as const,
theme: "dark" as const,
tabindex: 5,
};

const behavior = recaptchaVerification(customOptions);
const result = behavior.recaptchaVerification(mockUI, mockElement);

expect(RecaptchaVerifier).toHaveBeenCalledWith(mockUI.auth, mockElement, {
size: "normal",
theme: "dark",
tabindex: 5,
});
expect(result).toBe(mockRecaptchaVerifier);
});

it("should create a RecaptchaVerifier with partial custom options", () => {
const mockRecaptchaVerifier = { render: vi.fn() };
vi.mocked(RecaptchaVerifier).mockImplementation(() => mockRecaptchaVerifier as any);

const mockElement = document.createElement("div");
const mockUI = createMockUI();
const partialOptions = {
size: "compact" as const,
};

const behavior = recaptchaVerification(partialOptions);
const result = behavior.recaptchaVerification(mockUI, mockElement);

expect(RecaptchaVerifier).toHaveBeenCalledWith(mockUI.auth, mockElement, {
size: "compact",
theme: "light",
tabindex: 0,
});
expect(result).toBe(mockRecaptchaVerifier);
});

it("should work with hasBehavior and getBehavior", () => {
const mockRecaptchaVerifier = { render: vi.fn() };
vi.mocked(RecaptchaVerifier).mockImplementation(() => mockRecaptchaVerifier as any);

const mockElement = document.createElement("div");
const mockUI = createMockUI({
behaviors: {
recaptchaVerification: recaptchaVerification().recaptchaVerification,
},
});

expect(hasBehavior(mockUI, "recaptchaVerification")).toBe(true);

const behavior = getBehavior(mockUI, "recaptchaVerification");
const result = behavior(mockUI, mockElement);

expect(RecaptchaVerifier).toHaveBeenCalledWith(mockUI.auth, mockElement, {
size: "invisible",
theme: "light",
tabindex: 0,
});
expect(result).toBe(mockRecaptchaVerifier);
});

it("should throw error when trying to get non-existent recaptchaVerification behavior", () => {
const mockUI = createMockUI();

expect(hasBehavior(mockUI, "recaptchaVerification")).toBe(false);
expect(() => getBehavior(mockUI, "recaptchaVerification")).toThrow("Behavior recaptchaVerification not found");
});
});


60 changes: 23 additions & 37 deletions packages/core/src/behaviors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
signInAnonymously,
User,
UserCredential,
RecaptchaVerifier,
} from "firebase/auth";
import { FirebaseUIConfiguration } from "./config";

Expand All @@ -32,6 +33,7 @@ export type BehaviorHandlers = {
credential: AuthCredential
) => Promise<UserCredential | undefined>;
autoUpgradeAnonymousProvider: (ui: FirebaseUIConfiguration, provider: AuthProvider) => Promise<undefined | never>;
recaptchaVerification: (ui: FirebaseUIConfiguration, element: HTMLElement) => RecaptchaVerifier;
};

export type Behavior<T extends keyof BehaviorHandlers = keyof BehaviorHandlers> = Pick<BehaviorHandlers, T>;
Expand Down Expand Up @@ -112,40 +114,24 @@ export function autoUpgradeAnonymousUsers(): Behavior<
};
}

// export function autoUpgradeAnonymousCredential(): RegisteredBehavior<'autoUpgradeAnonymousCredential'> {
// return {
// key: 'autoUpgradeAnonymousCredential',
// handler: async (auth, credential) => {
// const currentUser = auth.currentUser;

// // Check if the user is anonymous. If not, we can't upgrade them.
// if (!currentUser?.isAnonymous) {
// return;
// }

// $state.set('linking');
// const result = await linkWithCredential(currentUser, credential);
// $state.set('idle');
// return result;
// },
// };
// }

// export function autoUpgradeAnonymousProvider(): RegisteredBehavior<'autoUpgradeAnonymousCredential'> {
// return {
// key: 'autoUpgradeAnonymousProvider',
// handler: async (auth, credential) => {
// const currentUser = auth.currentUser;

// // Check if the user is anonymous. If not, we can't upgrade them.
// if (!currentUser?.isAnonymous) {
// return;
// }

// $state.set('linking');
// const result = await linkWithRedirect(currentUser, credential);
// $state.set('idle');
// return result;
// },
// };
// }
export type RecaptchaVerification = {
size?: "normal" | "invisible" | "compact";
theme?: "light" | "dark";
tabindex?: number;
};

export function recaptchaVerification(options?: RecaptchaVerification): Behavior<"recaptchaVerification"> {
return {
recaptchaVerification: (ui, element) => {
return new RecaptchaVerifier(ui.auth, element, {
size: options?.size ?? "invisible",
theme: options?.theme ?? "light",
tabindex: options?.tabindex ?? 0,
});
},
};
}

export const defaultBehaviors = {
...recaptchaVerification(),
};
69 changes: 66 additions & 3 deletions packages/core/src/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Auth } from "firebase/auth";
import { describe, it, expect } from "vitest";
import { initializeUI } from "./config";
import { enUs, registerLocale } from "@firebase-ui/translations";
import { autoUpgradeAnonymousUsers } from "./behaviors";
import { autoUpgradeAnonymousUsers, defaultBehaviors } from "./behaviors";

describe('initializeUI', () => {
it('should return a valid deep store with default values', () => {
Expand All @@ -17,12 +17,12 @@ describe('initializeUI', () => {
expect(ui.get()).toBeDefined();
expect(ui.get().app).toBe(config.app);
expect(ui.get().auth).toBe(config.auth);
expect(ui.get().behaviors).toEqual({});
expect(ui.get().behaviors).toEqual(defaultBehaviors);
expect(ui.get().state).toEqual("idle");
expect(ui.get().locale).toEqual(enUs);
});

it('should merge behaviors', () => {
it('should merge behaviors with defaultBehaviors', () => {
const config = {
app: {} as FirebaseApp,
auth: {} as Auth,
Expand All @@ -32,6 +32,11 @@ describe('initializeUI', () => {
const ui = initializeUI(config);
expect(ui).toBeDefined();
expect(ui.get()).toBeDefined();

// Should have default behaviors
expect(ui.get().behaviors).toHaveProperty("recaptchaVerification");

// Should have custom behaviors
expect(ui.get().behaviors).toHaveProperty("autoUpgradeAnonymousCredential");
expect(ui.get().behaviors).toHaveProperty("autoUpgradeAnonymousProvider");
});
Expand Down Expand Up @@ -66,5 +71,63 @@ describe('initializeUI', () => {
ui.get().setLocale(testLocale2);
expect(ui.get().locale.locale).toEqual('test2');
});

it('should include defaultBehaviors even when no custom behaviors are provided', () => {
const config = {
app: {} as FirebaseApp,
auth: {} as Auth,
};

const ui = initializeUI(config);
expect(ui.get().behaviors).toEqual(defaultBehaviors);
expect(ui.get().behaviors).toHaveProperty("recaptchaVerification");
});

it('should allow overriding default behaviors', () => {
const customRecaptchaVerification = {
recaptchaVerification: () => {
// Custom implementation
return {} as any;
}
};

const config = {
app: {} as FirebaseApp,
auth: {} as Auth,
behaviors: [customRecaptchaVerification],
};

const ui = initializeUI(config);
expect(ui.get().behaviors).toHaveProperty("recaptchaVerification");
expect(ui.get().behaviors.recaptchaVerification).toBe(customRecaptchaVerification.recaptchaVerification);
});

it('should merge multiple behavior objects correctly', () => {
const behavior1 = autoUpgradeAnonymousUsers();
const behavior2 = {
recaptchaVerification: () => {
// Custom recaptcha implementation
return {} as any;
}
};

const config = {
app: {} as FirebaseApp,
auth: {} as Auth,
behaviors: [behavior1, behavior2],
};

const ui = initializeUI(config);

// Should have default behaviors
expect(ui.get().behaviors).toHaveProperty("recaptchaVerification");

// Should have autoUpgrade behaviors
expect(ui.get().behaviors).toHaveProperty("autoUpgradeAnonymousCredential");
expect(ui.get().behaviors).toHaveProperty("autoUpgradeAnonymousProvider");

// Should have custom recaptcha implementation
expect(ui.get().behaviors.recaptchaVerification).toBe(behavior2.recaptchaVerification);
});
});

15 changes: 11 additions & 4 deletions packages/core/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,14 @@ import { enUs, RegisteredLocale } from "@firebase-ui/translations";
import type { FirebaseApp } from "firebase/app";
import { Auth, getAuth } from "firebase/auth";
import { deepMap, DeepMapStore, map } from "nanostores";
import { Behavior, type BehaviorHandlers, type BehaviorKey, getBehavior, hasBehavior } from "./behaviors";
import {
Behavior,
type BehaviorHandlers,
type BehaviorKey,
defaultBehaviors,
getBehavior,
hasBehavior,
} from "./behaviors";
import { FirebaseUIState } from "./state";

type FirebaseUIConfigurationOptions = {
Expand All @@ -44,14 +51,14 @@ export type FirebaseUI = DeepMapStore<FirebaseUIConfiguration>;

export function initializeUI(config: FirebaseUIConfigurationOptions, name: string = "[DEFAULT]"): FirebaseUI {
// Reduce the behaviors to a single object.
const behaviors = config.behaviors?.reduce(
const behaviors = config.behaviors?.reduce<Partial<Record<BehaviorKey, BehaviorHandlers[BehaviorKey]>>>(
(acc, behavior) => {
return {
...acc,
...behavior,
};
},
{} as Record<BehaviorKey, BehaviorHandlers[BehaviorKey]>
defaultBehaviors
);

$config.setKey(
Expand All @@ -69,7 +76,7 @@ export function initializeUI(config: FirebaseUIConfigurationOptions, name: strin
const current = $config.get()[name]!;
current.setKey(`state`, state);
},
behaviors: behaviors ?? {},
behaviors: behaviors ?? defaultBehaviors,
})
);

Expand Down
Loading