From d22900025805897d46d082b3643d442ee3d2fdb2 Mon Sep 17 00:00:00 2001 From: sneak Date: Fri, 27 Feb 2026 12:06:32 +0700 Subject: [PATCH] Add dedicated wait/success/error screens for transaction status After broadcast, the user is taken to a full-screen wait view showing the amount, recipient, tx hash (copyable + etherscan link), and a count-up timer. The view polls every 10 seconds for confirmation. On confirmation: navigates to success screen showing block number, tx hash, and a Done button that returns to the address view. On 60-second timeout or error: navigates to error screen with the failure message, tx hash (if available), and Done button. Replaces the previous inline confirm-status div that was crammed onto the confirmation page. --- src/popup/index.html | 78 +++++++++++++++++- src/popup/index.js | 2 + src/popup/views/confirmTx.js | 67 ++------------- src/popup/views/helpers.js | 3 + src/popup/views/txStatus.js | 154 +++++++++++++++++++++++++++++++++++ 5 files changed, 238 insertions(+), 66 deletions(-) create mode 100644 src/popup/views/txStatus.js diff --git a/src/popup/index.html b/src/popup/index.html index c716436..0e05b95 100644 --- a/src/popup/index.html +++ b/src/popup/index.html @@ -494,10 +494,80 @@ > Send - + + + + + + + + + + diff --git a/src/popup/index.js b/src/popup/index.js index 598dfa9..b28bc23 100644 --- a/src/popup/index.js +++ b/src/popup/index.js @@ -15,6 +15,7 @@ const addressDetail = require("./views/addressDetail"); const addressToken = require("./views/addressToken"); const send = require("./views/send"); const confirmTx = require("./views/confirmTx"); +const txStatus = require("./views/txStatus"); const receive = require("./views/receive"); const addToken = require("./views/addToken"); const settings = require("./views/settings"); @@ -111,6 +112,7 @@ async function init() { addressToken.init(ctx); send.init(ctx); confirmTx.init(ctx); + txStatus.init(ctx); receive.init(ctx); addToken.init(ctx); settings.init(ctx); diff --git a/src/popup/views/confirmTx.js b/src/popup/views/confirmTx.js index a8225c4..e7a4ca6 100644 --- a/src/popup/views/confirmTx.js +++ b/src/popup/views/confirmTx.js @@ -27,6 +27,7 @@ const { isScamAddress } = require("../../shared/scamlist"); const { ERC20_ABI } = require("../../shared/constants"); const { log } = require("../../shared/log"); const makeBlockie = require("ethereum-blockies-base64"); +const txStatus = require("./txStatus"); const EXT_ICON = `` + @@ -36,7 +37,6 @@ const EXT_ICON = ``; let pendingTx = null; -let elapsedTimer = null; function etherscanTokenLink(address) { return `https://etherscan.io/token/${address}`; @@ -217,7 +217,6 @@ function show(txInfo) { // Gas estimate — show placeholder then fetch async $("confirm-fee").classList.remove("hidden"); $("confirm-fee-amount").textContent = "Estimating..."; - $("confirm-status").classList.add("hidden"); showView("confirm-tx"); estimateGas(txInfo); @@ -309,10 +308,7 @@ function init(ctx) { hidePasswordModal(); - const statusEl = $("confirm-status"); - statusEl.textContent = "Sending..."; - statusEl.classList.remove("hidden"); - + let tx; try { const signer = getSignerForAddress( wallet, @@ -322,7 +318,6 @@ function init(ctx) { const provider = getProvider(state.rpcUrl); const connectedSigner = signer.connect(provider); - let tx; if (pendingTx.token === "ETH") { tx = await connectedSigner.sendTransaction({ to: pendingTx.to, @@ -339,62 +334,10 @@ function init(ctx) { tx = await contract.transfer(pendingTx.to, amount); } - // Disable send button immediately after broadcast - const sendBtn = $("btn-confirm-send"); - sendBtn.disabled = true; - sendBtn.classList.add("text-muted"); - - // Show etherscan link and elapsed timer - const broadcastTime = Date.now(); - statusEl.innerHTML = ""; - statusEl.appendChild( - document.createTextNode( - "Broadcast. Waiting for confirmation... ", - ), - ); - const timerSpan = document.createElement("span"); - timerSpan.textContent = "(0s)"; - statusEl.appendChild(timerSpan); - statusEl.appendChild(document.createElement("br")); - const txLink = document.createElement("a"); - txLink.href = "https://etherscan.io/tx/" + tx.hash; - txLink.target = "_blank"; - txLink.rel = "noopener"; - txLink.className = "underline decoration-dashed break-all"; - txLink.textContent = tx.hash; - statusEl.appendChild(document.createTextNode("Tx: ")); - statusEl.appendChild(txLink); - - if (elapsedTimer) clearInterval(elapsedTimer); - elapsedTimer = setInterval(() => { - const elapsed = Math.floor((Date.now() - broadcastTime) / 1000); - timerSpan.textContent = "(" + elapsed + "s)"; - }, 1000); - - const receipt = await tx.wait(); - clearInterval(elapsedTimer); - elapsedTimer = null; - - statusEl.innerHTML = ""; - statusEl.appendChild( - document.createTextNode( - "Confirmed in block " + receipt.blockNumber + ". Tx: ", - ), - ); - const link = document.createElement("a"); - link.href = "https://etherscan.io/tx/" + receipt.hash; - link.target = "_blank"; - link.rel = "noopener"; - link.className = "underline decoration-dashed break-all"; - link.textContent = receipt.hash; - statusEl.appendChild(link); - ctx.doRefreshAndRender(); + txStatus.showWait(pendingTx, tx.hash); } catch (e) { - if (elapsedTimer) { - clearInterval(elapsedTimer); - elapsedTimer = null; - } - statusEl.textContent = "Failed: " + (e.shortMessage || e.message); + const hash = tx ? tx.hash : null; + txStatus.showError(pendingTx, hash, e.shortMessage || e.message); } }); } diff --git a/src/popup/views/helpers.js b/src/popup/views/helpers.js index 2f25af7..0d37291 100644 --- a/src/popup/views/helpers.js +++ b/src/popup/views/helpers.js @@ -16,6 +16,9 @@ const VIEWS = [ "address-token", "send", "confirm-tx", + "wait-tx", + "success-tx", + "error-tx", "receive", "add-token", "settings", diff --git a/src/popup/views/txStatus.js b/src/popup/views/txStatus.js new file mode 100644 index 0000000..cbce68c --- /dev/null +++ b/src/popup/views/txStatus.js @@ -0,0 +1,154 @@ +// Post-broadcast transaction status views: wait, success, error. + +const { + $, + showView, + showFlash, + addressDotHtml, + escapeHtml, +} = require("./helpers"); +const { state } = require("../../shared/state"); +const { getProvider } = require("../../shared/balances"); +const { log } = require("../../shared/log"); + +const EXT_ICON = + `` + + `` + + `` + + `` + + ``; + +let ctx; +let elapsedTimer = null; +let pollTimer = null; + +function clearTimers() { + if (elapsedTimer) { + clearInterval(elapsedTimer); + elapsedTimer = null; + } + if (pollTimer) { + clearInterval(pollTimer); + pollTimer = null; + } +} + +function toAddressHtml(address) { + const dot = addressDotHtml(address); + const link = `https://etherscan.io/address/${address}`; + const extLink = `${EXT_ICON}`; + return `
${dot}${escapeHtml(address)}${extLink}
`; +} + +function txHashHtml(hash) { + const link = `https://etherscan.io/tx/${hash}`; + const extLink = `${EXT_ICON}`; + return ( + `${escapeHtml(hash)}` + + extLink + ); +} + +function attachCopyHandlers(viewId) { + document + .getElementById(viewId) + .querySelectorAll("[data-copy]") + .forEach((el) => { + el.onclick = () => { + navigator.clipboard.writeText(el.dataset.copy); + showFlash("Copied!"); + }; + }); +} + +function showWait(txInfo, txHash) { + clearTimers(); + + const symbol = txInfo.token === "ETH" ? "ETH" : txInfo.tokenSymbol || "?"; + $("wait-tx-summary").textContent = txInfo.amount + " " + symbol; + $("wait-tx-to").innerHTML = toAddressHtml(txInfo.to); + $("wait-tx-hash").innerHTML = txHashHtml(txHash); + attachCopyHandlers("view-wait-tx"); + + const broadcastTime = Date.now(); + $("wait-tx-status").textContent = "Waiting for confirmation... 0s"; + + elapsedTimer = setInterval(() => { + const elapsed = Math.floor((Date.now() - broadcastTime) / 1000); + $("wait-tx-status").textContent = + "Waiting for confirmation... " + elapsed + "s"; + }, 1000); + + const provider = getProvider(state.rpcUrl); + pollTimer = setInterval(async () => { + try { + const receipt = await provider.getTransactionReceipt(txHash); + if (receipt) { + showSuccess(txInfo, txHash, receipt.blockNumber); + } + } catch (e) { + log.errorf("poll receipt failed:", e.message); + } + + const elapsed = Math.floor((Date.now() - broadcastTime) / 1000); + if (elapsed >= 60) { + showError( + txInfo, + txHash, + "Transaction was not confirmed within 60 seconds. It may still confirm later \u2014 check Etherscan.", + ); + } + }, 10000); + + showView("wait-tx"); +} + +function showSuccess(txInfo, txHash, blockNumber) { + clearTimers(); + + const symbol = txInfo.token === "ETH" ? "ETH" : txInfo.tokenSymbol || "?"; + $("success-tx-summary").textContent = txInfo.amount + " " + symbol; + $("success-tx-to").innerHTML = toAddressHtml(txInfo.to); + $("success-tx-block").textContent = String(blockNumber); + $("success-tx-hash").innerHTML = txHashHtml(txHash); + attachCopyHandlers("view-success-tx"); + + showView("success-tx"); + ctx.doRefreshAndRender(); +} + +function showError(txInfo, txHash, message) { + clearTimers(); + + const symbol = txInfo.token === "ETH" ? "ETH" : txInfo.tokenSymbol || "?"; + $("error-tx-summary").textContent = txInfo.amount + " " + symbol; + $("error-tx-to").innerHTML = toAddressHtml(txInfo.to); + $("error-tx-message").textContent = message; + + if (txHash) { + $("error-tx-hash").innerHTML = txHashHtml(txHash); + $("error-tx-hash-section").classList.remove("hidden"); + attachCopyHandlers("view-error-tx"); + } else { + $("error-tx-hash-section").classList.add("hidden"); + } + + showView("error-tx"); +} + +function navigateBack() { + if (state.selectedToken) { + ctx.showAddressToken(); + } else { + ctx.showAddressDetail(); + } +} + +function init(_ctx) { + ctx = _ctx; + + $("btn-success-tx-done").addEventListener("click", navigateBack); + $("btn-error-tx-done").addEventListener("click", navigateBack); +} + +module.exports = { init, showWait, showError };