@trezor/connect-web
Version:
High-level javascript interface for Trezor hardware wallet in web environment.
419 lines • 15.4 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 utils_1 = require("@trezor/utils");
const showPopupRequest_1 = require("./showPopupRequest");
const serviceworker_window_1 = require("../channels/serviceworker-window");
const window_window_1 = require("../channels/window-window");
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();
}
}
async postMessage(message) {
if (!this.popupWindow && message.type !== events_2.UI.REQUEST_UI_WINDOW && this.openTimeout) {
this.clear();
(0, showPopupRequest_1.showPopupRequest)(this.open.bind(this), () => {
this.emitClosed();
});
return;
}
if (this.popupPromise) {
await this.popupPromise.promise;
}
if (this.popupWindow?.mode === 'window') {
this.popupWindow.window.postMessage(message, this.origin);
}
else if (this.popupWindow?.mode === 'tab') {
this.channel.postMessage(message);
}
}
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