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;