diff --git a/packages/passport/sdk/src/Passport.test.ts b/packages/passport/sdk/src/Passport.test.ts index ae886e32b8..4a8a2f641d 100644 --- a/packages/passport/sdk/src/Passport.test.ts +++ b/packages/passport/sdk/src/Passport.test.ts @@ -583,6 +583,94 @@ 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 0932763495..c4c3922eb6 100644 --- a/packages/passport/sdk/src/Passport.ts +++ b/packages/passport/sdk/src/Passport.ts @@ -183,6 +183,8 @@ export class Passport { }, 'connectEvm', false); } + #loginPromise: Promise | null = null; + /** * Initiates the login process. * @param {Object} options - Login options @@ -200,7 +202,13 @@ export class Passport { useSilentLogin?: boolean; useRedirectFlow?: boolean; }): Promise { - return withMetricsAsync(async () => { + // 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 () => { const { useCachedSession = false, useSilentLogin } = options || {}; let user: User | null = null; @@ -235,6 +243,14 @@ 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; + } } /**