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
|
Send
|
||||||
</button>
|
</button>
|
||||||
<div
|
</div>
|
||||||
id="confirm-status"
|
|
||||||
class="mt-2 border border-border p-1 hidden"
|
<!-- ============ WAIT FOR TX CONFIRMATION ============ -->
|
||||||
></div>
|
<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>
|
</div>
|
||||||
|
|
||||||
<!-- ============ PASSWORD MODAL ============ -->
|
<!-- ============ PASSWORD MODAL ============ -->
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ const addressDetail = require("./views/addressDetail");
|
|||||||
const addressToken = require("./views/addressToken");
|
const addressToken = require("./views/addressToken");
|
||||||
const send = require("./views/send");
|
const send = require("./views/send");
|
||||||
const confirmTx = require("./views/confirmTx");
|
const confirmTx = require("./views/confirmTx");
|
||||||
|
const txStatus = require("./views/txStatus");
|
||||||
const receive = require("./views/receive");
|
const receive = require("./views/receive");
|
||||||
const addToken = require("./views/addToken");
|
const addToken = require("./views/addToken");
|
||||||
const settings = require("./views/settings");
|
const settings = require("./views/settings");
|
||||||
@@ -111,6 +112,7 @@ async function init() {
|
|||||||
addressToken.init(ctx);
|
addressToken.init(ctx);
|
||||||
send.init(ctx);
|
send.init(ctx);
|
||||||
confirmTx.init(ctx);
|
confirmTx.init(ctx);
|
||||||
|
txStatus.init(ctx);
|
||||||
receive.init(ctx);
|
receive.init(ctx);
|
||||||
addToken.init(ctx);
|
addToken.init(ctx);
|
||||||
settings.init(ctx);
|
settings.init(ctx);
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ const { isScamAddress } = require("../../shared/scamlist");
|
|||||||
const { ERC20_ABI } = require("../../shared/constants");
|
const { ERC20_ABI } = require("../../shared/constants");
|
||||||
const { log } = require("../../shared/log");
|
const { log } = require("../../shared/log");
|
||||||
const makeBlockie = require("ethereum-blockies-base64");
|
const makeBlockie = require("ethereum-blockies-base64");
|
||||||
|
const txStatus = require("./txStatus");
|
||||||
|
|
||||||
const EXT_ICON =
|
const EXT_ICON =
|
||||||
`<span style="display:inline-block;width:10px;height:10px;margin-left:4px;vertical-align:middle">` +
|
`<span style="display:inline-block;width:10px;height:10px;margin-left:4px;vertical-align:middle">` +
|
||||||
@@ -36,7 +37,6 @@ const EXT_ICON =
|
|||||||
`</svg></span>`;
|
`</svg></span>`;
|
||||||
|
|
||||||
let pendingTx = null;
|
let pendingTx = null;
|
||||||
let elapsedTimer = null;
|
|
||||||
|
|
||||||
function etherscanTokenLink(address) {
|
function etherscanTokenLink(address) {
|
||||||
return `https://etherscan.io/token/${address}`;
|
return `https://etherscan.io/token/${address}`;
|
||||||
@@ -217,7 +217,6 @@ function show(txInfo) {
|
|||||||
// Gas estimate — show placeholder then fetch async
|
// Gas estimate — show placeholder then fetch async
|
||||||
$("confirm-fee").classList.remove("hidden");
|
$("confirm-fee").classList.remove("hidden");
|
||||||
$("confirm-fee-amount").textContent = "Estimating...";
|
$("confirm-fee-amount").textContent = "Estimating...";
|
||||||
$("confirm-status").classList.add("hidden");
|
|
||||||
showView("confirm-tx");
|
showView("confirm-tx");
|
||||||
|
|
||||||
estimateGas(txInfo);
|
estimateGas(txInfo);
|
||||||
@@ -309,10 +308,7 @@ function init(ctx) {
|
|||||||
|
|
||||||
hidePasswordModal();
|
hidePasswordModal();
|
||||||
|
|
||||||
const statusEl = $("confirm-status");
|
let tx;
|
||||||
statusEl.textContent = "Sending...";
|
|
||||||
statusEl.classList.remove("hidden");
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const signer = getSignerForAddress(
|
const signer = getSignerForAddress(
|
||||||
wallet,
|
wallet,
|
||||||
@@ -322,7 +318,6 @@ function init(ctx) {
|
|||||||
const provider = getProvider(state.rpcUrl);
|
const provider = getProvider(state.rpcUrl);
|
||||||
const connectedSigner = signer.connect(provider);
|
const connectedSigner = signer.connect(provider);
|
||||||
|
|
||||||
let tx;
|
|
||||||
if (pendingTx.token === "ETH") {
|
if (pendingTx.token === "ETH") {
|
||||||
tx = await connectedSigner.sendTransaction({
|
tx = await connectedSigner.sendTransaction({
|
||||||
to: pendingTx.to,
|
to: pendingTx.to,
|
||||||
@@ -339,62 +334,10 @@ function init(ctx) {
|
|||||||
tx = await contract.transfer(pendingTx.to, amount);
|
tx = await contract.transfer(pendingTx.to, amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disable send button immediately after broadcast
|
txStatus.showWait(pendingTx, tx.hash);
|
||||||
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();
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (elapsedTimer) {
|
const hash = tx ? tx.hash : null;
|
||||||
clearInterval(elapsedTimer);
|
txStatus.showError(pendingTx, hash, e.shortMessage || e.message);
|
||||||
elapsedTimer = null;
|
|
||||||
}
|
|
||||||
statusEl.textContent = "Failed: " + (e.shortMessage || e.message);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ const VIEWS = [
|
|||||||
"address-token",
|
"address-token",
|
||||||
"send",
|
"send",
|
||||||
"confirm-tx",
|
"confirm-tx",
|
||||||
|
"wait-tx",
|
||||||
|
"success-tx",
|
||||||
|
"error-tx",
|
||||||
"receive",
|
"receive",
|
||||||
"add-token",
|
"add-token",
|
||||||
"settings",
|
"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