diff --git a/src/index.html b/src/index.html index 49f35fe..4468529 100644 --- a/src/index.html +++ b/src/index.html @@ -626,6 +626,13 @@

What is TON Magic?

+ + + + diff --git a/src/js/Controller.js b/src/js/Controller.js index 3bb18b8..a1a81a2 100644 --- a/src/js/Controller.js +++ b/src/js/Controller.js @@ -1,23 +1,123 @@ import storage from './util/storage.js'; +import {MethodError, serialiseError} from "./util/MethodError"; +import {JSON_RPC_VERSION} from "./util/const"; +import {PortMessage} from "./util/PortMessage"; +import {SendingTransactionContext} from "./util/SendingTransactionContext"; -let contentScriptPort = null; +/** + * @type {Set} + */ +let contentScriptPort = new Set(); let popupPort = null; const queueToPopup = []; +let currentPopupId = null; +let onPopupClosedOnesListeners = []; + +function onPopupClosedOnes(listener) { + onPopupClosedOnesListeners.push(listener); +} + + +async function repeatCall( fn ) { + let lastError = null; + for(let i = 0; i < 5; i++) { + try { + return await fn(); + } catch (e) { + lastError = e; + // repeat only known errors, not all + if (e && typeof e === 'string' && e.indexOf('unexpected lite server response')) { + console.warn('failed request', e) + } else { + throw e; + } + } + } + throw lastError; +} -const showExtensionPopup = () => { +/** + * Returns an Error if extension.runtime.lastError is present + * this is a workaround for the non-standard error object that's used + * + * @returns {Error|undefined} + */ +function checkForError() { + const lastError = chrome.runtime.lastError; + if (!lastError) { + return undefined; + } + // if it quacks like an Error, its an Error + if (lastError.stack && lastError.message) { + return lastError; + } + // repair incomplete error object (eg chromium v77) + return new Error(lastError.message); +} + +const focusWindowActivePopup = () => { + if (currentPopupId) { + chrome.windows.update(currentPopupId, { focused: true }, () => { + const err = checkForError(); + if (err) { + console.log('cant focus window', currentPopupId, err); + } + }); + } +} + +const showExtensionPopup = async () => { + /** + * @param {chrome.windows.Window} currentPopup + */ const cb = (currentPopup) => { // this._popupId = currentPopup.id + currentPopupId = currentPopup.id; + chrome.windows.onRemoved.addListener(function(windowId){ + if (windowId === currentPopup.id){ + onPopupClosedOnesListeners.forEach(fn => { + try { + fn() + } catch (e) { + console.log(); + } + }); + onPopupClosedOnesListeners = [] + } + }); }; - const creation = chrome.windows.create({ - url: 'popup.html', + const window = await getLastFocusedWindow().catch(e => { + console.log(e) + return null; + }); + const POPUP_WIDTH = 400; + const POPUP_HEIGHT = 600; + chrome.windows.create({ + url: 'popup.html?transactionFromApi=1', type: 'popup', - width: 400, - height: 600, - top: 0, - left: 0, + width: POPUP_WIDTH, + height: POPUP_HEIGHT, + top: window ? window.top : 0, + left: window ? window.left + (window.width - POPUP_WIDTH) : 0, }, cb); }; +/** + * + * @returns {Promise<{width:number,top:number,left:number}|null>} + */ +function getLastFocusedWindow() { + return new Promise((resolve, reject) => { + chrome.windows.getLastFocused((windowObject) => { + const error = checkForError(); + if (error) { + return reject(error); + } + return resolve(windowObject); + }); + }); +} + const BN = TonWeb.utils.BN; const nacl = TonWeb.utils.nacl; const Address = TonWeb.utils.Address; @@ -116,7 +216,9 @@ class Controller { this.pendingMessageResolvers = new Map(); this._lastMsgId = 1; + this.nextViewMessageId = 0; this.whenReady = this._init(); + this.onClosePopupOnesListeners = []; } /** @@ -597,6 +699,9 @@ class Controller { const myAmount = '-' + this.sendingData.amount.toString(); if (txAddr === myAddr && txAmount === myAmount) { + if (this.sendingData.ctx instanceof SendingTransactionContext) { + this.sendingData.ctx.success(); + } this.sendToView('showPopup', { name: 'done', message: formatNanograms(this.sendingData.amount) + ' TON have been sent' @@ -640,7 +745,7 @@ class Controller { } const query = await this.sign(toAddress, amount, comment, null, stateInit); - const all_fees = await query.estimateFee(); + const all_fees = await repeatCall(() => query.estimateFee()) const fees = all_fees.source_fees; const in_fwd_fee = new BN(fees.in_fwd_fee); const storage_fee = new BN(fees.storage_fee); @@ -661,14 +766,38 @@ class Controller { * @param comment? {string | Uint8Array} * @param needQueue? {boolean} * @param stateInit? {Cell} + * @param ctx {SendingTransactionContext|undefined} */ - async showSendConfirm(amount, toAddress, comment, needQueue, stateInit) { - if (!amount.gt(new BN(0)) || this.balance.lt(amount)) { + async showSendConfirm(amount, toAddress, comment, needQueue, stateInit, ctx) { + this.sendToView('showPopup', { + name: 'loader', + }, needQueue); + + const notify = (message) => { + setTimeout(() => { + this.sendToView('showPopup', { + name: 'notify', + message: message, + }, needQueue); + }, 1000) + } + + await this.whenReady; + + if (!amount.gt(new BN(0)) || !this.balance || this.balance.lt(amount)) { this.sendToView('sendCheckFailed'); + if (!amount.gt(new BN(0))) { + ctx && ctx.fail(new MethodError("BAD_AMOUNT", MethodError.ERR_BAD_AMOUNT)); + } else { + ctx && ctx.fail(new MethodError("NOT_ENOUGH_TONS", MethodError.ERR_NOT_ENOUGH_TONS)); + } + ctx && notify('Invalid amount') return; } if (!Address.isValid(toAddress)) { this.sendToView('sendCheckFailed'); + ctx && ctx.fail(new MethodError("BAD_ADDRESS", MethodError.ERR_BAD_ADDRESS)); + ctx && notify('Invalid address') return; } @@ -678,12 +807,16 @@ class Controller { fee = await this.getFees(amount, toAddress, comment, stateInit); } catch (e) { console.error(e); + ctx && ctx.fail(new MethodError("API_FAILED", MethodError.ERR_API_FAILED)); this.sendToView('sendCheckFailed'); + ctx && notify('can\'t calculate fees'); return; } if (this.balance.sub(fee).lt(amount)) { + ctx && ctx.fail(new MethodError("NOT_ENOUGH_TONS", MethodError.ERR_NOT_ENOUGH_TONS)); this.sendToView('sendCheckCantPayFee', {fee}); + ctx && notify('can\'t pay fees'); return; } @@ -695,6 +828,9 @@ class Controller { toAddress: toAddress, fee: fee.toString() }, needQueue); + this.onClosePopupOnes(() => { + ctx && ctx.decline(); + }); this.send(toAddress, amount, comment, null, stateInit); @@ -704,7 +840,7 @@ class Controller { this.processingVisible = true; this.sendToView('showPopup', {name: 'processing'}); const privateKey = await Controller.wordsToPrivateKey(words); - this.send(toAddress, amount, comment, privateKey, stateInit); + this.send(toAddress, amount, comment, privateKey, stateInit, ctx); }; this.sendToView('showPopup', { @@ -713,6 +849,9 @@ class Controller { toAddress: toAddress, fee: fee.toString() }, needQueue); + this.onClosePopupOnes(() => { + ctx && ctx.decline(); + }); } this.sendToView('sendCheckSucceeded'); @@ -752,8 +891,9 @@ class Controller { * @param comment {string} * @param privateKey {string} * @param stateInit? {Cell} + * @param ctx {SendingTransactionContext|undefined} */ - async send(toAddress, amount, comment, privateKey, stateInit) { + async send(toAddress, amount, comment, privateKey, stateInit, ctx) { try { let addressFormat = 0; if (this.isLedger) { @@ -792,25 +932,32 @@ class Controller { if (!seqno) seqno = 0; const query = await this.ledgerApp.transfer(ACCOUNT_NUMBER, this.walletContract, toAddress, amount, seqno, addressFormat); - this.sendingData = {toAddress: toAddress, amount: amount, comment: comment, query: query}; + this.sendingData = {toAddress: toAddress, amount: amount, comment: comment, query: query, ctx:ctx}; this.sendToView('showPopup', {name: 'processing'}); this.processingVisible = true; - await this.sendQuery(query); + const res = repeatCall(() => this.sendQuery(query)); + ctx && ctx.requestSent() + await res; } else { const keyPair = nacl.sign.keyPair.fromSeed(TonWeb.utils.base64ToBytes(privateKey)); const query = await this.sign(toAddress, amount, comment, keyPair, stateInit); - this.sendingData = {toAddress: toAddress, amount: amount, comment: comment, query: query}; - await this.sendQuery(query); + this.sendingData = {toAddress: toAddress, amount: amount, comment: comment, query: query, ctx:ctx}; + + const res = repeatCall(() => this.sendQuery(query)); + ctx && ctx.requestSent() + await res; } } catch (e) { - console.error(e); + ctx && ctx.fail(new MethodError('API_FAILED', MethodError.ERR_API_FAILED) ); + console.error('Fail sending', {toAddress, amount, comment, stateInit}, e); this.sendToView('closePopup'); - alert('Error sending'); + alert(`Error sending: ${e ? e.message : e}`); + throw e; } } @@ -837,6 +984,7 @@ class Controller { } else { this.sendToView('closePopup'); alert('Send error'); + throw new Error("Api failed") } } @@ -883,6 +1031,13 @@ class Controller { // TRANSPORT WITH VIEW sendToView(method, params, needQueue, needResult) { + if (params === undefined || params === null) { + params = {} + } + if (typeof params === 'object') { + this.nextViewMessageId++ + params._viewMessageId = this.nextViewMessageId; + } if (self.view) { const result = self.view.onMessage(method, params); if (needResult) { @@ -973,6 +1128,19 @@ class Controller { break; case 'onClosePopup': this.processingVisible = false; + if (this.onClosePopupOnesListeners.length) { + const messageId = (params && params._viewMessageId) ? params._viewMessageId : 0 + this.onClosePopupOnesListeners.forEach(([minMessageId, fn]) => { + if (minMessageId <= messageId) { + try { + fn(); + } catch (e) { + console.error(e); + } + } + }) + this.onClosePopupOnesListeners = []; + } break; case 'onMagicClick': await storage.setItem('magic', params ? 'true' : 'false'); @@ -989,14 +1157,16 @@ class Controller { } // TRANSPORT WITH DAPP - + // TODO: подумать зачем это нужно + // contentScriptPort это "ссылки" на открытые вкладки бразуераз + // правда ли надо их всех уведомлять о чем-то? sendToDapp(method, params) { - if (contentScriptPort) { - contentScriptPort.postMessage(JSON.stringify({ + contentScriptPort.forEach( port => { + port.postMessage(JSON.stringify({ type: 'gramWalletAPI', - message: {jsonrpc: '2.0', method: method, params: params} + message: {jsonrpc: JSON_RPC_VERSION, method: method, params: params} })); - } + } ) } requestPublicKey(needQueue) { @@ -1041,9 +1211,17 @@ class Controller { case 'ton_getBalance': return (this.balance ? this.balance.toString() : ''); case 'ton_sendTransaction': + await this.whenReady; const param = params[0]; + const ctx = new SendingTransactionContext(); + onPopupClosedOnes(() => { + ctx.decline(); + }); if (!popupPort) { - showExtensionPopup(); + await showExtensionPopup(); + await this.waitViewConnect(); + } else { + focusWindowActivePopup(); } if (param.data) { if (param.dataType === 'hex') { @@ -1057,7 +1235,14 @@ class Controller { if (param.stateInit) { param.stateInit = TonWeb.boc.Cell.oneFromBoc(TonWeb.utils.base64ToBytes(param.stateInit)); } - this.showSendConfirm(new BN(param.value), param.to, param.data, needQueue, param.stateInit); + if (this.activeSendindTransaction) { + this.activeSendindTransaction.decline() + } + this.activeSendindTransaction = ctx; + const waiter = ctx.wait(); + await this.showSendConfirm(new BN(param.value), param.to, param.data, needQueue, param.stateInit, ctx); + await waiter; + this.activeSendindTransaction = null; return true; case 'ton_rawSign': const signParam = params[0]; @@ -1070,6 +1255,20 @@ class Controller { return true; } } + + onClosePopupOnes(listener) { + console.log('register onClosePopupOnes', this.nextViewMessageId) + this.onClosePopupOnesListeners.push([this.nextViewMessageId, listener]); + } + + waitViewConnect() { + if (!this.waitViewConnectPromise) { + this.waitViewConnectPromise = new Promise((resolve) => { + this.onViewConnectedResolver = resolve; + }) + } + return this.waitViewConnectPromise; + } } const controller = new Controller(); @@ -1077,26 +1276,30 @@ const controller = new Controller(); if (IS_EXTENSION) { chrome.runtime.onConnect.addListener(port => { if (port.name === 'gramWalletContentScript') { - contentScriptPort = port; - contentScriptPort.onMessage.addListener(async msg => { + contentScriptPort.add(port) + port.onMessage.addListener(async (msg, port) => { if (msg.type === 'gramWalletAPI_ton_provider_connect') { controller.whenReady.then(() => { controller.initDapp(); }); } - if (!msg.message) return; - const result = await controller.onDappMessage(msg.message.method, msg.message.params); - if (contentScriptPort) { - contentScriptPort.postMessage(JSON.stringify({ - type: 'gramWalletAPI', - message: {jsonrpc: '2.0', id: msg.message.id, method: msg.message.method, result} - })); + if (!msg.message) { + console.warn('Receive bad message', msg); + return; + } + const response = new PortMessage(msg.message.id, msg.message.method); + try { + const result = await controller.onDappMessage(msg.message.method, msg.message.params); + port.postMessage(JSON.stringify(response.result(result))); + } catch (e) { + console.error(`Call method failed: ${msg.message.method}`, msg.message.params, e); + port.postMessage(JSON.stringify(response.error(serialiseError(e)))); } }); - contentScriptPort.onDisconnect.addListener(() => { - contentScriptPort = null; - }); + port.onDisconnect.addListener(port => { + contentScriptPort.delete(port) + }) } else if (port.name === 'gramWalletPopup') { popupPort = port; popupPort.onMessage.addListener(function (msg) { @@ -1126,6 +1329,10 @@ if (IS_EXTENSION) { controller.whenReady.then(async () => { await controller.initView(); runQueueToPopup(); + if (controller.onViewConnectedResolver) { + controller.onViewConnectedResolver(); + controller.onViewConnectedResolver = null; + } }); } }); diff --git a/src/js/extension/contentScript.js b/src/js/extension/contentScript.js index f3f35b1..ca2faa5 100644 --- a/src/js/extension/contentScript.js +++ b/src/js/extension/contentScript.js @@ -13,15 +13,48 @@ function injectScript() { injectScript(); // inject to dapp page -const port = chrome.runtime.connect({name: 'gramWalletContentScript'}); -port.onMessage.addListener(function (msg) { +/** + * @param {any} msg + */ +function onPortMessage(msg) { // Receive msg from Controller.js and resend to dapp page self.postMessage(msg, '*'); // todo: origin -}); +} + +const PORT_NAME = 'gramWalletContentScript' +let port = chrome.runtime.connect({name: PORT_NAME}); +port.onMessage.addListener(onPortMessage); + +function sendMessageToActivePort(payload, isRepeat = false) { + try { + port.postMessage(payload); + } catch (e) { + if (!isRepeat && !!e && !!e.message && e.message.toString().indexOf('disconnected port') !== -1) { + port.onMessage.removeListener(onPortMessage); + port = chrome.runtime.connect({name: PORT_NAME}); + port.onMessage.addListener(onPortMessage); + sendMessageToActivePort(payload, true); + } else { + console.log(`Fail send message to port`, e); + const {message: {id,method}} = payload + const response = { + type: 'gramWalletAPI', + message: { + id: id, + method: method, + error: (!!e && !!e.message) ? {message: e.message} : {message: JSON.stringify(e)}, + jsonrpc: true, + } + } + onPortMessage(JSON.stringify(response)); + } + } +} + self.addEventListener('message', function (event) { if (event.data && (event.data.type === 'gramWalletAPI_ton_provider_write' || event.data.type === 'gramWalletAPI_ton_provider_connect')) { // Receive msg from dapp page and resend to Controller.js - port.postMessage(event.data); + sendMessageToActivePort(event.data); } }); diff --git a/src/js/util/MethodError.js b/src/js/util/MethodError.js new file mode 100644 index 0000000..b2cab0b --- /dev/null +++ b/src/js/util/MethodError.js @@ -0,0 +1,39 @@ + +export class MethodError extends Error { + /** + * @param {string} message + * @param {number} code + */ + constructor(message, code) { + super(message); + this.code = code; + } +} + +MethodError.ERR_BAD_AMOUNT = 1 +MethodError.ERR_BAD_ADDRESS = 2 +MethodError.ERR_API_FAILED = 3 +MethodError.ERR_NOT_ENOUGH_TONS = 4 +MethodError.ERR_USER_DECLINE_REQUEST = 5 +MethodError.ERR_USER_DECLINE_REQUEST_AFTER_SENT_TRANSACTION = 6 + +/** + * @param e + * @returns {{code?: number, message: string}} + */ +export function serialiseError(e) { + if (e instanceof MethodError) { + return { + code: e.code, + message: e.message, + } + } else if (e instanceof Error) { + return { + message: e.message, + } + } else { + return { + message: JSON.stringify(e), + } + } +} \ No newline at end of file diff --git a/src/js/util/PortMessage.js b/src/js/util/PortMessage.js new file mode 100644 index 0000000..1c8f74c --- /dev/null +++ b/src/js/util/PortMessage.js @@ -0,0 +1,37 @@ +import {JSON_RPC_VERSION} from "./const"; + +export class PortMessage { + + + constructor(id, method) { + this.id = id + this.method = method + } + + container() { + return { + type: 'gramWalletAPI', + message: { + jsonrpc: JSON_RPC_VERSION, + id: this.id, + method: this.method, + } + } + } + + result(payload) { + const base = this.container() + base.message.result = payload; + return base; + } + + + /** + * @param {{code?:number, message:string}} err + */ + error(err) { + const base = this.container() + base.message.error = err; + return base; + } +} \ No newline at end of file diff --git a/src/js/util/SendingTransactionContext.js b/src/js/util/SendingTransactionContext.js new file mode 100644 index 0000000..6377588 --- /dev/null +++ b/src/js/util/SendingTransactionContext.js @@ -0,0 +1,46 @@ +import {MethodError} from "./MethodError"; + +export class SendingTransactionContext { + + + constructor() { + this.promise = null; + this.promiseResolvers = {resolve: (payload) => {}, reject: (err) => {} }; + this.requestWasSent = false; + } + + /** + * @param {MethodError} error + */ + fail(error) { + const {reject} = this.promiseResolvers; + reject(error); + } + + decline() { + if (this.requestWasSent) { + this.fail(new MethodError("USER_DECLINE_AFTER_SENT_TRANSACTION", MethodError.ERR_USER_DECLINE_REQUEST_AFTER_SENT_TRANSACTION)) + } else { + this.fail(new MethodError("USER_DECLINE_REQUEST", MethodError.ERR_USER_DECLINE_REQUEST)) + } + } + + requestSent() { + this.requestWasSent = true; + } + + success() { + const {resolve} = this.promiseResolvers; + resolve(true); + } + + + wait() { + if (!this.promise) { + this.prmise = new Promise((resolve,reject) => { + this.promiseResolvers = {resolve, reject}; + }); + } + return this.prmise; + } +} \ No newline at end of file diff --git a/src/js/util/const.js b/src/js/util/const.js new file mode 100644 index 0000000..714e384 --- /dev/null +++ b/src/js/util/const.js @@ -0,0 +1 @@ +export const JSON_RPC_VERSION = '2.0' \ No newline at end of file diff --git a/src/js/view/Lottie.js b/src/js/view/Lottie.js index 4f6d66a..d884484 100644 --- a/src/js/view/Lottie.js +++ b/src/js/view/Lottie.js @@ -51,9 +51,12 @@ function initLottie(div) { async function initLotties() { const divs = $$('tgs-player'); + const promises = []; for (let i = 0; i < divs.length; i++) { - await initLottie(divs[i]); + const p = initLottie(divs[i]); + promises.push(p); } + await Promise.all(promises); } export {initLotties, lotties}; \ No newline at end of file diff --git a/src/js/view/View.js b/src/js/view/View.js index 2f2a95c..bb62d2e 100644 --- a/src/js/view/View.js +++ b/src/js/view/View.js @@ -20,6 +20,17 @@ const toNano = TonWeb.utils.toNano; const formatNanograms = TonWeb.utils.fromNano; const BN = TonWeb.utils.BN; +function isTransactionFromApi() { + const params = new URLSearchParams(window.location.search); + return !!params.get('transactionFromApi'); +} + +function closeIfTxApi() { + if (isTransactionFromApi()) { + window.close(); + } +} + function toggleLottie(lottie, visible, params) { params = params || {}; clearTimeout(lottie.hideTimeout); @@ -57,6 +68,8 @@ class View { this.isTestnet = false; /** @type {string} */ this.popup = ''; // current opened popup + /** @type {number} **/ + this._viewMessageId = 0; this.createWordInputs({ count: IMPORT_WORDS_COUNT, @@ -276,9 +289,9 @@ class View { toggle($('#receive_showAddressOnDeviceBtn'), !!this.isLedger); this.showPopup('receive'); }); - $('#sendButton').addEventListener('click', () => this.onMessage('showPopup', {name: 'send'})); + $('#sendButton').addEventListener('click', () => this.onSelfMessage('showPopup', {name: 'send'})); - $('#modal').addEventListener('click', () => this.closePopup()); + $('#modal').addEventListener('click', () => this.closePopupFromUi()); if (IS_FIREFOX) { toggle($('#menu_magic'), false); @@ -304,7 +317,7 @@ class View { $('#menu_extension_chrome').addEventListener('click', () => window.open('https://chrome.google.com/webstore/detail/ton-wallet/nphplpgoakhhjchkkhmiggakijnkhfnd', '_blank')); $('#menu_extension_firefox').addEventListener('click', () => window.open('https://addons.mozilla.org/ru/firefox/addon/', '_blank')); $('#menu_about').addEventListener('click', () => this.showPopup('about')); - $('#menu_changePassword').addEventListener('click', () => this.onMessage('showPopup', {name: 'changePassword'})); + $('#menu_changePassword').addEventListener('click', () => this.onSelfMessage('showPopup', {name: 'changePassword'})); $('#menu_backupWallet').addEventListener('click', () => this.sendMessage('onBackupWalletClick')); $('#menu_delete').addEventListener('click', () => this.showPopup('delete')); @@ -312,7 +325,7 @@ class View { $('#receive_invoiceBtn').addEventListener('click', () => this.onCreateInvoiceClick()); $('#receive_shareBtn').addEventListener('click', () => this.onShareAddressClick(false)); $('#receive .addr').addEventListener('click', () => this.onShareAddressClick(true)); - $('#receive_closeBtn').addEventListener('click', () => this.closePopup()); + $('#receive_closeBtn').addEventListener('click', () => this.closePopupFromUi()); $('#invoice_qrBtn').addEventListener('click', () => this.onCreateInvoiceQrClick()); $('#invoice_shareBtn').addEventListener('click', () => this.onShareInvoiceClick()); @@ -322,9 +335,9 @@ class View { $('#invoiceQr_closeBtn').addEventListener('click', () => this.showPopup('invoice')); $('#transaction_sendBtn').addEventListener('click', () => this.onTransactionButtonClick()); - $('#transaction_closeBtn').addEventListener('click', () => this.closePopup()); + $('#transaction_closeBtn').addEventListener('click', () => this.closePopupFromUi()); - $('#connectLedger_cancelBtn').addEventListener('click', () => this.closePopup()); + $('#connectLedger_cancelBtn').addEventListener('click', () => this.closePopupFromUi()); $('#send_btn').addEventListener('click', (e) => { const amount = Number($('#amountInput').value); @@ -343,19 +356,19 @@ class View { this.toggleButtonLoader(e.currentTarget, true); this.sendMessage('onSend', {amount: amountNano.toString(), toAddress, comment}); }); - $('#send_closeBtn').addEventListener('click', () => this.closePopup()); + $('#send_closeBtn').addEventListener('click', () => this.closePopupFromUi()); - $('#sendConfirm_closeBtn').addEventListener('click', () => this.closePopup()); - $('#sendConfirm_cancelBtn').addEventListener('click', () => this.closePopup()); - $('#sendConfirm_okBtn').addEventListener('click', () => this.onMessage('showPopup', {name: 'enterPassword'})); + $('#sendConfirm_closeBtn').addEventListener('click', () => this.closePopupFromUi()); + $('#sendConfirm_cancelBtn').addEventListener('click', () => this.closePopupFromUi()); + $('#sendConfirm_okBtn').addEventListener('click', () => this.onSelfMessage('showPopup', {name: 'enterPassword'})); - $('#signConfirm_closeBtn').addEventListener('click', () => this.closePopup()); - $('#signConfirm_cancelBtn').addEventListener('click', () => this.closePopup()); - $('#signConfirm_okBtn').addEventListener('click', () => this.onMessage('showPopup', {name: 'enterPassword'})); + $('#signConfirm_closeBtn').addEventListener('click', () => this.closePopupFromUi()); + $('#signConfirm_cancelBtn').addEventListener('click', () => this.closePopupFromUi()); + $('#signConfirm_okBtn').addEventListener('click', () => this.onSelfMessage('showPopup', {name: 'enterPassword'})); - $('#processing_closeBtn').addEventListener('click', () => this.closePopup()); - $('#done_closeBtn').addEventListener('click', () => this.closePopup()); - $('#about_closeBtn').addEventListener('click', () => this.closePopup()); + $('#processing_closeBtn').addEventListener('click', () => this.closePopupFromUi()); + $('#done_closeBtn').addEventListener('click', () => this.closePopupFromUi()); + $('#about_closeBtn').addEventListener('click', () => this.closePopupFromUi()); $('#about_version').addEventListener('click', (e) => { if (e.shiftKey) { this.showAlert({ @@ -379,7 +392,7 @@ class View { } }); - $('#changePassword_cancelBtn').addEventListener('click', () => this.closePopup()); + $('#changePassword_cancelBtn').addEventListener('click', () => this.closePopupFromUi()); $('#changePassword_okBtn').addEventListener('click', async (e) => { const oldPassword = $('#changePassword_oldInput').value; const newPassword = $('#changePassword_newInput').value; @@ -401,7 +414,7 @@ class View { this.sendMessage('onChangePassword', {oldPassword, newPassword}); }); - $('#enterPassword_cancelBtn').addEventListener('click', () => this.closePopup()); + $('#enterPassword_cancelBtn').addEventListener('click', () => this.closePopupFromUi()); $('#enterPassword_okBtn').addEventListener('click', async (e) => { const password = $('#enterPassword_input').value; @@ -409,7 +422,7 @@ class View { this.sendMessage('onEnterPassword', {password}); }); - $('#delete_cancelBtn').addEventListener('click', () => this.closePopup()); + $('#delete_cancelBtn').addEventListener('click', () => this.closePopupFromUi()); $('#delete_okBtn').addEventListener('click', () => this.sendMessage('disconnect')); } @@ -472,7 +485,7 @@ class View { toggleFaded($('#modal'), name !== ''); - const popups = ['alert', 'receive', 'invoice', 'invoiceQr', 'send', 'sendConfirm', 'signConfirm', 'processing', 'done', 'menuDropdown', 'about', 'delete', 'changePassword', 'enterPassword', 'transaction', 'connectLedger']; + const popups = ['alert', 'receive', 'invoice', 'invoiceQr', 'send', 'sendConfirm', 'signConfirm', 'processing', 'done', 'menuDropdown', 'about', 'delete', 'changePassword', 'enterPassword', 'transaction', 'connectLedger', 'loader']; popups.forEach(popup => { toggleFaded($('#' + popup), name === popup); @@ -490,6 +503,11 @@ class View { this.sendMessage('onClosePopup'); } + closePopupFromUi() { + this.closePopup() + closeIfTxApi(); + } + // BACKUP SCREEN setBackupWords(words) { @@ -879,7 +897,7 @@ class View { } onTransactionButtonClick() { - this.onMessage('showPopup', {name: 'send', toAddr: this.currentTransactionAddr}); + this.onSelfMessage('showPopup', {name: 'send', toAddr: this.currentTransactionAddr}); } // SEND POPUP @@ -923,7 +941,7 @@ class View { // RECEIVE INVOICE POPUP onCreateInvoiceClick() { - this.onMessage('showPopup', {name: 'invoice'}); + this.onSelfMessage('showPopup', {name: 'invoice'}); } updateInvoiceLink() { @@ -944,7 +962,7 @@ class View { // RECEIVE INVOICE QR POPUP onCreateInvoiceQrClick() { - this.onMessage('showPopup', {name: 'invoiceQr'}); + this.onSelfMessage('showPopup', {name: 'invoiceQr'}); } drawInvoiceQr(link) { @@ -965,6 +983,12 @@ class View { // send message to Controller.js sendMessage(method, params) { + if (typeof params === "undefined" || params === null) { + params = {} + } + if (typeof params === "object") { + params._viewMessageId = this._viewMessageId; + } if (this.controller) { this.controller.onViewMessage(method, params); } else { @@ -972,8 +996,22 @@ class View { } } + /** + * @param {string} method + * @param {object} params + */ + onSelfMessage(method, params) { + params._viewMessageId = this._viewMessageId; + this.onMessage(method, params); + } + // receive message from Controller.js onMessage(method, params) { + let messageId = 0 + if (params && params._viewMessageId) { + messageId = params._viewMessageId; + this._viewMessageId = Math.max(params._viewMessageId, this._viewMessageId); + } switch (method) { case 'disableCreated': $('#createdContinueButton').disabled = params; @@ -1095,6 +1133,10 @@ class View { break; case 'showPopup': + if (this.currentPopUpId && messageId < this.currentPopUpId) { + break; + } + this.currentPopUpId = messageId; this.showPopup(params.name); switch (params.name) { @@ -1138,6 +1180,13 @@ class View { const hex = params.data.length > 48 ? params.data.substring(0, 47) + '…' : params.data; setAddr($('#signConfirmData'), hex); break; + case 'notify': + $('#notify').innerText = params.message; + setTimeout(() => { + triggerClass($('#notify'), 'faded-show', 2000); + toggleFaded($('#modal'), false); + }, 16) + break; } break;