diff --git a/packages/passport/sdk/src/Passport.int.test.ts b/packages/passport/sdk/src/Passport.int.test.ts index 487bafb046..13a430ef9e 100644 --- a/packages/passport/sdk/src/Passport.int.test.ts +++ b/packages/passport/sdk/src/Passport.int.test.ts @@ -111,6 +111,14 @@ describe('Passport', () => { close: jest.fn(), }); + // Mock crypto.randomUUID for authManager login functionality + Object.defineProperty(window, 'crypto', { + value: { + randomUUID: jest.fn().mockReturnValue('mock-uuid-12345'), + }, + writable: true, + }); + mockMagicUserIsLoggedIn.mockResolvedValue(true); (UserManager as jest.Mock).mockImplementation(() => ({ signinPopup: mockSigninPopup, diff --git a/packages/passport/sdk/src/Passport.test.ts b/packages/passport/sdk/src/Passport.test.ts index 26ccc5a7b3..99da4547b1 100644 --- a/packages/passport/sdk/src/Passport.test.ts +++ b/packages/passport/sdk/src/Passport.test.ts @@ -550,94 +550,6 @@ describe('Passport', () => { expect(getUserMock).toBeCalledTimes(1); expect(authLoginMock).toBeCalledTimes(1); }); - - it('should debounce concurrent login calls and return the same result', async () => { - getUserMock.mockReturnValue(null); - authLoginMock.mockReturnValue(mockUserImx); - - // Make multiple concurrent login calls - const loginPromise1 = passport.login(); - const loginPromise2 = passport.login(); - const loginPromise3 = passport.login(); - - // All promises should be the same reference - expect(loginPromise1).toEqual(loginPromise2); - expect(loginPromise2).toEqual(loginPromise3); - - // Wait for all to complete - const [result1, result2, result3] = await Promise.all([ - loginPromise1, - loginPromise2, - loginPromise3, - ]); - - // All results should be the same - expect(result1).toEqual(mockUserImx.profile); - expect(result2).toEqual(mockUserImx.profile); - expect(result3).toEqual(mockUserImx.profile); - - // AuthManager.login should only be called once despite multiple login calls - expect(authLoginMock).toBeCalledTimes(1); - }); - - it('should reset login promise after successful completion and allow new login calls', async () => { - getUserMock.mockReturnValue(null); - authLoginMock.mockReturnValue(mockUserImx); - - // First login call - const firstLoginResult = await passport.login(); - expect(firstLoginResult).toEqual(mockUserImx.profile); - - // Second login call after first completes should create a new login process - const secondLoginResult = await passport.login(); - expect(secondLoginResult).toEqual(mockUserImx.profile); - - // AuthManager.login should be called twice (once for each login) - expect(authLoginMock).toBeCalledTimes(2); - }); - - it('should reset login promise after failed completion and allow new login calls', async () => { - const error = new Error('Login failed'); - getUserMock.mockReturnValue(null); - authLoginMock.mockRejectedValue(error); - - // First login call should fail - await expect(passport.login()).rejects.toThrow(error); - - // Second login call after first fails should create a new login process - authLoginMock.mockReturnValue(mockUserImx); - const secondLoginResult = await passport.login(); - expect(secondLoginResult).toEqual(mockUserImx.profile); - - // AuthManager.login should be called twice (once for failed, once for successful) - expect(authLoginMock).toBeCalledTimes(2); - }); - - it('should debounce concurrent login calls even when they fail', async () => { - const error = new Error('Login failed'); - getUserMock.mockReturnValue(null); - authLoginMock.mockRejectedValue(error); - - // Make multiple concurrent login calls that will fail - const [loginPromise1, loginPromise2, loginPromise3] = [ - passport.login(), - passport.login(), - passport.login(), - ]; - - // All promises should be the same reference - expect(loginPromise1).toEqual(loginPromise2); - expect(loginPromise2).toEqual(loginPromise3); - - // All should reject with the same error - try { - await Promise.all([loginPromise1, loginPromise2, loginPromise3]); - } catch (e: unknown) { - expect(e).toEqual(error); - // AuthManager.login should only be called once despite multiple login calls - expect(authLoginMock).toBeCalledTimes(1); - } - }); }); describe('linkExternalWallet', () => { diff --git a/packages/passport/sdk/src/Passport.ts b/packages/passport/sdk/src/Passport.ts index f3a7af3127..272ed7e5fd 100644 --- a/packages/passport/sdk/src/Passport.ts +++ b/packages/passport/sdk/src/Passport.ts @@ -182,8 +182,6 @@ export class Passport { }, 'connectEvm', false); } - #loginPromise: Promise | null = null; - /** * Initiates the login process. * @param {Object} options - Login options @@ -207,13 +205,7 @@ export class Passport { useRedirectFlow?: boolean; directLoginOptions?: DirectLoginOptions; }): Promise { - // If there's already a login in progress, return that promise - if (this.#loginPromise) { - return this.#loginPromise; - } - - // Create and store the login promise - this.#loginPromise = withMetricsAsync(async () => { + return withMetricsAsync(async () => { const { useCachedSession = false, useSilentLogin } = options || {}; let user: User | null = null; @@ -248,14 +240,6 @@ export class Passport { return user ? user.profile : null; }, 'login'); - - try { - const result = await this.#loginPromise; - return result; - } finally { - // Reset the login promise when the login process completes - this.#loginPromise = null; - } } /** diff --git a/packages/passport/sdk/src/authManager.test.ts b/packages/passport/sdk/src/authManager.test.ts index 13031504e7..c790348ff4 100644 --- a/packages/passport/sdk/src/authManager.test.ts +++ b/packages/passport/sdk/src/authManager.test.ts @@ -104,6 +104,14 @@ describe('AuthManager', () => { close: jest.fn(), }); + // Mock crypto.randomUUID globally for all tests + Object.defineProperty(window, 'crypto', { + value: { + randomUUID: jest.fn().mockReturnValue('mock-uuid-12345'), + }, + writable: true, + }); + mockSigninPopup = jest.fn(); mockSigninCallback = jest.fn(); mockSigninRedirectCallback = jest.fn(); @@ -440,6 +448,48 @@ describe('AuthManager', () => { }); }); + describe('when tryAgainOnClick is called multiple times', () => { + it('should call window.open with the same popupWindowTarget to focus existing popup', async () => { + let tryAgainCallback: (() => Promise) | undefined; + + mockOverlayAppend.mockImplementation(async (tryAgain: () => Promise) => { + tryAgainCallback = tryAgain; + // First call - should open new popup + await tryAgain(); + }); + + mockSigninPopup.mockReturnValue(mockOidcUser); + + const loginPromise = authManager.login(); + + // Wait for the overlay to be set up + await new Promise((resolve) => { setTimeout(resolve, 10); }); + + // Verify the popupWindowTarget was generated + const expectedTarget = 'mock-uuid-12345'; + + // Verify first signinPopup call used the target + expect(mockSigninPopup).toHaveBeenCalledWith( + expect.objectContaining({ + popupWindowTarget: expectedTarget, + }), + ); + + // Reset window.open mock to track subsequent calls + (window.open as jest.Mock).mockClear(); + + // Call tryAgain again - should focus existing popup + if (tryAgainCallback) { + await tryAgainCallback(); + } + + // Verify window.open was called with empty URL and the same target + expect(window.open).toHaveBeenCalledWith('', expectedTarget); + + await loginPromise; + }); + }); + describe('when onCloseClick is called', () => { it('should remove the overlay', async () => { mockOverlayAppend.mockImplementation(async (_: () => Promise, onCloseClick: () => void) => { @@ -1029,7 +1079,7 @@ describe('AuthManager', () => { width: 410, height: 450, }, - popupWindowTarget: 'passportLoginPrompt', + popupWindowTarget: expect.any(String), }); }); @@ -1047,7 +1097,7 @@ describe('AuthManager', () => { width: 410, height: 450, }, - popupWindowTarget: 'passportLoginPrompt', + popupWindowTarget: expect.any(String), }); }); }); diff --git a/packages/passport/sdk/src/authManager.ts b/packages/passport/sdk/src/authManager.ts index c3e4be50e5..c0702d80a3 100644 --- a/packages/passport/sdk/src/authManager.ts +++ b/packages/passport/sdk/src/authManager.ts @@ -228,7 +228,7 @@ export default class AuthManager { */ public async login(anonymousId?: string, directLoginOptions?: DirectLoginOptions): Promise { return withPassportError(async () => { - const popupWindowTarget = 'passportLoginPrompt'; + const popupWindowTarget = window.crypto.randomUUID(); const signinPopup = async () => { const extraQueryParams = this.buildExtraQueryParams(anonymousId, directLoginOptions);