Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/passport/sdk/src/Passport.int.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
88 changes: 0 additions & 88 deletions packages/passport/sdk/src/Passport.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
18 changes: 1 addition & 17 deletions packages/passport/sdk/src/Passport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,6 @@ export class Passport {
}, 'connectEvm', false);
}

#loginPromise: Promise<UserProfile | null> | null = null;

/**
* Initiates the login process.
* @param {Object} options - Login options
Expand All @@ -207,13 +205,7 @@ export class Passport {
useRedirectFlow?: boolean;
directLoginOptions?: DirectLoginOptions;
}): Promise<UserProfile | null> {
// 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;

Expand Down Expand Up @@ -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;
}
}

/**
Expand Down
54 changes: 52 additions & 2 deletions packages/passport/sdk/src/authManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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<void>) | undefined;

mockOverlayAppend.mockImplementation(async (tryAgain: () => Promise<void>) => {
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<void>, onCloseClick: () => void) => {
Expand Down Expand Up @@ -1029,7 +1079,7 @@ describe('AuthManager', () => {
width: 410,
height: 450,
},
popupWindowTarget: 'passportLoginPrompt',
popupWindowTarget: expect.any(String),
});
});

Expand All @@ -1047,7 +1097,7 @@ describe('AuthManager', () => {
width: 410,
height: 450,
},
popupWindowTarget: 'passportLoginPrompt',
popupWindowTarget: expect.any(String),
});
});
});
Expand Down
2 changes: 1 addition & 1 deletion packages/passport/sdk/src/authManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ export default class AuthManager {
*/
public async login(anonymousId?: string, directLoginOptions?: DirectLoginOptions): Promise<User> {
return withPassportError<User>(async () => {
const popupWindowTarget = 'passportLoginPrompt';
const popupWindowTarget = window.crypto.randomUUID();
const signinPopup = async () => {
const extraQueryParams = this.buildExtraQueryParams(anonymousId, directLoginOptions);

Expand Down