diff --git a/packages/game-bridge/src/index.ts b/packages/game-bridge/src/index.ts index 35bce75434..633d75a871 100644 --- a/packages/game-bridge/src/index.ts +++ b/packages/game-bridge/src/index.ts @@ -220,7 +220,7 @@ window.callFunction = async (jsonData: string) => { case PASSPORT_FUNCTIONS.init: { const request = JSON.parse(data); const redirect: string | null = request?.redirectUri; - const logoutMode: 'silent' | 'redirect' = request?.logoutMode === 'silent' ? 'silent' : 'redirect'; + const logoutMode: 'silent' | 'redirect' = request?.isSilentLogout === true ? 'silent' : 'redirect'; if (!passportClient) { const passportConfig = { baseConfig: new config.ImmutableConfiguration({ diff --git a/packages/passport/sdk/src/Passport.int.test.ts b/packages/passport/sdk/src/Passport.int.test.ts index b7e915bf91..0cb5a821fd 100644 --- a/packages/passport/sdk/src/Passport.int.test.ts +++ b/packages/passport/sdk/src/Passport.int.test.ts @@ -93,6 +93,7 @@ describe('Passport', () => { (Magic as jest.Mock).mockImplementation(() => ({ openid: { loginWithOIDC: mockLoginWithOidc }, rpcProvider: { request: mockMagicRequest }, + preload: jest.fn(), })); }); diff --git a/packages/passport/sdk/src/magicAdapter.test.ts b/packages/passport/sdk/src/magicAdapter.test.ts index acad6c4252..762f99621d 100644 --- a/packages/passport/sdk/src/magicAdapter.test.ts +++ b/packages/passport/sdk/src/magicAdapter.test.ts @@ -4,7 +4,7 @@ import MagicAdapter from './magicAdapter'; import { PassportConfiguration } from './config'; import { PassportError, PassportErrorType } from './errors/passportError'; -const loginWithOIDCMock: jest.MockedFunction<(args: LoginWithOpenIdParams) => Promise> = jest.fn(); +const loginWithOIDCMock:jest.MockedFunction<(args: LoginWithOpenIdParams) => Promise> = jest.fn(); const rpcProvider = {}; @@ -23,6 +23,7 @@ describe('MagicWallet', () => { magicProviderId: providerId, } as PassportConfiguration; const idToken = 'e30=.e30=.e30='; + const preload = jest.fn(); beforeEach(() => { jest.resetAllMocks(); @@ -34,6 +35,7 @@ describe('MagicWallet', () => { logout: logoutMock, }, rpcProvider, + preload, })); }); @@ -54,9 +56,10 @@ describe('MagicWallet', () => { }); it('starts initialising the magicClient', () => { jest.spyOn(window.document, 'readyState', 'get').mockReturnValue('complete'); + preload.mockResolvedValue(Promise.resolve()); const magicAdapter = new MagicAdapter(config); - // @ts-expect-error: client is private - expect(magicAdapter.client).toBeDefined(); + // @ts-ignore + expect(magicAdapter.lazyMagicClient).toBeDefined(); }); }); @@ -72,31 +75,15 @@ describe('MagicWallet', () => { it('does nothing', () => { const magicAdapter = new MagicAdapter(config); - // @ts-expect-error: client is private - expect(magicAdapter.client).toBeUndefined(); - }); - - it('should throw a browser error for loginWithOIDC', async () => { - const magicAdapter = new MagicAdapter(config); - - let type = ''; - let message = ''; - - try { - await magicAdapter.login(idToken); - } catch (e: any) { - type = e.type; - message = e.message; - } - - expect(type).toEqual(PassportErrorType.WALLET_CONNECTION_ERROR); - expect(message).toEqual('Cannot perform this action outside of the browser'); + // @ts-ignore + expect(magicAdapter.magicClientPromise).toBeUndefined(); }); }); }); describe('login', () => { it('should call loginWithOIDC and initialise the provider with the correct arguments', async () => { + preload.mockResolvedValue(Promise.resolve()); const magicAdapter = new MagicAdapter(config); const magicProvider = await magicAdapter.login(idToken); @@ -114,6 +101,7 @@ describe('MagicWallet', () => { }); it('should throw a PassportError when an error is thrown', async () => { + preload.mockResolvedValue(Promise.resolve()); const magicAdapter = new MagicAdapter(config); loginWithOIDCMock.mockImplementation(() => { @@ -133,6 +121,7 @@ describe('MagicWallet', () => { describe('logout', () => { it('calls the logout function', async () => { + preload.mockResolvedValue(Promise.resolve()); const magicAdapter = new MagicAdapter(config); await magicAdapter.login(idToken); await magicAdapter.logout(); diff --git a/packages/passport/sdk/src/magicAdapter.ts b/packages/passport/sdk/src/magicAdapter.ts index c30cb1690d..7e0d512e0d 100644 --- a/packages/passport/sdk/src/magicAdapter.ts +++ b/packages/passport/sdk/src/magicAdapter.ts @@ -5,6 +5,7 @@ import { ethers } from 'ethers'; import { trackDuration } from '@imtbl/metrics'; import { PassportErrorType, withPassportError } from './errors/passportError'; import { PassportConfiguration } from './config'; +import { lazyDocumentReady } from './utils/lazyLoad'; type MagicClient = InstanceWithExtensions; @@ -13,24 +14,28 @@ const MAINNET = 'mainnet'; export default class MagicAdapter { private readonly config: PassportConfiguration; - private readonly client?: MagicClient; + private readonly lazyMagicClient?: Promise; constructor(config: PassportConfiguration) { this.config = config; if (typeof window !== 'undefined') { - this.client = new Magic(this.config.magicPublishableApiKey, { - extensions: [new OpenIdExtension()], - network: MAINNET, // We always connect to mainnet to ensure addresses are the same across envs + this.lazyMagicClient = lazyDocumentReady(() => { + const client = new Magic(this.config.magicPublishableApiKey, { + extensions: [new OpenIdExtension()], + network: MAINNET, // We always connect to mainnet to ensure addresses are the same across envs + }); + client.preload(); + return client; }); } } - private get magicClient(): MagicClient { - if (!this.client) { + private get magicClient(): Promise { + if (!this.lazyMagicClient) { throw new Error('Cannot perform this action outside of the browser'); } - return this.client; + return this.lazyMagicClient; } async login( @@ -39,7 +44,8 @@ export default class MagicAdapter { return withPassportError(async () => { const startTime = performance.now(); - await this.magicClient.openid.loginWithOIDC({ + const magicClient = await this.magicClient; + await magicClient.openid.loginWithOIDC({ jwt: idToken, providerId: this.config.magicProviderId, }); @@ -50,13 +56,14 @@ export default class MagicAdapter { Math.round(performance.now() - startTime), ); - return this.magicClient.rpcProvider as unknown as ethers.providers.ExternalProvider; + return magicClient.rpcProvider as unknown as ethers.providers.ExternalProvider; }, PassportErrorType.WALLET_CONNECTION_ERROR); } async logout() { - if (this.magicClient.user) { - await this.magicClient.user.logout(); + const magicClient = await this.magicClient; + if (magicClient.user) { + await magicClient.user.logout(); } } } diff --git a/packages/passport/sdk/src/utils/lazyLoad.test.ts b/packages/passport/sdk/src/utils/lazyLoad.test.ts new file mode 100644 index 0000000000..de6a5d577d --- /dev/null +++ b/packages/passport/sdk/src/utils/lazyLoad.test.ts @@ -0,0 +1,56 @@ +import { lazyDocumentReady, lazyLoad } from './lazyLoad'; + +describe('lazyLoad', () => { + it('should return call initFunction and returns the value', async () => { + const initFunction = jest.fn().mockReturnValue('test'); + const promiseToAwait = jest.fn().mockResolvedValue(undefined); + const result = await lazyLoad(promiseToAwait, initFunction); + expect(result).toEqual('test'); + expect(initFunction).toHaveBeenCalled(); + }); +}); + +describe('lazyDocumentReady', () => { + let mockInitialiseFunction: jest.Mock; + let originalDocument: Document | undefined; + + beforeEach(() => { + mockInitialiseFunction = jest.fn(); + originalDocument = window.document; + const mockDocument = { + ...window.document, + readyState: 'complete', + }; + (window as any).document = mockDocument; + }); + + afterEach(() => { + // Restore the original document.readyState value after each test + (window as any).document = originalDocument; + }); + + it('should call the initialiseFunction when the document is already ready', async () => { + jest.spyOn(window.document, 'readyState', 'get').mockReturnValue('complete'); + + await lazyDocumentReady(mockInitialiseFunction); + + expect(mockInitialiseFunction).toHaveBeenCalledTimes(1); + }); + + it('should call the initialiseFunction when the document becomes ready', async () => { + jest.spyOn(window.document, 'readyState', 'get').mockReturnValue('loading'); + const mockAddEventListener = jest.spyOn(window.document, 'addEventListener'); + + const lazyDocumentPromise = lazyDocumentReady(mockInitialiseFunction); + expect(mockInitialiseFunction).toHaveBeenCalledTimes(0); + + jest.spyOn(window.document, 'readyState', 'get').mockReturnValue('complete'); + const mockEvent = new Event('readystatechange'); + window.document.dispatchEvent(mockEvent); + + await lazyDocumentPromise; + + expect(mockAddEventListener).toHaveBeenCalledWith('readystatechange', expect.any(Function)); + expect(mockInitialiseFunction).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/passport/sdk/src/utils/lazyLoad.ts b/packages/passport/sdk/src/utils/lazyLoad.ts new file mode 100644 index 0000000000..2c22dfeec6 --- /dev/null +++ b/packages/passport/sdk/src/utils/lazyLoad.ts @@ -0,0 +1,22 @@ +export const lazyLoad = ( + promiseToAwait: () => Promise, + initialiseFunction: (arg: Y) => Promise | T, +): Promise => promiseToAwait().then(initialiseFunction); + +export const lazyDocumentReady = (initialiseFunction: () => Promise | T): Promise => { + const documentReadyPromise = () => new Promise((resolve) => { + if (window.document.readyState === 'complete') { + resolve(); + } else { + const onReadyStateChange = () => { + if (window.document.readyState === 'complete') { + resolve(); + window.document.removeEventListener('readystatechange', onReadyStateChange); + } + }; + window.document.addEventListener('readystatechange', onReadyStateChange); + } + }); + + return lazyLoad(documentReadyPromise, initialiseFunction); +};