Skip to content

Commit e42e891

Browse files
authored
Merge pull request #1243 from firebase/@invertase/bb-45
2 parents 40ca180 + 314401f commit e42e891

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
signInWithMultiFactorAssertion,
1617
} from "./auth";
1718

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

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)