From 00b319fb41602f824c28e593a314761440b8bb5b Mon Sep 17 00:00:00 2001 From: Hayden Fowler Date: Mon, 6 Nov 2023 10:19:58 +1100 Subject: [PATCH] fix: ID-1174 Passport - delay preload until window ready --- .../passport/sdk/src/Passport.int.test.ts | 2 +- packages/passport/sdk/src/Passport.ts | 2 +- packages/passport/sdk/src/authManager.test.ts | 4 +- packages/passport/sdk/src/authManager.ts | 2 +- .../passport/sdk/src/magicAdapter.test.ts | 69 +++++++++++++------ packages/passport/sdk/src/magicAdapter.ts | 43 +++++++----- .../src/starkEx/passportImxProvider.test.ts | 2 +- .../sdk/src/starkEx/passportImxProvider.ts | 2 +- .../passportImxProviderFactory.test.ts | 2 +- .../src/starkEx/passportImxProviderFactory.ts | 2 +- .../passport/sdk/src/utils/lazyLoad.test.ts | 56 +++++++++++++++ packages/passport/sdk/src/utils/lazyLoad.ts | 21 ++++++ .../sdk/src/{ => utils}/token.test.ts | 0 .../passport/sdk/src/{ => utils}/token.ts | 0 .../src/{ => utils}/typedEventEmitter.test.ts | 0 .../sdk/src/{ => utils}/typedEventEmitter.ts | 0 .../sdk/src/zkEvm/zkEvmProvider.test.ts | 2 +- .../passport/sdk/src/zkEvm/zkEvmProvider.ts | 2 +- 18 files changed, 162 insertions(+), 49 deletions(-) create mode 100644 packages/passport/sdk/src/utils/lazyLoad.test.ts create mode 100644 packages/passport/sdk/src/utils/lazyLoad.ts rename packages/passport/sdk/src/{ => utils}/token.test.ts (100%) rename packages/passport/sdk/src/{ => utils}/token.ts (100%) rename packages/passport/sdk/src/{ => utils}/typedEventEmitter.test.ts (100%) rename packages/passport/sdk/src/{ => utils}/typedEventEmitter.ts (100%) diff --git a/packages/passport/sdk/src/Passport.int.test.ts b/packages/passport/sdk/src/Passport.int.test.ts index a48d423b7f..966ac905e8 100644 --- a/packages/passport/sdk/src/Passport.int.test.ts +++ b/packages/passport/sdk/src/Passport.int.test.ts @@ -2,7 +2,7 @@ import { Magic } from 'magic-sdk'; import { UserManager } from 'oidc-client-ts'; import { TransactionRequest } from '@ethersproject/providers'; import { Environment, ImmutableConfiguration } from '@imtbl/config'; -import { mockValidIdToken } from './token.test'; +import { mockValidIdToken } from './utils/token.test'; import { Passport } from './Passport'; import { RequestArguments } from './zkEvm/types'; import { diff --git a/packages/passport/sdk/src/Passport.ts b/packages/passport/sdk/src/Passport.ts index a4fdcadf4c..b172cf3c84 100644 --- a/packages/passport/sdk/src/Passport.ts +++ b/packages/passport/sdk/src/Passport.ts @@ -17,7 +17,7 @@ import { import { ConfirmationScreen } from './confirmation'; import { ZkEvmProvider } from './zkEvm'; import { Provider } from './zkEvm/types'; -import TypedEventEmitter from './typedEventEmitter'; +import TypedEventEmitter from './utils/typedEventEmitter'; export class Passport { private readonly authManager: AuthManager; diff --git a/packages/passport/sdk/src/authManager.test.ts b/packages/passport/sdk/src/authManager.test.ts index e86fd9c06a..ac84487760 100644 --- a/packages/passport/sdk/src/authManager.test.ts +++ b/packages/passport/sdk/src/authManager.test.ts @@ -4,10 +4,10 @@ import AuthManager from './authManager'; import { PassportError, PassportErrorType } from './errors/passportError'; import { PassportConfiguration } from './config'; import { mockUser, mockUserImx, mockUserZkEvm } from './test/mocks'; -import { isTokenExpired } from './token'; +import { isTokenExpired } from './utils/token'; jest.mock('oidc-client-ts'); -jest.mock('./token'); +jest.mock('./utils/token'); const baseConfig = new ImmutableConfiguration({ environment: Environment.SANDBOX, diff --git a/packages/passport/sdk/src/authManager.ts b/packages/passport/sdk/src/authManager.ts index 73f4d28825..a37aec6975 100644 --- a/packages/passport/sdk/src/authManager.ts +++ b/packages/passport/sdk/src/authManager.ts @@ -9,7 +9,7 @@ import axios from 'axios'; import DeviceCredentialsManager from 'storage/device_credentials_manager'; import * as crypto from 'crypto'; import jwt_decode from 'jwt-decode'; -import { isTokenExpired } from './token'; +import { isTokenExpired } from './utils/token'; import { PassportErrorType, withPassportError } from './errors/passportError'; import { PassportMetadata, diff --git a/packages/passport/sdk/src/magicAdapter.test.ts b/packages/passport/sdk/src/magicAdapter.test.ts index 498863b6f0..d6af133387 100644 --- a/packages/passport/sdk/src/magicAdapter.test.ts +++ b/packages/passport/sdk/src/magicAdapter.test.ts @@ -17,7 +17,6 @@ jest.mock('@magic-ext/oidc', () => ({ })); describe('MagicWallet', () => { - let magicWallet: MagicAdapter; const apiKey = 'pk_live_A7D9211D7547A338'; const providerId = 'mPGZAvZsFkyfT6OWfML1HgTKjPqYOPkhhOj-8qCGeqI='; const config: PassportConfiguration = { @@ -40,32 +39,55 @@ describe('MagicWallet', () => { rpcProvider, preload, })); - magicWallet = new MagicAdapter(config); }); - describe('preload', () => { - it('should have called the magic client preload method', () => { - expect(preload).toHaveBeenCalled(); + describe('constructor', () => { + describe('when window defined', () => { + let originalDocument: Document | undefined; + + beforeAll(() => { + originalDocument = window.document; + const mockDocument = { + ...window.document, + readyState: 'complete', + }; + (window as any).document = mockDocument; + }); + afterAll(() => { + (window as any).document = originalDocument; + }); + it('starts initialising the magicClient', () => { + jest.spyOn(window.document, 'readyState', 'get').mockReturnValue('complete'); + preload.mockResolvedValue(Promise.resolve()); + const magicAdapter = new MagicAdapter(config); + // @ts-ignore + expect(magicAdapter.lazyMagicClient).toBeDefined(); + }); }); - }); - describe('window is not defined', () => { - const { window } = global; - beforeAll(() => { - // @ts-expect-error - delete global.window; - }); - afterAll(() => { - global.window = window; - }); - it('does not call the magic preload method', () => { - expect(preload).toBeCalledTimes(0); + describe('when window is undefined', () => { + const { window } = global; + beforeAll(() => { + // @ts-expect-error + delete global.window; + }); + afterAll(() => { + global.window = window; + }); + + it('does nothing', () => { + const magicAdapter = new MagicAdapter(config); + // @ts-ignore + expect(magicAdapter.magicClientPromise).toBeUndefined(); + }); }); }); describe('login', () => { it('should call loginWithOIDC and initialise the provider with the correct arguments', async () => { - const magicProvider = await magicWallet.login(idToken); + preload.mockResolvedValue(Promise.resolve()); + const magicAdapter = new MagicAdapter(config); + const magicProvider = await magicAdapter.login(idToken); expect(Magic).toHaveBeenCalledWith(apiKey, { network: config.network, @@ -81,12 +103,15 @@ describe('MagicWallet', () => { }); it('should throw a PassportError when an error is thrown', async () => { + preload.mockResolvedValue(Promise.resolve()); + const magicAdapter = new MagicAdapter(config); + loginWithOIDCMock.mockImplementation(() => { throw new Error('oops'); }); await expect(async () => { - await magicWallet.login(idToken); + await magicAdapter.login(idToken); }).rejects.toThrow( new PassportError( 'oops', @@ -98,8 +123,10 @@ describe('MagicWallet', () => { describe('logout', () => { it('calls the logout function', async () => { - await magicWallet.login(idToken); - await magicWallet.logout(); + preload.mockResolvedValue(Promise.resolve()); + const magicAdapter = new MagicAdapter(config); + await magicAdapter.login(idToken); + await magicAdapter.logout(); expect(logoutMock).toHaveBeenCalled(); }); diff --git a/packages/passport/sdk/src/magicAdapter.ts b/packages/passport/sdk/src/magicAdapter.ts index bd7869f6dd..21c79390a1 100644 --- a/packages/passport/sdk/src/magicAdapter.ts +++ b/packages/passport/sdk/src/magicAdapter.ts @@ -4,46 +4,55 @@ import { OpenIdExtension } from '@magic-ext/oidc'; import { ethers } from 'ethers'; import { PassportErrorType, withPassportError } from './errors/passportError'; import { PassportConfiguration } from './config'; +import { lazyDocumentReady } from './utils/lazyLoad'; + +type MagicClient = InstanceWithExtensions; export default class MagicAdapter { private readonly config: PassportConfiguration; - private magicClient?: InstanceWithExtensions; + private readonly lazyMagicClient?: Promise; constructor(config: PassportConfiguration) { this.config = config; if (typeof window !== 'undefined') { - this.magicClient = this.initMagicClient(); - this.magicClient.preload(); + this.lazyMagicClient = lazyDocumentReady(() => { + const client = new Magic(this.config.magicPublishableApiKey, { + extensions: [new OpenIdExtension()], + network: this.config.network, + }); + client.preload(); + return client; + }); + } + } + + private get magicClient(): Promise { + if (!this.lazyMagicClient) { + throw new Error('Cannot perform this action outside of the browser'); } + + return this.lazyMagicClient; } async login( idToken: string, ): Promise { return withPassportError(async () => { - if (!this.magicClient) { - this.magicClient = this.initMagicClient(); - } - await this.magicClient.openid.loginWithOIDC({ + const magicClient = await this.magicClient; + await magicClient.openid.loginWithOIDC({ jwt: idToken, providerId: this.config.magicProviderId, }); - 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(); } } - - initMagicClient() { - return new Magic(this.config.magicPublishableApiKey, { - extensions: [new OpenIdExtension()], - network: this.config.network, - }); - } } diff --git a/packages/passport/sdk/src/starkEx/passportImxProvider.test.ts b/packages/passport/sdk/src/starkEx/passportImxProvider.test.ts index 6a42bc0509..f0595227ad 100644 --- a/packages/passport/sdk/src/starkEx/passportImxProvider.test.ts +++ b/packages/passport/sdk/src/starkEx/passportImxProvider.test.ts @@ -23,7 +23,7 @@ import { import { ConfirmationScreen } from '../confirmation'; import { PassportConfiguration } from '../config'; import { PassportEventMap, PassportEvents } from '../types'; -import TypedEventEmitter from '../typedEventEmitter'; +import TypedEventEmitter from '../utils/typedEventEmitter'; import AuthManager from '../authManager'; jest.mock('./workflows'); diff --git a/packages/passport/sdk/src/starkEx/passportImxProvider.ts b/packages/passport/sdk/src/starkEx/passportImxProvider.ts index ed6a3cb619..0a56dbf537 100644 --- a/packages/passport/sdk/src/starkEx/passportImxProvider.ts +++ b/packages/passport/sdk/src/starkEx/passportImxProvider.ts @@ -29,7 +29,7 @@ import { } from './workflows'; import { ConfirmationScreen } from '../confirmation'; import { PassportConfiguration } from '../config'; -import TypedEventEmitter from '../typedEventEmitter'; +import TypedEventEmitter from '../utils/typedEventEmitter'; import AuthManager from '../authManager'; export interface PassportImxProviderInput { diff --git a/packages/passport/sdk/src/starkEx/passportImxProviderFactory.test.ts b/packages/passport/sdk/src/starkEx/passportImxProviderFactory.test.ts index 3822c8e39b..1a30a62e31 100644 --- a/packages/passport/sdk/src/starkEx/passportImxProviderFactory.test.ts +++ b/packages/passport/sdk/src/starkEx/passportImxProviderFactory.test.ts @@ -10,7 +10,7 @@ import { PassportEventMap } from '../types'; import { PassportImxProvider } from './passportImxProvider'; import { getStarkSigner } from './getStarkSigner'; import { mockUser, mockUserImx, testConfig } from '../test/mocks'; -import TypedEventEmitter from '../typedEventEmitter'; +import TypedEventEmitter from '../utils/typedEventEmitter'; jest.mock('@ethersproject/providers'); jest.mock('./workflows/registration'); diff --git a/packages/passport/sdk/src/starkEx/passportImxProviderFactory.ts b/packages/passport/sdk/src/starkEx/passportImxProviderFactory.ts index 8742b5f927..442f76a6f1 100644 --- a/packages/passport/sdk/src/starkEx/passportImxProviderFactory.ts +++ b/packages/passport/sdk/src/starkEx/passportImxProviderFactory.ts @@ -13,7 +13,7 @@ import { } from '../types'; import { PassportImxProvider } from './passportImxProvider'; import { getStarkSigner } from './getStarkSigner'; -import TypedEventEmitter from '../typedEventEmitter'; +import TypedEventEmitter from '../utils/typedEventEmitter'; export type PassportImxProviderFactoryInput = { authManager: AuthManager; 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..a4476b6f75 --- /dev/null +++ b/packages/passport/sdk/src/utils/lazyLoad.ts @@ -0,0 +1,21 @@ +export const lazyLoad = (promiseToAwait: () => Promise, initialiseFunction: () => T): Promise => ( + promiseToAwait().then(initialiseFunction) +); + +export const lazyDocumentReady = (initialiseFunction: () => 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); +}; diff --git a/packages/passport/sdk/src/token.test.ts b/packages/passport/sdk/src/utils/token.test.ts similarity index 100% rename from packages/passport/sdk/src/token.test.ts rename to packages/passport/sdk/src/utils/token.test.ts diff --git a/packages/passport/sdk/src/token.ts b/packages/passport/sdk/src/utils/token.ts similarity index 100% rename from packages/passport/sdk/src/token.ts rename to packages/passport/sdk/src/utils/token.ts diff --git a/packages/passport/sdk/src/typedEventEmitter.test.ts b/packages/passport/sdk/src/utils/typedEventEmitter.test.ts similarity index 100% rename from packages/passport/sdk/src/typedEventEmitter.test.ts rename to packages/passport/sdk/src/utils/typedEventEmitter.test.ts diff --git a/packages/passport/sdk/src/typedEventEmitter.ts b/packages/passport/sdk/src/utils/typedEventEmitter.ts similarity index 100% rename from packages/passport/sdk/src/typedEventEmitter.ts rename to packages/passport/sdk/src/utils/typedEventEmitter.ts diff --git a/packages/passport/sdk/src/zkEvm/zkEvmProvider.test.ts b/packages/passport/sdk/src/zkEvm/zkEvmProvider.test.ts index 1f38fdce2b..8768547d3d 100644 --- a/packages/passport/sdk/src/zkEvm/zkEvmProvider.test.ts +++ b/packages/passport/sdk/src/zkEvm/zkEvmProvider.test.ts @@ -7,7 +7,7 @@ import GuardianClient from '../guardian/guardian'; import { RelayerClient } from './relayerClient'; import { Provider } from './types'; import { PassportEventMap, PassportEvents } from '../types'; -import TypedEventEmitter from '../typedEventEmitter'; +import TypedEventEmitter from '../utils/typedEventEmitter'; import { mockUserZkEvm } from '../test/mocks'; import { signTypedDataV4 } from './signTypedDataV4'; diff --git a/packages/passport/sdk/src/zkEvm/zkEvmProvider.ts b/packages/passport/sdk/src/zkEvm/zkEvmProvider.ts index 8b89205e7a..68f4f83125 100644 --- a/packages/passport/sdk/src/zkEvm/zkEvmProvider.ts +++ b/packages/passport/sdk/src/zkEvm/zkEvmProvider.ts @@ -11,7 +11,7 @@ import { } from './types'; import AuthManager from '../authManager'; import MagicAdapter from '../magicAdapter'; -import TypedEventEmitter from '../typedEventEmitter'; +import TypedEventEmitter from '../utils/typedEventEmitter'; import { PassportConfiguration } from '../config'; import { ConfirmationScreen } from '../confirmation'; import { PassportEventMap, PassportEvents, UserZkEvm } from '../types';