mirror of
https://github.com/trezor/trezor-suite.git
synced 2026-02-26 19:52:18 +01:00
chore(connect-web): refactor into classes
This commit is contained in:
committed by
Tomáš Martykán
parent
322651b7b3
commit
fc7a45b19d
@@ -17,320 +17,332 @@ import {
|
||||
createUiMessage,
|
||||
createErrorMessage,
|
||||
UiResponseEvent,
|
||||
CallMethod,
|
||||
CoreEventMessage,
|
||||
CallMethodPayload,
|
||||
} from '@trezor/connect/src/events';
|
||||
import type { ConnectSettings, Manifest } from '@trezor/connect/src/types';
|
||||
import { factory } from '@trezor/connect/src/factory';
|
||||
import { initLog } from '@trezor/connect/src/utils/debug';
|
||||
import { ConnectFactoryDependencies, factory } from '@trezor/connect/src/factory';
|
||||
import { Log, initLog } from '@trezor/connect/src/utils/debug';
|
||||
import { config } from '@trezor/connect/src/data/config';
|
||||
import { createDeferredManager } from '@trezor/utils/src/createDeferredManager';
|
||||
import { DeferredManager, createDeferredManager } from '@trezor/utils/src/createDeferredManager';
|
||||
|
||||
import * as iframe from '../iframe';
|
||||
import * as popup from '../popup';
|
||||
import webUSBButton from '../webusb/button';
|
||||
import { parseConnectSettings } from '../connectSettings';
|
||||
|
||||
const eventEmitter = new EventEmitter();
|
||||
const _log = initLog('@trezor/connect-web');
|
||||
export class CoreInIframe implements ConnectFactoryDependencies {
|
||||
public eventEmitter = new EventEmitter();
|
||||
protected _settings: ConnectSettings;
|
||||
|
||||
let _settings = parseConnectSettings();
|
||||
let _popupManager: popup.PopupManager | undefined;
|
||||
private _log: Log;
|
||||
private _popupManager?: popup.PopupManager;
|
||||
private _messagePromises: DeferredManager<{ id: number; success: boolean; payload: any }>;
|
||||
|
||||
const messagePromises = createDeferredManager({ initialId: 1 });
|
||||
private readonly boundHandleMessage = this.handleMessage.bind(this);
|
||||
private readonly boundDispose = this.dispose.bind(this);
|
||||
|
||||
const initPopupManager = () => {
|
||||
const pm = new popup.PopupManager(_settings, { logger: _log });
|
||||
pm.on(POPUP.CLOSED, (error?: string) => {
|
||||
iframe.postMessage({
|
||||
type: POPUP.CLOSED,
|
||||
payload: error ? { error } : null,
|
||||
public constructor() {
|
||||
this._settings = parseConnectSettings();
|
||||
this._log = initLog('@trezor/connect-web');
|
||||
this._messagePromises = createDeferredManager({ initialId: 1 });
|
||||
}
|
||||
|
||||
private initPopupManager() {
|
||||
const pm = new popup.PopupManager(this._settings, { logger: this._log });
|
||||
pm.on(POPUP.CLOSED, (error?: string) => {
|
||||
iframe.postMessage({
|
||||
type: POPUP.CLOSED,
|
||||
payload: error ? { error } : null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return pm;
|
||||
};
|
||||
|
||||
const manifest = (data: Manifest) => {
|
||||
_settings = parseConnectSettings({
|
||||
..._settings,
|
||||
manifest: data,
|
||||
});
|
||||
};
|
||||
|
||||
const dispose = () => {
|
||||
eventEmitter.removeAllListeners();
|
||||
iframe.dispose();
|
||||
_settings = parseConnectSettings();
|
||||
if (_popupManager) {
|
||||
_popupManager.close();
|
||||
return pm;
|
||||
}
|
||||
|
||||
return Promise.resolve(undefined);
|
||||
};
|
||||
|
||||
const cancel = (error?: string) => {
|
||||
if (_popupManager) {
|
||||
_popupManager.emit(POPUP.CLOSED, error);
|
||||
public manifest(data: Manifest) {
|
||||
this._settings = parseConnectSettings({
|
||||
...this._settings,
|
||||
manifest: data,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// handle message received from iframe
|
||||
const handleMessage = (messageEvent: MessageEvent<CoreEventMessage>) => {
|
||||
// ignore messages from domain other then iframe origin
|
||||
if (messageEvent.origin !== iframe.origin) return;
|
||||
|
||||
const message = parseMessage<CoreEventMessage>(messageEvent.data);
|
||||
|
||||
_log.log('handleMessage', message);
|
||||
|
||||
switch (message.event) {
|
||||
case RESPONSE_EVENT: {
|
||||
const { id = 0, success, payload } = message;
|
||||
const resolved = messagePromises.resolve(id, { id, success, payload });
|
||||
if (!resolved) _log.warn(`Unknown message id ${id}`);
|
||||
break;
|
||||
public dispose() {
|
||||
this.eventEmitter.removeAllListeners();
|
||||
iframe.dispose();
|
||||
this._settings = parseConnectSettings();
|
||||
if (this._popupManager) {
|
||||
this._popupManager.close();
|
||||
}
|
||||
case DEVICE_EVENT:
|
||||
// pass DEVICE event up to html
|
||||
eventEmitter.emit(message.event, message);
|
||||
eventEmitter.emit(message.type, message.payload); // DEVICE_EVENT also emit single events (connect/disconnect...)
|
||||
break;
|
||||
window.removeEventListener('message', this.boundHandleMessage);
|
||||
window.removeEventListener('unload', this.boundDispose);
|
||||
|
||||
case TRANSPORT_EVENT:
|
||||
eventEmitter.emit(message.event, message);
|
||||
eventEmitter.emit(message.type, message.payload);
|
||||
break;
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
case BLOCKCHAIN_EVENT:
|
||||
eventEmitter.emit(message.event, message);
|
||||
eventEmitter.emit(message.type, message.payload);
|
||||
break;
|
||||
public cancel(error?: string) {
|
||||
if (this._popupManager) {
|
||||
this._popupManager.emit(POPUP.CLOSED, error);
|
||||
}
|
||||
}
|
||||
|
||||
case UI_EVENT:
|
||||
if (message.type === IFRAME.BOOTSTRAP) {
|
||||
iframe.clearTimeout();
|
||||
// handle message received from iframe
|
||||
private handleMessage(messageEvent: MessageEvent<CoreEventMessage>) {
|
||||
// ignore messages from domain other then iframe origin
|
||||
if (messageEvent.origin !== iframe.origin) return;
|
||||
|
||||
const message = parseMessage<CoreEventMessage>(messageEvent.data);
|
||||
|
||||
this._log.log('handleMessage', message);
|
||||
|
||||
switch (message.event) {
|
||||
case RESPONSE_EVENT: {
|
||||
const { id = 0, success, payload } = message;
|
||||
const resolved = this._messagePromises.resolve(id, { id, success, payload });
|
||||
if (!resolved) this._log.warn(`Unknown message id ${id}`);
|
||||
break;
|
||||
}
|
||||
if (message.type === IFRAME.LOADED) {
|
||||
iframe.initPromise.resolve();
|
||||
}
|
||||
if (message.type === IFRAME.ERROR) {
|
||||
iframe.initPromise.reject(message.payload.error as any);
|
||||
}
|
||||
case DEVICE_EVENT:
|
||||
// pass DEVICE event up to html
|
||||
this.eventEmitter.emit(message.event, message);
|
||||
this.eventEmitter.emit(message.type, message.payload); // DEVICE_EVENT also emit single events (connect/disconnect...)
|
||||
break;
|
||||
|
||||
// pass UI event up
|
||||
eventEmitter.emit(message.event, message);
|
||||
eventEmitter.emit(message.type, message.payload);
|
||||
break;
|
||||
case TRANSPORT_EVENT:
|
||||
this.eventEmitter.emit(message.event, message);
|
||||
this.eventEmitter.emit(message.type, message.payload);
|
||||
break;
|
||||
|
||||
default:
|
||||
_log.log('Undefined message', messageEvent.data);
|
||||
}
|
||||
};
|
||||
case BLOCKCHAIN_EVENT:
|
||||
this.eventEmitter.emit(message.event, message);
|
||||
this.eventEmitter.emit(message.type, message.payload);
|
||||
break;
|
||||
|
||||
const init = async (settings: Partial<ConnectSettings> = {}): Promise<void> => {
|
||||
if (iframe.instance) {
|
||||
throw ERRORS.TypedError('Init_AlreadyInitialized');
|
||||
}
|
||||
|
||||
_settings = parseConnectSettings({ ..._settings, ...settings });
|
||||
|
||||
if (!_settings.manifest) {
|
||||
throw ERRORS.TypedError('Init_ManifestMissing');
|
||||
}
|
||||
|
||||
// defaults for connect-web
|
||||
if (!_settings.transports?.length) {
|
||||
_settings.transports = ['BridgeTransport', 'WebUsbTransport'];
|
||||
}
|
||||
|
||||
if (_settings.lazyLoad) {
|
||||
// reset "lazyLoad" after first use
|
||||
_settings.lazyLoad = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_popupManager) {
|
||||
_popupManager = initPopupManager();
|
||||
}
|
||||
|
||||
_log.enabled = !!_settings.debug;
|
||||
|
||||
window.addEventListener('message', handleMessage);
|
||||
window.addEventListener('unload', dispose);
|
||||
|
||||
await iframe.init(_settings);
|
||||
|
||||
// sharedLogger can be disable but it is enable by default.
|
||||
if (_settings.sharedLogger !== false) {
|
||||
// connect-web is running in third-party domain so we use iframe to pass logs to shared worker.
|
||||
iframe.initIframeLogger();
|
||||
}
|
||||
};
|
||||
|
||||
const call: CallMethod = async params => {
|
||||
if (!iframe.instance && !iframe.timeout) {
|
||||
// init popup with lazy loading before iframe initialization
|
||||
_settings = parseConnectSettings(_settings);
|
||||
|
||||
if (!_settings.manifest) {
|
||||
return createErrorMessage(ERRORS.TypedError('Init_ManifestMissing'));
|
||||
}
|
||||
|
||||
if (!_popupManager) {
|
||||
_popupManager = initPopupManager();
|
||||
}
|
||||
_popupManager.request();
|
||||
|
||||
// auto init with default settings
|
||||
try {
|
||||
await init(_settings);
|
||||
} catch (error) {
|
||||
if (_popupManager) {
|
||||
// Catch fatal iframe errors (not loading)
|
||||
if (['Init_IframeBlocked', 'Init_IframeTimeout'].includes(error.code)) {
|
||||
_popupManager.postMessage(createUiMessage(UI.IFRAME_FAILURE));
|
||||
} else {
|
||||
_popupManager.clear();
|
||||
case UI_EVENT:
|
||||
if (message.type === IFRAME.BOOTSTRAP) {
|
||||
iframe.clearTimeout();
|
||||
break;
|
||||
}
|
||||
if (message.type === IFRAME.LOADED) {
|
||||
iframe.initPromise.resolve();
|
||||
}
|
||||
if (message.type === IFRAME.ERROR) {
|
||||
iframe.initPromise.reject(message.payload.error as any);
|
||||
}
|
||||
|
||||
// pass UI event up
|
||||
this.eventEmitter.emit(message.event, message);
|
||||
this.eventEmitter.emit(message.type, message.payload);
|
||||
break;
|
||||
|
||||
default:
|
||||
this._log.log('Undefined message', messageEvent.data);
|
||||
}
|
||||
}
|
||||
|
||||
public async init(settings: Partial<ConnectSettings> = {}) {
|
||||
if (iframe.instance) {
|
||||
throw ERRORS.TypedError('Init_AlreadyInitialized');
|
||||
}
|
||||
|
||||
this._settings = parseConnectSettings({ ...this._settings, ...settings });
|
||||
|
||||
if (!this._settings.manifest) {
|
||||
throw ERRORS.TypedError('Init_ManifestMissing');
|
||||
}
|
||||
|
||||
// defaults for connect-web
|
||||
if (!this._settings.transports?.length) {
|
||||
this._settings.transports = ['BridgeTransport', 'WebUsbTransport'];
|
||||
}
|
||||
|
||||
if (this._settings.lazyLoad) {
|
||||
// reset "lazyLoad" after first use
|
||||
this._settings.lazyLoad = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._popupManager) {
|
||||
this._popupManager = this.initPopupManager();
|
||||
}
|
||||
|
||||
this._log.enabled = !!this._settings.debug;
|
||||
|
||||
window.addEventListener('message', this.boundHandleMessage);
|
||||
window.addEventListener('unload', this.boundDispose);
|
||||
|
||||
await iframe.init(this._settings);
|
||||
|
||||
// sharedLogger can be disable but it is enable by default.
|
||||
if (this._settings.sharedLogger !== false) {
|
||||
// connect-web is running in third-party domain so we use iframe to pass logs to shared worker.
|
||||
iframe.initIframeLogger();
|
||||
}
|
||||
}
|
||||
|
||||
public async call(params: CallMethodPayload) {
|
||||
if (!iframe.instance && !iframe.timeout) {
|
||||
// init popup with lazy loading before iframe initialization
|
||||
this._settings = parseConnectSettings(this._settings);
|
||||
|
||||
if (!this._settings.manifest) {
|
||||
return createErrorMessage(ERRORS.TypedError('Init_ManifestMissing'));
|
||||
}
|
||||
|
||||
if (!this._popupManager) {
|
||||
this._popupManager = this.initPopupManager();
|
||||
}
|
||||
this._popupManager.request();
|
||||
|
||||
// auto init with default settings
|
||||
try {
|
||||
await this.init(this._settings);
|
||||
} catch (error) {
|
||||
if (this._popupManager) {
|
||||
// Catch fatal iframe errors (not loading)
|
||||
if (['Init_IframeBlocked', 'Init_IframeTimeout'].includes(error.code)) {
|
||||
this._popupManager.postMessage(createUiMessage(UI.IFRAME_FAILURE));
|
||||
} else {
|
||||
this._popupManager.clear();
|
||||
}
|
||||
}
|
||||
|
||||
return createErrorMessage(error);
|
||||
}
|
||||
}
|
||||
|
||||
if (iframe.timeout) {
|
||||
// this.init was called, but iframe doesn't return handshake yet
|
||||
await iframe.initPromise.promise;
|
||||
}
|
||||
|
||||
if (iframe.error) {
|
||||
// iframe was initialized with error
|
||||
return createErrorMessage(iframe.error);
|
||||
}
|
||||
|
||||
// request popup window it might be used in the future
|
||||
if (this._settings.popup && this._popupManager) {
|
||||
this._popupManager.request();
|
||||
}
|
||||
|
||||
// post message to iframe
|
||||
try {
|
||||
const { promiseId, promise } = this._messagePromises.create();
|
||||
iframe.postMessage({ id: promiseId, type: IFRAME.CALL, payload: params });
|
||||
const response = await promise;
|
||||
if (response) {
|
||||
if (
|
||||
!response.success &&
|
||||
response.payload.code !== 'Device_CallInProgress' &&
|
||||
this._popupManager
|
||||
) {
|
||||
this._popupManager.unlock();
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
if (this._popupManager) {
|
||||
this._popupManager.unlock();
|
||||
}
|
||||
|
||||
return createErrorMessage(ERRORS.TypedError('Method_NoResponse'));
|
||||
} catch (error) {
|
||||
this._log.error('__call error', error);
|
||||
if (this._popupManager) {
|
||||
this._popupManager.clear(false);
|
||||
}
|
||||
|
||||
return createErrorMessage(error);
|
||||
}
|
||||
}
|
||||
|
||||
if (iframe.timeout) {
|
||||
// this.init was called, but iframe doesn't return handshake yet
|
||||
await iframe.initPromise.promise;
|
||||
public uiResponse(response: UiResponseEvent) {
|
||||
if (!iframe.instance) {
|
||||
throw ERRORS.TypedError('Init_NotInitialized');
|
||||
}
|
||||
iframe.postMessage(response);
|
||||
}
|
||||
|
||||
if (iframe.error) {
|
||||
// iframe was initialized with error
|
||||
return createErrorMessage(iframe.error);
|
||||
public renderWebUSBButton(className?: string) {
|
||||
webUSBButton(className, this._settings.webusbSrc);
|
||||
}
|
||||
|
||||
// request popup window it might be used in the future
|
||||
if (_settings.popup && _popupManager) {
|
||||
_popupManager.request();
|
||||
}
|
||||
public async requestLogin(params: any) {
|
||||
if (typeof params.callback === 'function') {
|
||||
const { callback } = params;
|
||||
|
||||
// post message to iframe
|
||||
try {
|
||||
const { promiseId, promise } = messagePromises.create();
|
||||
iframe.postMessage({ id: promiseId, type: IFRAME.CALL, payload: params });
|
||||
const response = await promise;
|
||||
if (response) {
|
||||
if (
|
||||
!response.success &&
|
||||
response.payload.code !== 'Device_CallInProgress' &&
|
||||
_popupManager
|
||||
) {
|
||||
_popupManager.unlock();
|
||||
}
|
||||
// TODO: set message listener only if iframe is loaded correctly
|
||||
const loginChallengeListener = async (event: MessageEvent<CoreEventMessage>) => {
|
||||
const { data } = event;
|
||||
if (data && data.type === UI.LOGIN_CHALLENGE_REQUEST) {
|
||||
try {
|
||||
const payload = await callback();
|
||||
iframe.postMessage({
|
||||
type: UI.LOGIN_CHALLENGE_RESPONSE,
|
||||
payload,
|
||||
});
|
||||
} catch (error) {
|
||||
iframe.postMessage({
|
||||
type: UI.LOGIN_CHALLENGE_RESPONSE,
|
||||
payload: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', loginChallengeListener, false);
|
||||
|
||||
const response = await this.call({
|
||||
method: 'requestLogin',
|
||||
...params,
|
||||
asyncChallenge: true,
|
||||
callback: null,
|
||||
});
|
||||
window.removeEventListener('message', loginChallengeListener);
|
||||
|
||||
return response;
|
||||
}
|
||||
if (_popupManager) {
|
||||
_popupManager.unlock();
|
||||
|
||||
return this.call({ method: 'requestLogin', ...params });
|
||||
}
|
||||
|
||||
public disableWebUSB() {
|
||||
if (!iframe.instance) {
|
||||
throw ERRORS.TypedError('Init_NotInitialized');
|
||||
}
|
||||
iframe.postMessage({ type: TRANSPORT.DISABLE_WEBUSB });
|
||||
}
|
||||
|
||||
return createErrorMessage(ERRORS.TypedError('Method_NoResponse'));
|
||||
} catch (error) {
|
||||
_log.error('__call error', error);
|
||||
if (_popupManager) {
|
||||
_popupManager.clear(false);
|
||||
/**
|
||||
* Initiate device pairing procedure.
|
||||
*/
|
||||
public async requestWebUSBDevice() {
|
||||
try {
|
||||
await window.navigator.usb.requestDevice({ filters: config.webusb });
|
||||
iframe.postMessage({ type: TRANSPORT.REQUEST_DEVICE });
|
||||
} catch (_err) {
|
||||
// user hits cancel gets "DOMException: No device selected."
|
||||
// no need to log this
|
||||
}
|
||||
|
||||
return createErrorMessage(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const uiResponse = (response: UiResponseEvent) => {
|
||||
if (!iframe.instance) {
|
||||
throw ERRORS.TypedError('Init_NotInitialized');
|
||||
}
|
||||
iframe.postMessage(response);
|
||||
};
|
||||
const methods = new CoreInIframe();
|
||||
|
||||
const renderWebUSBButton = (className?: string) => {
|
||||
webUSBButton(className, _settings.webusbSrc);
|
||||
};
|
||||
|
||||
const requestLogin = async (params: any) => {
|
||||
if (typeof params.callback === 'function') {
|
||||
const { callback } = params;
|
||||
|
||||
// TODO: set message listener only if iframe is loaded correctly
|
||||
const loginChallengeListener = async (event: MessageEvent<CoreEventMessage>) => {
|
||||
const { data } = event;
|
||||
if (data && data.type === UI.LOGIN_CHALLENGE_REQUEST) {
|
||||
try {
|
||||
const payload = await callback();
|
||||
iframe.postMessage({
|
||||
type: UI.LOGIN_CHALLENGE_RESPONSE,
|
||||
payload,
|
||||
});
|
||||
} catch (error) {
|
||||
iframe.postMessage({
|
||||
type: UI.LOGIN_CHALLENGE_RESPONSE,
|
||||
payload: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', loginChallengeListener, false);
|
||||
|
||||
const response = await call({
|
||||
method: 'requestLogin',
|
||||
...params,
|
||||
asyncChallenge: true,
|
||||
callback: null,
|
||||
});
|
||||
window.removeEventListener('message', loginChallengeListener);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
return call({ method: 'requestLogin', ...params });
|
||||
};
|
||||
|
||||
const disableWebUSB = () => {
|
||||
if (!iframe.instance) {
|
||||
throw ERRORS.TypedError('Init_NotInitialized');
|
||||
}
|
||||
iframe.postMessage({ type: TRANSPORT.DISABLE_WEBUSB });
|
||||
};
|
||||
|
||||
/**
|
||||
* Initiate device pairing procedure.
|
||||
*/
|
||||
const requestWebUSBDevice = async () => {
|
||||
try {
|
||||
await window.navigator.usb.requestDevice({ filters: config.webusb });
|
||||
iframe.postMessage({ type: TRANSPORT.REQUEST_DEVICE });
|
||||
} catch (_err) {
|
||||
// user hits cancel gets "DOMException: No device selected."
|
||||
// no need to log this
|
||||
}
|
||||
};
|
||||
|
||||
export const methods = {
|
||||
eventEmitter,
|
||||
manifest,
|
||||
init,
|
||||
call,
|
||||
requestLogin,
|
||||
uiResponse,
|
||||
renderWebUSBButton,
|
||||
disableWebUSB,
|
||||
requestWebUSBDevice,
|
||||
cancel,
|
||||
dispose,
|
||||
};
|
||||
|
||||
const TrezorConnect = factory(methods);
|
||||
|
||||
export default TrezorConnect;
|
||||
// Exported to enable using directly
|
||||
export const TrezorConnect = factory({
|
||||
// Bind all methods due to shadowing `this`
|
||||
eventEmitter: methods.eventEmitter,
|
||||
init: methods.init.bind(methods),
|
||||
call: methods.call.bind(methods),
|
||||
manifest: methods.manifest.bind(methods),
|
||||
requestLogin: methods.requestLogin.bind(methods),
|
||||
uiResponse: methods.uiResponse.bind(methods),
|
||||
renderWebUSBButton: methods.renderWebUSBButton.bind(methods),
|
||||
disableWebUSB: methods.disableWebUSB.bind(methods),
|
||||
requestWebUSBDevice: methods.requestWebUSBDevice.bind(methods),
|
||||
cancel: methods.cancel.bind(methods),
|
||||
dispose: methods.dispose.bind(methods),
|
||||
});
|
||||
|
||||
@@ -198,8 +198,10 @@ export class CoreInPopup implements ConnectFactoryDependencies {
|
||||
}
|
||||
|
||||
const methods = new CoreInPopup();
|
||||
// Bind all methods due to shadowing `this`
|
||||
const TrezorConnect = factory({
|
||||
|
||||
// Exported to enable using directly
|
||||
export const TrezorConnect = factory({
|
||||
// Bind all methods due to shadowing `this`
|
||||
eventEmitter: methods.eventEmitter,
|
||||
init: methods.init.bind(methods),
|
||||
call: methods.call.bind(methods),
|
||||
@@ -212,5 +214,3 @@ const TrezorConnect = factory({
|
||||
cancel: methods.cancel.bind(methods),
|
||||
dispose: methods.dispose.bind(methods),
|
||||
});
|
||||
|
||||
export default TrezorConnect;
|
||||
|
||||
@@ -1,50 +1,187 @@
|
||||
import TrezorConnectIframe from './impl/core-in-iframe';
|
||||
import TrezorConnectCoreInPopup from './impl/core-in-popup';
|
||||
import type {
|
||||
ConnectSettings,
|
||||
Manifest,
|
||||
TrezorConnect as TrezorConnectInterface,
|
||||
} from '@trezor/connect/src/types';
|
||||
import { ConnectFactoryDependencies, factory } from '@trezor/connect/src/factory';
|
||||
import { CoreInIframe } from './impl/core-in-iframe';
|
||||
import { CoreInPopup } from './impl/core-in-popup';
|
||||
import type { ConnectSettings, Manifest } from '@trezor/connect/src/types';
|
||||
import EventEmitter from 'events';
|
||||
import { CallMethodPayload } from '@trezor/connect';
|
||||
|
||||
type TrezorConnectType = 'core-in-popup' | 'iframe';
|
||||
|
||||
class TrezorConnectProxyHandler implements ProxyHandler<TrezorConnectInterface> {
|
||||
private currentTarget: TrezorConnectType = 'iframe';
|
||||
class ProxyEventEmitter implements EventEmitter {
|
||||
private eventEmitters: EventEmitter[];
|
||||
|
||||
private getTarget(defaultTarget: TrezorConnectInterface) {
|
||||
switch (this.currentTarget) {
|
||||
case 'core-in-popup':
|
||||
return TrezorConnectCoreInPopup;
|
||||
case 'iframe':
|
||||
default:
|
||||
// Use default target
|
||||
return defaultTarget;
|
||||
}
|
||||
constructor(eventEmitters: EventEmitter[]) {
|
||||
this.eventEmitters = eventEmitters;
|
||||
}
|
||||
|
||||
get(defaultTarget: TrezorConnectInterface, prop: string, receiver: any) {
|
||||
if (prop === 'init') {
|
||||
return async (settings: { manifest: Manifest } & Partial<ConnectSettings>) => {
|
||||
if (settings?.useCoreInPopup === true) {
|
||||
this.currentTarget = 'core-in-popup';
|
||||
} else {
|
||||
this.currentTarget = 'iframe';
|
||||
}
|
||||
emit(eventName: string | symbol, ...args: any[]): boolean {
|
||||
this.eventEmitters.forEach(emitter => emitter.emit(eventName, ...args));
|
||||
|
||||
const target = this.getTarget(defaultTarget);
|
||||
return true;
|
||||
}
|
||||
|
||||
return await target.init.apply(target, [settings]);
|
||||
};
|
||||
}
|
||||
on(eventName: string | symbol, listener: (...args: any[]) => void): this {
|
||||
this.eventEmitters.forEach(emitter => emitter.on(eventName, listener));
|
||||
|
||||
return Reflect.get(this.getTarget(defaultTarget), prop, receiver);
|
||||
return this;
|
||||
}
|
||||
|
||||
off(eventName: string | symbol, listener: (...args: any[]) => void): this {
|
||||
this.eventEmitters.forEach(emitter => emitter.off(eventName, listener));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
once(eventName: string | symbol, listener: (...args: any[]) => void): this {
|
||||
this.eventEmitters.forEach(emitter => emitter.once(eventName, listener));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
addListener(eventName: string | symbol, listener: (...args: any[]) => void): this {
|
||||
this.eventEmitters.forEach(emitter => emitter.addListener(eventName, listener));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
prependListener(eventName: string | symbol, listener: (...args: any[]) => void): this {
|
||||
this.eventEmitters.forEach(emitter => emitter.prependListener(eventName, listener));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
prependOnceListener(eventName: string | symbol, listener: (...args: any[]) => void): this {
|
||||
this.eventEmitters.forEach(emitter => emitter.prependOnceListener(eventName, listener));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
removeAllListeners(event?: string | symbol | undefined): this {
|
||||
this.eventEmitters.forEach(emitter => emitter.removeAllListeners(event));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
removeListener(eventName: string | symbol, listener: (...args: any[]) => void): this {
|
||||
this.eventEmitters.forEach(emitter => emitter.removeListener(eventName, listener));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
setMaxListeners(n: number): this {
|
||||
this.eventEmitters.forEach(emitter => emitter.setMaxListeners(n));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
eventNames(): (string | symbol)[] {
|
||||
return this.eventEmitters[0].eventNames();
|
||||
}
|
||||
|
||||
getMaxListeners(): number {
|
||||
return this.eventEmitters[0].getMaxListeners();
|
||||
}
|
||||
|
||||
listenerCount(eventName: string | symbol, listener?: FunctionConstructor | undefined) {
|
||||
return this.eventEmitters[0].listenerCount(eventName, listener);
|
||||
}
|
||||
|
||||
rawListeners(eventName: string | symbol) {
|
||||
return this.eventEmitters[0].rawListeners(eventName);
|
||||
}
|
||||
|
||||
listeners(eventName: string | symbol) {
|
||||
return this.eventEmitters[0].listeners(eventName);
|
||||
}
|
||||
}
|
||||
|
||||
const TrezorConnect = new Proxy<TrezorConnectInterface>(
|
||||
TrezorConnectIframe, // default target
|
||||
new TrezorConnectProxyHandler(),
|
||||
);
|
||||
/**
|
||||
* Implementation of TrezorConnect that can dynamically switch between iframe and core-in-popup implementations
|
||||
*/
|
||||
export class TrezorConnectDynamicImpl implements ConnectFactoryDependencies {
|
||||
public eventEmitter: EventEmitter;
|
||||
|
||||
private currentTarget: TrezorConnectType = 'iframe';
|
||||
private coreInIframeImpl: CoreInIframe;
|
||||
private coreInPopupImpl: CoreInPopup;
|
||||
|
||||
public constructor() {
|
||||
this.coreInIframeImpl = new CoreInIframe();
|
||||
this.coreInPopupImpl = new CoreInPopup();
|
||||
this.eventEmitter = new ProxyEventEmitter([
|
||||
this.coreInIframeImpl.eventEmitter,
|
||||
this.coreInPopupImpl.eventEmitter,
|
||||
]);
|
||||
}
|
||||
|
||||
private getTarget() {
|
||||
return this.currentTarget === 'iframe' ? this.coreInIframeImpl : this.coreInPopupImpl;
|
||||
}
|
||||
|
||||
public manifest(data: Manifest) {
|
||||
this.getTarget().manifest(data);
|
||||
}
|
||||
|
||||
public init(settings: Partial<ConnectSettings> = {}) {
|
||||
if (settings.useCoreInPopup) {
|
||||
this.currentTarget = 'core-in-popup';
|
||||
} else {
|
||||
this.currentTarget = 'iframe';
|
||||
}
|
||||
|
||||
return this.getTarget().init(settings);
|
||||
}
|
||||
|
||||
public call(params: CallMethodPayload) {
|
||||
return this.getTarget().call(params);
|
||||
}
|
||||
|
||||
public requestLogin(params: any) {
|
||||
return this.getTarget().requestLogin(params);
|
||||
}
|
||||
|
||||
public uiResponse(params: any) {
|
||||
return this.getTarget().uiResponse(params);
|
||||
}
|
||||
|
||||
public renderWebUSBButton() {
|
||||
return this.getTarget().renderWebUSBButton();
|
||||
}
|
||||
|
||||
public disableWebUSB() {
|
||||
return this.getTarget().disableWebUSB();
|
||||
}
|
||||
|
||||
public requestWebUSBDevice() {
|
||||
return this.getTarget().requestWebUSBDevice();
|
||||
}
|
||||
|
||||
public cancel() {
|
||||
return this.getTarget().cancel();
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
this.eventEmitter.removeAllListeners();
|
||||
|
||||
return this.getTarget().dispose();
|
||||
}
|
||||
}
|
||||
|
||||
const methods = new TrezorConnectDynamicImpl();
|
||||
|
||||
const TrezorConnect = factory({
|
||||
eventEmitter: methods.eventEmitter,
|
||||
init: methods.init.bind(methods),
|
||||
call: methods.call.bind(methods),
|
||||
manifest: methods.manifest.bind(methods),
|
||||
requestLogin: methods.requestLogin.bind(methods),
|
||||
uiResponse: methods.uiResponse.bind(methods),
|
||||
renderWebUSBButton: methods.renderWebUSBButton.bind(methods),
|
||||
disableWebUSB: methods.disableWebUSB.bind(methods),
|
||||
requestWebUSBDevice: methods.requestWebUSBDevice.bind(methods),
|
||||
cancel: methods.cancel.bind(methods),
|
||||
dispose: methods.dispose.bind(methods),
|
||||
});
|
||||
|
||||
export default TrezorConnect;
|
||||
export * from '@trezor/connect/src/exports';
|
||||
|
||||
@@ -11,7 +11,7 @@ type DeferredManagerOptions = {
|
||||
initialId?: number;
|
||||
};
|
||||
|
||||
type DeferredManager<T> = {
|
||||
export type DeferredManager<T> = {
|
||||
/** How many pending promises are there */
|
||||
length: () => number;
|
||||
/** ID of the next created promise (for specific use case, should be removed in the future) */
|
||||
|
||||
Reference in New Issue
Block a user