Skip to content

Commit b60f7fc

Browse files
authored
Merge pull request #1202 from firebase/@invertase/core-recaptcha-verification
2 parents f63fc32 + d2cbb65 commit b60f7fc

File tree

5 files changed

+207
-49
lines changed

5 files changed

+207
-49
lines changed

packages/core/src/auth.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,12 @@ import {
2222
signInAnonymously as _signInAnonymously,
2323
signInWithPhoneNumber as _signInWithPhoneNumber,
2424
ActionCodeSettings,
25+
ApplicationVerifier,
2526
AuthProvider,
2627
ConfirmationResult,
2728
EmailAuthProvider,
2829
linkWithCredential,
2930
PhoneAuthProvider,
30-
RecaptchaVerifier,
3131
signInWithCredential,
3232
signInWithRedirect,
3333
UserCredential,
@@ -108,11 +108,11 @@ export async function createUserWithEmailAndPassword(
108108
export async function signInWithPhoneNumber(
109109
ui: FirebaseUIConfiguration,
110110
phoneNumber: string,
111-
recaptchaVerifier: RecaptchaVerifier
111+
appVerifier: ApplicationVerifier
112112
): Promise<ConfirmationResult> {
113113
try {
114114
ui.setState("pending");
115-
return await _signInWithPhoneNumber(ui.auth, phoneNumber, recaptchaVerifier);
115+
return await _signInWithPhoneNumber(ui.auth, phoneNumber, appVerifier);
116116
} catch (error) {
117117
handleFirebaseError(ui, error);
118118
} finally {

packages/core/src/behaviors.test.ts

Lines changed: 104 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { describe, it, expect, vi, beforeEach } from "vitest";
22
import { createMockUI } from "~/tests/utils";
3-
import { autoAnonymousLogin, autoUpgradeAnonymousUsers, getBehavior, hasBehavior } from "./behaviors";
4-
import { Auth, signInAnonymously, User, UserCredential, linkWithCredential, linkWithRedirect, AuthCredential, AuthProvider } from "firebase/auth";
3+
import { autoAnonymousLogin, autoUpgradeAnonymousUsers, getBehavior, hasBehavior, recaptchaVerification } from "./behaviors";
4+
import { Auth, signInAnonymously, User, UserCredential, linkWithCredential, linkWithRedirect, AuthCredential, AuthProvider, RecaptchaVerifier } from "firebase/auth";
55

66
vi.mock("firebase/auth", () => ({
77
signInAnonymously: vi.fn(),
88
linkWithCredential: vi.fn(),
99
linkWithRedirect: vi.fn(),
10+
RecaptchaVerifier: vi.fn(),
1011
}));
1112

1213
describe("hasBehavior", () => {
@@ -218,3 +219,104 @@ describe("autoUpgradeAnonymousUsers", () => {
218219
});
219220
});
220221
});
222+
223+
describe("recaptchaVerification", () => {
224+
beforeEach(() => {
225+
vi.clearAllMocks();
226+
});
227+
228+
it("should create a RecaptchaVerifier with default options", () => {
229+
const mockRecaptchaVerifier = { render: vi.fn() };
230+
vi.mocked(RecaptchaVerifier).mockImplementation(() => mockRecaptchaVerifier as any);
231+
232+
const mockElement = document.createElement("div");
233+
const mockUI = createMockUI();
234+
235+
const behavior = recaptchaVerification();
236+
const result = behavior.recaptchaVerification(mockUI, mockElement);
237+
238+
expect(RecaptchaVerifier).toHaveBeenCalledWith(mockUI.auth, mockElement, {
239+
size: "invisible",
240+
theme: "light",
241+
tabindex: 0,
242+
});
243+
expect(result).toBe(mockRecaptchaVerifier);
244+
});
245+
246+
it("should create a RecaptchaVerifier with custom options", () => {
247+
const mockRecaptchaVerifier = { render: vi.fn() };
248+
vi.mocked(RecaptchaVerifier).mockImplementation(() => mockRecaptchaVerifier as any);
249+
250+
const mockElement = document.createElement("div");
251+
const mockUI = createMockUI();
252+
const customOptions = {
253+
size: "normal" as const,
254+
theme: "dark" as const,
255+
tabindex: 5,
256+
};
257+
258+
const behavior = recaptchaVerification(customOptions);
259+
const result = behavior.recaptchaVerification(mockUI, mockElement);
260+
261+
expect(RecaptchaVerifier).toHaveBeenCalledWith(mockUI.auth, mockElement, {
262+
size: "normal",
263+
theme: "dark",
264+
tabindex: 5,
265+
});
266+
expect(result).toBe(mockRecaptchaVerifier);
267+
});
268+
269+
it("should create a RecaptchaVerifier with partial custom options", () => {
270+
const mockRecaptchaVerifier = { render: vi.fn() };
271+
vi.mocked(RecaptchaVerifier).mockImplementation(() => mockRecaptchaVerifier as any);
272+
273+
const mockElement = document.createElement("div");
274+
const mockUI = createMockUI();
275+
const partialOptions = {
276+
size: "compact" as const,
277+
};
278+
279+
const behavior = recaptchaVerification(partialOptions);
280+
const result = behavior.recaptchaVerification(mockUI, mockElement);
281+
282+
expect(RecaptchaVerifier).toHaveBeenCalledWith(mockUI.auth, mockElement, {
283+
size: "compact",
284+
theme: "light",
285+
tabindex: 0,
286+
});
287+
expect(result).toBe(mockRecaptchaVerifier);
288+
});
289+
290+
it("should work with hasBehavior and getBehavior", () => {
291+
const mockRecaptchaVerifier = { render: vi.fn() };
292+
vi.mocked(RecaptchaVerifier).mockImplementation(() => mockRecaptchaVerifier as any);
293+
294+
const mockElement = document.createElement("div");
295+
const mockUI = createMockUI({
296+
behaviors: {
297+
recaptchaVerification: recaptchaVerification().recaptchaVerification,
298+
},
299+
});
300+
301+
expect(hasBehavior(mockUI, "recaptchaVerification")).toBe(true);
302+
303+
const behavior = getBehavior(mockUI, "recaptchaVerification");
304+
const result = behavior(mockUI, mockElement);
305+
306+
expect(RecaptchaVerifier).toHaveBeenCalledWith(mockUI.auth, mockElement, {
307+
size: "invisible",
308+
theme: "light",
309+
tabindex: 0,
310+
});
311+
expect(result).toBe(mockRecaptchaVerifier);
312+
});
313+
314+
it("should throw error when trying to get non-existent recaptchaVerification behavior", () => {
315+
const mockUI = createMockUI();
316+
317+
expect(hasBehavior(mockUI, "recaptchaVerification")).toBe(false);
318+
expect(() => getBehavior(mockUI, "recaptchaVerification")).toThrow("Behavior recaptchaVerification not found");
319+
});
320+
});
321+
322+

packages/core/src/behaviors.ts

Lines changed: 23 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
signInAnonymously,
2323
User,
2424
UserCredential,
25+
RecaptchaVerifier,
2526
} from "firebase/auth";
2627
import { FirebaseUIConfiguration } from "./config";
2728

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

3739
export type Behavior<T extends keyof BehaviorHandlers = keyof BehaviorHandlers> = Pick<BehaviorHandlers, T>;
@@ -112,40 +114,24 @@ export function autoUpgradeAnonymousUsers(): Behavior<
112114
};
113115
}
114116

115-
// export function autoUpgradeAnonymousCredential(): RegisteredBehavior<'autoUpgradeAnonymousCredential'> {
116-
// return {
117-
// key: 'autoUpgradeAnonymousCredential',
118-
// handler: async (auth, credential) => {
119-
// const currentUser = auth.currentUser;
120-
121-
// // Check if the user is anonymous. If not, we can't upgrade them.
122-
// if (!currentUser?.isAnonymous) {
123-
// return;
124-
// }
125-
126-
// $state.set('linking');
127-
// const result = await linkWithCredential(currentUser, credential);
128-
// $state.set('idle');
129-
// return result;
130-
// },
131-
// };
132-
// }
133-
134-
// export function autoUpgradeAnonymousProvider(): RegisteredBehavior<'autoUpgradeAnonymousCredential'> {
135-
// return {
136-
// key: 'autoUpgradeAnonymousProvider',
137-
// handler: async (auth, credential) => {
138-
// const currentUser = auth.currentUser;
139-
140-
// // Check if the user is anonymous. If not, we can't upgrade them.
141-
// if (!currentUser?.isAnonymous) {
142-
// return;
143-
// }
144-
145-
// $state.set('linking');
146-
// const result = await linkWithRedirect(currentUser, credential);
147-
// $state.set('idle');
148-
// return result;
149-
// },
150-
// };
151-
// }
117+
export type RecaptchaVerification = {
118+
size?: "normal" | "invisible" | "compact";
119+
theme?: "light" | "dark";
120+
tabindex?: number;
121+
};
122+
123+
export function recaptchaVerification(options?: RecaptchaVerification): Behavior<"recaptchaVerification"> {
124+
return {
125+
recaptchaVerification: (ui, element) => {
126+
return new RecaptchaVerifier(ui.auth, element, {
127+
size: options?.size ?? "invisible",
128+
theme: options?.theme ?? "light",
129+
tabindex: options?.tabindex ?? 0,
130+
});
131+
},
132+
};
133+
}
134+
135+
export const defaultBehaviors = {
136+
...recaptchaVerification(),
137+
};

packages/core/src/config.test.ts

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Auth } from "firebase/auth";
33
import { describe, it, expect } from "vitest";
44
import { initializeUI } from "./config";
55
import { enUs, registerLocale } from "@firebase-ui/translations";
6-
import { autoUpgradeAnonymousUsers } from "./behaviors";
6+
import { autoUpgradeAnonymousUsers, defaultBehaviors } from "./behaviors";
77

88
describe('initializeUI', () => {
99
it('should return a valid deep store with default values', () => {
@@ -17,12 +17,12 @@ describe('initializeUI', () => {
1717
expect(ui.get()).toBeDefined();
1818
expect(ui.get().app).toBe(config.app);
1919
expect(ui.get().auth).toBe(config.auth);
20-
expect(ui.get().behaviors).toEqual({});
20+
expect(ui.get().behaviors).toEqual(defaultBehaviors);
2121
expect(ui.get().state).toEqual("idle");
2222
expect(ui.get().locale).toEqual(enUs);
2323
});
2424

25-
it('should merge behaviors', () => {
25+
it('should merge behaviors with defaultBehaviors', () => {
2626
const config = {
2727
app: {} as FirebaseApp,
2828
auth: {} as Auth,
@@ -32,6 +32,11 @@ describe('initializeUI', () => {
3232
const ui = initializeUI(config);
3333
expect(ui).toBeDefined();
3434
expect(ui.get()).toBeDefined();
35+
36+
// Should have default behaviors
37+
expect(ui.get().behaviors).toHaveProperty("recaptchaVerification");
38+
39+
// Should have custom behaviors
3540
expect(ui.get().behaviors).toHaveProperty("autoUpgradeAnonymousCredential");
3641
expect(ui.get().behaviors).toHaveProperty("autoUpgradeAnonymousProvider");
3742
});
@@ -66,5 +71,63 @@ describe('initializeUI', () => {
6671
ui.get().setLocale(testLocale2);
6772
expect(ui.get().locale.locale).toEqual('test2');
6873
});
74+
75+
it('should include defaultBehaviors even when no custom behaviors are provided', () => {
76+
const config = {
77+
app: {} as FirebaseApp,
78+
auth: {} as Auth,
79+
};
80+
81+
const ui = initializeUI(config);
82+
expect(ui.get().behaviors).toEqual(defaultBehaviors);
83+
expect(ui.get().behaviors).toHaveProperty("recaptchaVerification");
84+
});
85+
86+
it('should allow overriding default behaviors', () => {
87+
const customRecaptchaVerification = {
88+
recaptchaVerification: () => {
89+
// Custom implementation
90+
return {} as any;
91+
}
92+
};
93+
94+
const config = {
95+
app: {} as FirebaseApp,
96+
auth: {} as Auth,
97+
behaviors: [customRecaptchaVerification],
98+
};
99+
100+
const ui = initializeUI(config);
101+
expect(ui.get().behaviors).toHaveProperty("recaptchaVerification");
102+
expect(ui.get().behaviors.recaptchaVerification).toBe(customRecaptchaVerification.recaptchaVerification);
103+
});
104+
105+
it('should merge multiple behavior objects correctly', () => {
106+
const behavior1 = autoUpgradeAnonymousUsers();
107+
const behavior2 = {
108+
recaptchaVerification: () => {
109+
// Custom recaptcha implementation
110+
return {} as any;
111+
}
112+
};
113+
114+
const config = {
115+
app: {} as FirebaseApp,
116+
auth: {} as Auth,
117+
behaviors: [behavior1, behavior2],
118+
};
119+
120+
const ui = initializeUI(config);
121+
122+
// Should have default behaviors
123+
expect(ui.get().behaviors).toHaveProperty("recaptchaVerification");
124+
125+
// Should have autoUpgrade behaviors
126+
expect(ui.get().behaviors).toHaveProperty("autoUpgradeAnonymousCredential");
127+
expect(ui.get().behaviors).toHaveProperty("autoUpgradeAnonymousProvider");
128+
129+
// Should have custom recaptcha implementation
130+
expect(ui.get().behaviors.recaptchaVerification).toBe(behavior2.recaptchaVerification);
131+
});
69132
});
70133

packages/core/src/config.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,14 @@ import { enUs, RegisteredLocale } from "@firebase-ui/translations";
1818
import type { FirebaseApp } from "firebase/app";
1919
import { Auth, getAuth } from "firebase/auth";
2020
import { deepMap, DeepMapStore, map } from "nanostores";
21-
import { Behavior, type BehaviorHandlers, type BehaviorKey, getBehavior, hasBehavior } from "./behaviors";
21+
import {
22+
Behavior,
23+
type BehaviorHandlers,
24+
type BehaviorKey,
25+
defaultBehaviors,
26+
getBehavior,
27+
hasBehavior,
28+
} from "./behaviors";
2229
import { FirebaseUIState } from "./state";
2330

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

4552
export function initializeUI(config: FirebaseUIConfigurationOptions, name: string = "[DEFAULT]"): FirebaseUI {
4653
// Reduce the behaviors to a single object.
47-
const behaviors = config.behaviors?.reduce(
54+
const behaviors = config.behaviors?.reduce<Partial<Record<BehaviorKey, BehaviorHandlers[BehaviorKey]>>>(
4855
(acc, behavior) => {
4956
return {
5057
...acc,
5158
...behavior,
5259
};
5360
},
54-
{} as Record<BehaviorKey, BehaviorHandlers[BehaviorKey]>
61+
defaultBehaviors
5562
);
5663

5764
$config.setKey(
@@ -69,7 +76,7 @@ export function initializeUI(config: FirebaseUIConfigurationOptions, name: strin
6976
const current = $config.get()[name]!;
7077
current.setKey(`state`, state);
7178
},
72-
behaviors: behaviors ?? {},
79+
behaviors: behaviors ?? defaultBehaviors,
7380
})
7481
);
7582

0 commit comments

Comments
 (0)