@trezor/connect-web
Version:
High-level javascript interface for Trezor hardware wallet in web environment.
391 lines (390 loc) • 12.5 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.PopupManager = void 0;
const tslib_1 = require("tslib");
const events_1 = tslib_1.__importDefault(require("events"));
const version_1 = require("@trezor/connect/lib/data/version");
const events_2 = require("@trezor/connect/lib/events");
const urlUtils_1 = require("@trezor/connect/lib/utils/urlUtils");
const serviceworker_window_1 = require("@trezor/connect-common/lib/messageChannel/serviceworker-window");
const window_window_1 = require("@trezor/connect-common/lib/messageChannel/window-window");
const utils_1 = require("@trezor/utils");
const showPopupRequest_1 = require("./showPopupRequest");
const checkIfTabExists = tabId => new Promise(resolve => {
if (!tabId) return resolve(false);
function callback() {
if (chrome.runtime.lastError) {
resolve(false);
} else {
resolve(true);
}
}
chrome.tabs.get(tabId, callback);
});
const POPUP_REQUEST_TIMEOUT = 850;
const POPUP_CLOSE_INTERVAL = 500;
const POPUP_OPEN_TIMEOUT = 5000;
class PopupManager extends events_1.default {
popupWindow;
settings;
origin;
locked = false;
channel;
channelIframe;
handshakePromise;
iframeHandshakePromise;
popupPromise;
requestTimeout;
openTimeout;
closeInterval;
extensionTabId = 0;
logger;
constructor(settings, {
logger
}) {
super();
this.settings = settings;
this.origin = (0, urlUtils_1.getOrigin)(settings.popupSrc);
this.logger = logger;
if (this.isWebExtensionWithTab()) {
this.channel = new serviceworker_window_1.ServiceWorkerWindowChannel({
name: 'trezor-connect',
channel: {
here: '@trezor/connect-webextension',
peer: '@trezor/connect-content-script'
},
logger,
currentId: () => {
if (this.popupWindow?.mode === 'tab') return this.popupWindow?.tab.id;
},
legacyMode: !this.settings.useCoreInPopup
});
} else {
this.channel = new window_window_1.WindowWindowChannel({
windowHere: window,
windowPeer: () => {
if (this.popupWindow?.mode === 'window') return this.popupWindow?.window;
},
channel: {
here: '@trezor/connect-web',
peer: '@trezor/connect-popup'
},
logger,
origin: this.origin,
legacyMode: !this.settings.useCoreInPopup
});
}
if (!this.settings.useCoreInPopup) {
this.iframeHandshakePromise = (0, utils_1.createDeferred)(events_2.IFRAME.LOADED);
this.channelIframe = new window_window_1.WindowWindowChannel({
windowHere: window,
windowPeer: () => window,
channel: {
here: '@trezor/connect-web',
peer: '@trezor/connect-iframe'
},
logger,
origin: this.origin
});
this.channelIframe?.on('message', this.handleMessage.bind(this));
}
if (this.settings.useCoreInPopup) {
this.handshakePromise = (0, utils_1.createDeferred)();
this.channel.on('message', this.handleCoreMessage.bind(this));
return;
} else if (this.isWebExtensionWithTab()) {
this.channel.on('message', this.handleExtensionMessage.bind(this));
} else {
this.channel.on('message', this.handleMessage.bind(this));
}
this.channel.init();
}
async request() {
if (this.settings.useCoreInPopup && this.popupWindow?.mode === 'tab') {
const currentPopupExists = await checkIfTabExists(this.popupWindow?.tab?.id);
if (!currentPopupExists) {
this.clear();
}
}
if (this.locked) {
if (this.popupWindow?.mode === 'tab' && this.popupWindow.tab.id) {
chrome.tabs.update(this.popupWindow.tab.id, {
active: true
});
} else if (this.popupWindow?.mode === 'window') {
this.popupWindow.window.focus();
}
return;
}
if (this.popupWindow && !this.locked) {
this.close();
}
const openFn = this.open.bind(this);
this.locked = true;
const timeout = this.settings.env === 'webextension' ? 1 : POPUP_REQUEST_TIMEOUT;
this.requestTimeout = setTimeout(() => {
this.requestTimeout = undefined;
openFn();
}, timeout);
}
unlock() {
this.locked = false;
}
open() {
const src = this.settings.popupSrc;
this.popupPromise = (0, utils_1.createDeferred)(events_2.POPUP.LOADED);
const url = this.buildPopupUrl(src);
this.openWrapper(url);
this.closeInterval = setInterval(() => {
if (!this.popupWindow) return;
if (this.popupWindow.mode === 'tab' && this.popupWindow.tab.id) {
chrome.tabs.get(this.popupWindow.tab.id, tab => {
if (!tab) {
this.emitClosed();
this.clear();
}
});
} else if (this.popupWindow.mode === 'window' && this.popupWindow.window.closed) {
this.clear();
this.emitClosed();
}
}, POPUP_CLOSE_INTERVAL);
if (this.settings.useCoreInPopup) {
return;
}
this.openTimeout = setTimeout(() => {
this.clear();
(0, showPopupRequest_1.showPopupRequest)(this.open.bind(this), () => {
this.emitClosed();
});
}, POPUP_OPEN_TIMEOUT);
}
buildPopupUrl(src) {
const params = new URLSearchParams();
params.set('version', version_1.VERSION);
params.set('env', this.settings.env);
if (this.settings.env === 'webextension' && chrome?.runtime?.id) {
params.set('extension-id', chrome.runtime.id);
params.set('cs-ver', version_1.CONTENT_SCRIPT_VERSION.toString());
}
return src + '?' + params.toString();
}
openWrapper(url) {
if (this.isWebExtensionWithTab()) {
chrome.windows.getCurrent(currentWindow => {
this.logger.debug('opening popup. currentWindow: ', currentWindow);
if (currentWindow.type !== 'normal') {
chrome.windows.create({
url
}, newWindow => {
chrome.tabs.query({
windowId: newWindow?.id,
active: true
}, tabs => {
this.popupWindow = {
mode: 'tab',
tab: tabs[0]
};
this.injectContentScript(tabs[0].id);
});
});
} else {
chrome.tabs.query({
currentWindow: true,
active: true
}, tabs => {
this.extensionTabId = tabs[0].id;
chrome.tabs.create({
url,
index: tabs[0].index + 1
}, tab => {
this.popupWindow = {
mode: 'tab',
tab
};
this.injectContentScript(tab.id);
});
});
}
});
} else {
const windowResult = window.open(url, 'modal');
if (!windowResult) return;
this.popupWindow = {
mode: 'window',
window: windowResult
};
}
if (!this.channel.isConnected) {
this.channel.connect();
}
}
injectContentScript = tabId => {
chrome.permissions.getAll(permissions => {
if (permissions.permissions?.includes('scripting')) {
(0, utils_1.scheduleAction)(() => chrome.scripting.executeScript({
target: {
tabId
},
func: () => {}
}).then(() => {
this.logger.debug('content script injected');
}).catch(error => {
this.logger.error('content script injection error', error);
throw error;
}), {
attempts: new Array(3).fill({
timeout: 100
})
});
} else {}
});
};
handleCoreMessage(message) {
if (message.type === events_2.POPUP.BOOTSTRAP) {
this.channel.init();
} else if (message.type === events_2.POPUP.LOADED) {
this.handleMessage(message);
this.channel.postMessage({
type: events_2.POPUP.INIT,
payload: {
settings: this.settings,
useCore: true
}
});
} else if (message.type === events_2.POPUP.CORE_LOADED) {
this.channel.postMessage({
type: events_2.POPUP.HANDSHAKE,
payload: {
settings: this.settings
}
});
this.handshakePromise?.resolve();
} else if (message.type === events_2.POPUP.CLOSED) {
this.emitClosed();
} else if (message.type === events_2.POPUP.CONTENT_SCRIPT_LOADED) {
const {
contentScriptVersion
} = message.payload;
if (contentScriptVersion !== version_1.CONTENT_SCRIPT_VERSION) {
console.warn(`Content script version mismatch. Expected ${version_1.CONTENT_SCRIPT_VERSION}, got ${contentScriptVersion}`);
}
} else if (message.event === events_2.DEVICE_EVENT) {
this.emit(events_2.DEVICE_EVENT, message);
}
}
handleExtensionMessage(data) {
if (data.type === events_2.POPUP.ERROR || data.type === events_2.POPUP.LOADED || data.type === events_2.POPUP.BOOTSTRAP) {
this.handleMessage(data);
} else if (data.type === events_2.POPUP.EXTENSION_USB_PERMISSIONS) {
chrome.tabs.query({
currentWindow: true,
active: true
}, tabs => {
chrome.tabs.create({
url: 'trezor-usb-permissions.html',
index: tabs[0].index + 1
}, _tab => {});
});
} else if (data.type === events_2.POPUP.CLOSE_WINDOW) {
this.clear();
}
}
handleMessage(data) {
if (data.type === events_2.IFRAME.LOADED) {
this.iframeHandshakePromise?.resolve(data.payload);
} else if (data.type === events_2.POPUP.BOOTSTRAP) {
if (this.openTimeout) clearTimeout(this.openTimeout);
} else if (data.type === events_2.POPUP.ERROR && this.popupWindow) {
const errorMessage = data.payload && typeof data.payload.error === 'string' ? data.payload.error : null;
this.emit(events_2.POPUP.CLOSED, errorMessage ? `Popup error: ${errorMessage}` : null);
this.clear();
} else if (data.type === events_2.POPUP.LOADED) {
if (this.openTimeout) clearTimeout(this.openTimeout);
if (this.popupPromise) {
this.popupPromise.resolve();
this.popupPromise = undefined;
}
this.iframeHandshakePromise?.promise.then(payload => {
this.channel.postMessage({
type: events_2.POPUP.INIT,
payload: {
...payload,
settings: this.settings
}
});
});
} else if (data.type === events_2.POPUP.CANCEL_POPUP_REQUEST) {
clearTimeout(this.requestTimeout);
if (this.popupPromise) {
this.close();
}
this.unlock();
} else if (data.type === events_2.UI.CLOSE_UI_WINDOW) {
this.clear(false);
}
}
clear(focus = true) {
this.locked = false;
this.popupPromise = undefined;
this.handshakePromise = (0, utils_1.createDeferred)();
if (this.channel) {
this.channel.disconnect();
}
if (this.requestTimeout) {
clearTimeout(this.requestTimeout);
this.requestTimeout = undefined;
}
if (this.openTimeout) {
clearTimeout(this.openTimeout);
this.openTimeout = undefined;
}
if (this.closeInterval) {
clearInterval(this.closeInterval);
this.closeInterval = undefined;
}
if (focus && this.extensionTabId) {
chrome.tabs.update(this.extensionTabId, {
active: true
});
this.extensionTabId = 0;
}
}
close() {
if (!this.popupWindow) return;
this.logger.debug('closing popup');
if (this.popupWindow.mode === 'tab') {
let _e = chrome.runtime.lastError;
if (this.popupWindow.tab.id) {
chrome.tabs.remove(this.popupWindow.tab.id, () => {
_e = chrome.runtime.lastError;
if (_e) {
this.logger.error('closed with error', _e);
}
});
}
} else if (this.popupWindow.mode === 'window') {
this.popupWindow.window.close();
}
this.popupWindow = undefined;
if (this.settings?.useCoreInPopup) {
this.channel.clear();
}
}
isWebExtensionWithTab() {
return this.settings?.env === 'webextension' && typeof chrome !== 'undefined' && typeof chrome?.tabs !== 'undefined';
}
emitClosed() {
if (this.settings?.useCoreInPopup) {
this.channel.resolveMessagePromises({
code: 'Method_Interrupted',
error: events_2.POPUP.CLOSED
});
}
this.emit(events_2.POPUP.CLOSED);
}
}
exports.PopupManager = PopupManager;
//# sourceMappingURL=index.js.map