Skip to content

Commit

Permalink
fix: ID-1174 Passport - delay preload until window ready
Browse files Browse the repository at this point in the history
  • Loading branch information
haydenfowler authored Nov 5, 2023
1 parent b7ae1d2 commit 00b319f
Show file tree
Hide file tree
Showing 18 changed files with 162 additions and 49 deletions.
2 changes: 1 addition & 1 deletion packages/passport/sdk/src/Passport.int.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion packages/passport/sdk/src/Passport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions packages/passport/sdk/src/authManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
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 @@ -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,
Expand Down
69 changes: 48 additions & 21 deletions packages/passport/sdk/src/magicAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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,
Expand All @@ -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',
Expand All @@ -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();
});
Expand Down
43 changes: 26 additions & 17 deletions packages/passport/sdk/src/magicAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<SDKBase, [OpenIdExtension]>;

export default class MagicAdapter {
private readonly config: PassportConfiguration;

private magicClient?: InstanceWithExtensions<SDKBase, [OpenIdExtension]>;
private readonly lazyMagicClient?: Promise<MagicClient>;

constructor(config: PassportConfiguration) {
this.config = config;
if (typeof window !== 'undefined') {
this.magicClient = this.initMagicClient();
this.magicClient.preload();
this.lazyMagicClient = lazyDocumentReady<MagicClient>(() => {
const client = new Magic(this.config.magicPublishableApiKey, {
extensions: [new OpenIdExtension()],
network: this.config.network,
});
client.preload();
return client;
});
}
}

private get magicClient(): Promise<MagicClient> {
if (!this.lazyMagicClient) {
throw new Error('Cannot perform this action outside of the browser');
}

return this.lazyMagicClient;
}

async login(
idToken: string,
): Promise<ethers.providers.ExternalProvider> {
return withPassportError<ethers.providers.ExternalProvider>(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,
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
2 changes: 1 addition & 1 deletion packages/passport/sdk/src/starkEx/passportImxProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
56 changes: 56 additions & 0 deletions packages/passport/sdk/src/utils/lazyLoad.test.ts
Original file line number Diff line number Diff line change
@@ -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<any, any>;
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);
});
});
21 changes: 21 additions & 0 deletions packages/passport/sdk/src/utils/lazyLoad.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export const lazyLoad = <T, Y = void>(promiseToAwait: () => Promise<Y>, initialiseFunction: () => T): Promise<T> => (
promiseToAwait().then(initialiseFunction)
);

export const lazyDocumentReady = <T>(initialiseFunction: () => T): Promise<T> => {
const documentReadyPromise = () => new Promise<void>((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);
};
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion packages/passport/sdk/src/zkEvm/zkEvmProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
2 changes: 1 addition & 1 deletion packages/passport/sdk/src/zkEvm/zkEvmProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down

0 comments on commit 00b319f

Please sign in to comment.