Skip to content

Commit 5174a4c

Browse files
committed
fix(core): Ensure completeEmailLinkSignIn handles anon upgrade behavior
1 parent dd0d5bb commit 5174a4c

File tree

2 files changed

+215
-0
lines changed

2 files changed

+215
-0
lines changed

packages/core/src/auth.test.ts

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
signInWithProvider,
1313
signInWithCustomToken,
1414
generateTotpQrCode,
15+
completeEmailLinkSignIn,
1516
} from "./auth";
1617

1718
vi.mock("firebase/auth", () => ({
@@ -1226,3 +1227,207 @@ describe("generateTotpQrCode", () => {
12261227
expect(mockSecret.generateQrCodeUrl).not.toHaveBeenCalled();
12271228
});
12281229
});
1230+
1231+
describe("completeEmailLinkSignIn", () => {
1232+
beforeEach(() => {
1233+
vi.clearAllMocks();
1234+
Object.defineProperty(window, "location", {
1235+
value: { href: "https://example.com/auth?oobCode=abc123" },
1236+
writable: true,
1237+
});
1238+
});
1239+
1240+
afterEach(() => {
1241+
window.localStorage.clear();
1242+
});
1243+
1244+
it("should return null when URL is not an email link", async () => {
1245+
const mockUI = createMockUI();
1246+
const currentUrl = "https://example.com/not-email-link";
1247+
1248+
vi.mocked(_isSignInWithEmailLink).mockReturnValue(false);
1249+
1250+
const result = await completeEmailLinkSignIn(mockUI, currentUrl);
1251+
1252+
expect(_isSignInWithEmailLink).toHaveBeenCalledWith(mockUI.auth, currentUrl);
1253+
expect(result).toBeNull();
1254+
1255+
expect(vi.mocked(mockUI.setState).mock.calls).toEqual([["idle"]]);
1256+
});
1257+
1258+
it("should return null when no email is stored in localStorage", async () => {
1259+
const mockUI = createMockUI();
1260+
const currentUrl = "https://example.com/auth?oobCode=abc123";
1261+
1262+
vi.mocked(_isSignInWithEmailLink).mockReturnValue(true);
1263+
1264+
const result = await completeEmailLinkSignIn(mockUI, currentUrl);
1265+
1266+
expect(_isSignInWithEmailLink).toHaveBeenCalledWith(mockUI.auth, currentUrl);
1267+
expect(result).toBeNull();
1268+
expect(vi.mocked(mockUI.setState).mock.calls).toEqual([["idle"]]);
1269+
});
1270+
1271+
it("should complete email link sign-in with no behavior", async () => {
1272+
const mockUI = createMockUI();
1273+
const currentUrl = "https://example.com/auth?oobCode=abc123";
1274+
const email = "test@example.com";
1275+
const mockCredential = { providerId: "emailLink" } as UserCredential;
1276+
const emailLinkCredential = { providerId: "emailLink" } as any;
1277+
1278+
vi.mocked(_isSignInWithEmailLink).mockReturnValue(true);
1279+
window.localStorage.setItem("emailForSignIn", email);
1280+
vi.mocked(hasBehavior).mockReturnValue(false);
1281+
vi.mocked(EmailAuthProvider.credentialWithLink).mockReturnValue(emailLinkCredential);
1282+
vi.mocked(_signInWithCredential).mockResolvedValue(mockCredential);
1283+
1284+
const result = await completeEmailLinkSignIn(mockUI, currentUrl);
1285+
1286+
expect(_isSignInWithEmailLink).toHaveBeenCalledWith(mockUI.auth, currentUrl);
1287+
expect(hasBehavior).toHaveBeenCalledWith(mockUI, "autoUpgradeAnonymousCredential");
1288+
expect(EmailAuthProvider.credentialWithLink).toHaveBeenCalledWith(email, currentUrl);
1289+
expect(_signInWithCredential).toHaveBeenCalledWith(mockUI.auth, emailLinkCredential);
1290+
expect(result).toBe(mockCredential);
1291+
expect(vi.mocked(mockUI.setState).mock.calls).toEqual([["pending"], ["pending"], ["idle"], ["idle"]]);
1292+
expect(window.localStorage.getItem("emailForSignIn")).toBeNull();
1293+
});
1294+
1295+
it("should call autoUpgradeAnonymousCredential behavior when enabled and return result", async () => {
1296+
const mockUI = createMockUI();
1297+
const currentUrl = "https://example.com/auth?oobCode=abc123";
1298+
const email = "test@example.com";
1299+
const emailLinkCredential = { providerId: "emailLink" } as any;
1300+
const mockResult = { providerId: "upgraded" } as UserCredential;
1301+
1302+
vi.mocked(_isSignInWithEmailLink).mockReturnValue(true);
1303+
window.localStorage.setItem("emailForSignIn", email);
1304+
vi.mocked(hasBehavior).mockReturnValue(true);
1305+
vi.mocked(EmailAuthProvider.credentialWithLink).mockReturnValue(emailLinkCredential);
1306+
const mockBehavior = vi.fn().mockResolvedValue(mockResult);
1307+
vi.mocked(getBehavior).mockReturnValue(mockBehavior);
1308+
1309+
const result = await completeEmailLinkSignIn(mockUI, currentUrl);
1310+
1311+
expect(_isSignInWithEmailLink).toHaveBeenCalledWith(mockUI.auth, currentUrl);
1312+
expect(hasBehavior).toHaveBeenCalledWith(mockUI, "autoUpgradeAnonymousCredential");
1313+
expect(EmailAuthProvider.credentialWithLink).toHaveBeenCalledWith(email, currentUrl);
1314+
expect(getBehavior).toHaveBeenCalledWith(mockUI, "autoUpgradeAnonymousCredential");
1315+
expect(mockBehavior).toHaveBeenCalledWith(mockUI, emailLinkCredential);
1316+
expect(result).toBe(mockResult);
1317+
expect(vi.mocked(mockUI.setState).mock.calls).toEqual([["pending"], ["idle"]]);
1318+
expect(window.localStorage.getItem("emailForSignIn")).toBeNull();
1319+
});
1320+
1321+
it("should fall back to signInWithEmailLink when autoUpgradeAnonymousCredential behavior returns undefined", async () => {
1322+
const mockUI = createMockUI();
1323+
const currentUrl = "https://example.com/auth?oobCode=abc123";
1324+
const email = "test@example.com";
1325+
const emailLinkCredential = { providerId: "emailLink" } as any;
1326+
const mockResult = { providerId: "emailLink" } as UserCredential;
1327+
1328+
vi.mocked(_isSignInWithEmailLink).mockReturnValue(true);
1329+
window.localStorage.setItem("emailForSignIn", email);
1330+
vi.mocked(hasBehavior).mockReturnValue(true);
1331+
vi.mocked(EmailAuthProvider.credentialWithLink).mockReturnValue(emailLinkCredential);
1332+
const mockBehavior = vi.fn().mockResolvedValue(undefined);
1333+
vi.mocked(getBehavior).mockReturnValue(mockBehavior);
1334+
vi.mocked(_signInWithCredential).mockResolvedValue(mockResult);
1335+
1336+
const result = await completeEmailLinkSignIn(mockUI, currentUrl);
1337+
1338+
expect(_isSignInWithEmailLink).toHaveBeenCalledWith(mockUI.auth, currentUrl);
1339+
expect(hasBehavior).toHaveBeenCalledWith(mockUI, "autoUpgradeAnonymousCredential");
1340+
expect(EmailAuthProvider.credentialWithLink).toHaveBeenCalledWith(email, currentUrl);
1341+
expect(getBehavior).toHaveBeenCalledWith(mockUI, "autoUpgradeAnonymousCredential");
1342+
expect(mockBehavior).toHaveBeenCalledWith(mockUI, emailLinkCredential);
1343+
expect(_signInWithCredential).toHaveBeenCalledWith(mockUI.auth, emailLinkCredential);
1344+
expect(result).toBe(mockResult);
1345+
expect(vi.mocked(mockUI.setState).mock.calls).toEqual([["pending"], ["pending"], ["idle"], ["idle"]]);
1346+
expect(window.localStorage.getItem("emailForSignIn")).toBeNull();
1347+
});
1348+
1349+
it("should call handleFirebaseError if an error is thrown", async () => {
1350+
const mockUI = createMockUI();
1351+
const currentUrl = "https://example.com/auth?oobCode=abc123";
1352+
const email = "test@example.com";
1353+
const error = new FirebaseError("auth/invalid-action-code", "Invalid action code");
1354+
const emailLinkCredential = { providerId: "emailLink" } as any;
1355+
1356+
vi.mocked(_isSignInWithEmailLink).mockReturnValue(true);
1357+
window.localStorage.setItem("emailForSignIn", email);
1358+
vi.mocked(hasBehavior).mockReturnValue(false);
1359+
vi.mocked(EmailAuthProvider.credentialWithLink).mockReturnValue(emailLinkCredential);
1360+
vi.mocked(_signInWithCredential).mockRejectedValue(error);
1361+
1362+
const result = await completeEmailLinkSignIn(mockUI, currentUrl);
1363+
1364+
expect(handleFirebaseError).toHaveBeenCalledWith(mockUI, error);
1365+
expect(vi.mocked(mockUI.setState).mock.calls).toEqual([["pending"], ["pending"], ["idle"], ["idle"]]);
1366+
expect(window.localStorage.getItem("emailForSignIn")).toBeNull();
1367+
expect(result).toBeUndefined();
1368+
});
1369+
1370+
it("should call handleFirebaseError if autoUpgradeAnonymousCredential behavior throws error", async () => {
1371+
const mockUI = createMockUI();
1372+
const currentUrl = "https://example.com/auth?oobCode=abc123";
1373+
const email = "test@example.com";
1374+
const emailLinkCredential = { providerId: "emailLink" } as any;
1375+
const error = new Error("Behavior error");
1376+
1377+
vi.mocked(_isSignInWithEmailLink).mockReturnValue(true);
1378+
window.localStorage.setItem("emailForSignIn", email);
1379+
vi.mocked(hasBehavior).mockReturnValue(true);
1380+
vi.mocked(EmailAuthProvider.credentialWithLink).mockReturnValue(emailLinkCredential);
1381+
const mockBehavior = vi.fn().mockRejectedValue(error);
1382+
vi.mocked(getBehavior).mockReturnValue(mockBehavior);
1383+
1384+
const result = await completeEmailLinkSignIn(mockUI, currentUrl);
1385+
1386+
expect(handleFirebaseError).toHaveBeenCalledWith(mockUI, error);
1387+
expect(vi.mocked(mockUI.setState).mock.calls).toEqual([["pending"], ["idle"]]);
1388+
expect(window.localStorage.getItem("emailForSignIn")).toBeNull();
1389+
expect(result).toBeUndefined();
1390+
});
1391+
1392+
it("should clear email from localStorage even when error occurs", async () => {
1393+
const mockUI = createMockUI();
1394+
const currentUrl = "https://example.com/auth?oobCode=abc123";
1395+
const email = "test@example.com";
1396+
const error = new FirebaseError("auth/invalid-action-code", "Invalid action code");
1397+
const emailLinkCredential = { providerId: "emailLink" } as any;
1398+
1399+
vi.mocked(_isSignInWithEmailLink).mockReturnValue(true);
1400+
window.localStorage.setItem("emailForSignIn", email);
1401+
vi.mocked(hasBehavior).mockReturnValue(false);
1402+
vi.mocked(EmailAuthProvider.credentialWithLink).mockReturnValue(emailLinkCredential);
1403+
vi.mocked(_signInWithCredential).mockRejectedValue(error);
1404+
1405+
await completeEmailLinkSignIn(mockUI, currentUrl);
1406+
1407+
expect(window.localStorage.getItem("emailForSignIn")).toBeNull();
1408+
});
1409+
1410+
it("should clear email from localStorage even when URL is not an email link", async () => {
1411+
const mockUI = createMockUI();
1412+
const currentUrl = "https://example.com/not-email-link";
1413+
const email = "test@example.com";
1414+
1415+
vi.mocked(_isSignInWithEmailLink).mockReturnValue(false);
1416+
window.localStorage.setItem("emailForSignIn", email);
1417+
1418+
await completeEmailLinkSignIn(mockUI, currentUrl);
1419+
1420+
expect(window.localStorage.getItem("emailForSignIn")).toBeNull();
1421+
});
1422+
1423+
it("should not clear email from localStorage when no email is stored", async () => {
1424+
const mockUI = createMockUI();
1425+
const currentUrl = "https://example.com/auth?oobCode=abc123";
1426+
1427+
vi.mocked(_isSignInWithEmailLink).mockReturnValue(true);
1428+
1429+
await completeEmailLinkSignIn(mockUI, currentUrl);
1430+
1431+
expect(window.localStorage.getItem("emailForSignIn")).toBeNull();
1432+
});
1433+
});

packages/core/src/auth.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,16 @@ export async function completeEmailLinkSignIn(ui: FirebaseUI, currentUrl: string
315315
if (!email) return null;
316316

317317
setPendingState(ui);
318+
319+
if (hasBehavior(ui, "autoUpgradeAnonymousCredential")) {
320+
const emailLinkCredential = EmailAuthProvider.credentialWithLink(email, currentUrl);
321+
const credential = await getBehavior(ui, "autoUpgradeAnonymousCredential")(ui, emailLinkCredential);
322+
323+
if (credential) {
324+
return handlePendingCredential(ui, credential);
325+
}
326+
}
327+
318328
const result = await signInWithEmailLink(ui, email, currentUrl);
319329
return handlePendingCredential(ui, result);
320330
} catch (error) {

0 commit comments

Comments
 (0)