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.
This commit is contained in:
@@ -494,10 +494,80 @@
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
<div
|
||||
id="confirm-status"
|
||||
class="mt-2 border border-border p-1 hidden"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<!-- ============ WAIT FOR TX CONFIRMATION ============ -->
|
||||
<div id="view-wait-tx" class="view hidden">
|
||||
<h2 class="font-bold mb-2">Transaction Broadcast</h2>
|
||||
<div class="mb-3">
|
||||
<div class="text-xs text-muted mb-1">Amount</div>
|
||||
<div id="wait-tx-summary" class="font-bold"></div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="text-xs text-muted mb-1">To</div>
|
||||
<div id="wait-tx-to" class="text-xs break-all"></div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="text-xs text-muted mb-1">Transaction hash</div>
|
||||
<div id="wait-tx-hash" class="text-xs break-all"></div>
|
||||
</div>
|
||||
<div id="wait-tx-status" class="text-xs text-muted"></div>
|
||||
</div>
|
||||
|
||||
<!-- ============ TX SUCCESS ============ -->
|
||||
<div id="view-success-tx" class="view hidden">
|
||||
<h2 class="font-bold mb-2">Transaction Confirmed</h2>
|
||||
<div class="mb-3">
|
||||
<div class="text-xs text-muted mb-1">Amount</div>
|
||||
<div id="success-tx-summary" class="font-bold"></div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="text-xs text-muted mb-1">To</div>
|
||||
<div id="success-tx-to" class="text-xs break-all"></div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="text-xs text-muted mb-1">Block</div>
|
||||
<div id="success-tx-block" class="text-xs"></div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="text-xs text-muted mb-1">Transaction hash</div>
|
||||
<div id="success-tx-hash" class="text-xs break-all"></div>
|
||||
</div>
|
||||
<button
|
||||
id="btn-success-tx-done"
|
||||
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
|
||||
>
|
||||
Done
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- ============ TX ERROR ============ -->
|
||||
<div id="view-error-tx" class="view hidden">
|
||||
<h2 class="font-bold mb-2">Transaction Failed</h2>
|
||||
<div class="mb-3">
|
||||
<div class="text-xs text-muted mb-1">Amount</div>
|
||||
<div id="error-tx-summary" class="font-bold"></div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="text-xs text-muted mb-1">To</div>
|
||||
<div id="error-tx-to" class="text-xs break-all"></div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div
|
||||
id="error-tx-message"
|
||||
class="text-xs border border-border border-dashed p-2"
|
||||
></div>
|
||||
</div>
|
||||
<div id="error-tx-hash-section" class="mb-3 hidden">
|
||||
<div class="text-xs text-muted mb-1">Transaction hash</div>
|
||||
<div id="error-tx-hash" class="text-xs break-all"></div>
|
||||
</div>
|
||||
<button
|
||||
id="btn-error-tx-done"
|
||||
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
|
||||
>
|
||||
Done
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- ============ PASSWORD MODAL ============ -->
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 =
|
||||
`<span style="display:inline-block;width:10px;height:10px;margin-left:4px;vertical-align:middle">` +
|
||||
@@ -36,7 +37,6 @@ const EXT_ICON =
|
||||
`</svg></span>`;
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -16,6 +16,9 @@ const VIEWS = [
|
||||
"address-token",
|
||||
"send",
|
||||
"confirm-tx",
|
||||
"wait-tx",
|
||||
"success-tx",
|
||||
"error-tx",
|
||||
"receive",
|
||||
"add-token",
|
||||
"settings",
|
||||
|
||||
154
src/popup/views/txStatus.js
Normal file
154
src/popup/views/txStatus.js
Normal file
@@ -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 =
|
||||
`<span style="display:inline-block;width:10px;height:10px;margin-left:4px;vertical-align:middle">` +
|
||||
`<svg viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5">` +
|
||||
`<path d="M4.5 1.5H2a.5.5 0 00-.5.5v8a.5.5 0 00.5.5h8a.5.5 0 00.5-.5V7.5"/>` +
|
||||
`<path d="M7 1.5h3.5V5M7 5.5L10.5 1.5"/>` +
|
||||
`</svg></span>`;
|
||||
|
||||
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 = `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;
|
||||
return `<div class="flex items-center">${dot}<span class="break-all">${escapeHtml(address)}</span>${extLink}</div>`;
|
||||
}
|
||||
|
||||
function txHashHtml(hash) {
|
||||
const link = `https://etherscan.io/tx/${hash}`;
|
||||
const extLink = `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;
|
||||
return (
|
||||
`<span class="underline decoration-dashed cursor-pointer break-all" data-copy="${escapeHtml(hash)}">${escapeHtml(hash)}</span>` +
|
||||
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 };
|
||||
Reference in New Issue
Block a user