@@ -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+ } ) ;
0 commit comments