Centralize view state into app ctx with viewData persistence
Všechny kontroly byly úspěšné
check / check (push) Successful in 17s

Creates a centralized transactionDetail.js view module, replacing
the duplicated showTxDetail/copyableHtml/blockieHtml/txDetailAddressHtml
code that was in both addressDetail.js and addressToken.js (~120 lines
removed). Transaction data is stored in state.viewData and persisted,
so the transaction detail view survives popup close/reopen.

Adds viewData to persisted state. Each view that needs data for
restore stores it in state.viewData before rendering. The ctx object
now has showTransactionDetail() alongside all other show methods.

Restorable views expanded to include: transaction (via viewData.tx),
success-tx (via viewData.hash/blockNumber), error-tx (via
viewData.message). txStatus.js split into show (sets data) + render
(reads data) for each screen, enabling restore.

Non-restorable views (send, confirm-tx, wait-tx, add-wallet,
import-key, add-token) fall back to the nearest parent since they
involve active form state or network polling.
Tento commit je obsažen v:
2026-02-27 12:16:33 +07:00
rodič 034253077c
revize 2467dfd09c
6 změnil soubory, kde provedl 254 přidání a 178 odebrání

Zobrazit soubor

@@ -16,6 +16,7 @@ const addressToken = require("./views/addressToken");
const send = require("./views/send");
const confirmTx = require("./views/confirmTx");
const txStatus = require("./views/txStatus");
const transactionDetail = require("./views/transactionDetail");
const receive = require("./views/receive");
const addToken = require("./views/addToken");
const settings = require("./views/settings");
@@ -53,6 +54,7 @@ const ctx = {
showAddTokenView: () => addToken.show(),
showConfirmTx: (txInfo) => confirmTx.show(txInfo),
showReceive: () => receive.show(),
showTransactionDetail: (tx) => transactionDetail.show(tx),
};
// Views that can be fully re-rendered from persisted state.
@@ -63,26 +65,37 @@ const RESTORABLE_VIEWS = new Set([
"address-token",
"receive",
"settings",
"transaction",
"success-tx",
"error-tx",
]);
function needsAddress(view) {
return (
view === "address" ||
view === "address-token" ||
view === "receive" ||
view === "transaction"
);
}
function hasValidAddress() {
return (
state.selectedWallet !== null &&
state.selectedAddress !== null &&
state.wallets[state.selectedWallet] &&
state.wallets[state.selectedWallet].addresses[state.selectedAddress]
);
}
function restoreView() {
const view = state.currentView;
if (!view || !RESTORABLE_VIEWS.has(view)) {
return fallbackView();
}
// Validate that selectedWallet/selectedAddress still point to valid data
if (view === "address" || view === "address-token" || view === "receive") {
if (
state.selectedWallet === null ||
state.selectedAddress === null ||
!state.wallets[state.selectedWallet] ||
!state.wallets[state.selectedWallet].addresses[
state.selectedAddress
]
) {
return fallbackView();
}
if (needsAddress(view) && !hasValidAddress()) {
return fallbackView();
}
if (view === "address-token" && !state.selectedToken) {
@@ -102,6 +115,27 @@ function restoreView() {
case "settings":
settings.show();
break;
case "transaction":
if (state.viewData && state.viewData.tx) {
transactionDetail.render();
} else {
fallbackView();
}
break;
case "success-tx":
if (state.viewData && state.viewData.hash) {
txStatus.renderSuccess();
} else {
fallbackView();
}
break;
case "error-tx":
if (state.viewData && state.viewData.message) {
txStatus.renderError();
} else {
fallbackView();
}
break;
default:
fallbackView();
break;
@@ -169,6 +203,7 @@ async function init() {
send.init(ctx);
confirmTx.init(ctx);
txStatus.init(ctx);
transactionDetail.init(ctx);
receive.init(ctx);
addToken.init(ctx);
settings.init(ctx);

Zobrazit soubor

@@ -5,7 +5,6 @@ const {
balanceLinesForAddress,
addressDotHtml,
escapeHtml,
formatAddressHtml,
truncateMiddle,
} = require("./helpers");
const { state, currentAddress, saveState } = require("../../shared/state");
@@ -32,10 +31,6 @@ function etherscanAddressLink(address) {
return `https://etherscan.io/address/${address}`;
}
function etherscanTxLink(hash) {
return `https://etherscan.io/tx/${hash}`;
}
function show() {
state.selectedToken = null;
const wallet = state.wallets[state.selectedWallet];
@@ -211,77 +206,15 @@ function renderTransactions(txs) {
list.querySelectorAll(".tx-row").forEach((row) => {
row.addEventListener("click", () => {
const idx = parseInt(row.dataset.tx, 10);
showTxDetail(loadedTxs[idx]);
const tx = loadedTxs[idx];
const counterparty = tx.direction === "sent" ? tx.to : tx.from;
tx.fromEns = ensNameMap.get(tx.from) || null;
tx.toEns = ensNameMap.get(tx.to) || null;
ctx.showTransactionDetail(tx);
});
});
}
function copyableHtml(text, extraClass) {
const cls =
"underline decoration-dashed cursor-pointer" +
(extraClass ? " " + extraClass : "");
return `<span class="${cls}" data-copy="${escapeHtml(text)}">${escapeHtml(text)}</span>`;
}
function blockieHtml(address) {
const src = makeBlockie(address);
return `<img src="${src}" width="48" height="48" style="image-rendering:pixelated;border-radius:50%;display:inline-block">`;
}
function txDetailAddressHtml(address) {
const ensName = ensNameMap.get(address) || null;
const blockie = blockieHtml(address);
const dot = addressDotHtml(address);
const link = etherscanAddressLink(address);
const extLink = `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;
let html = `<div class="mb-1">${blockie}</div>`;
if (ensName) {
html +=
`<div class="flex items-center">${dot}` +
copyableHtml(ensName, "") +
extLink +
`</div>` +
`<div class="break-all">` +
copyableHtml(address, "break-all") +
`</div>`;
} else {
html +=
`<div class="flex items-center">${dot}` +
copyableHtml(address, "break-all") +
extLink +
`</div>`;
}
return html;
}
function txDetailHashHtml(hash) {
const link = etherscanTxLink(hash);
const extLink = `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;
return copyableHtml(hash, "break-all") + extLink;
}
function showTxDetail(tx) {
$("tx-detail-hash").innerHTML = txDetailHashHtml(tx.hash);
$("tx-detail-from").innerHTML = txDetailAddressHtml(tx.from);
$("tx-detail-to").innerHTML = txDetailAddressHtml(tx.to);
$("tx-detail-value").textContent = tx.value + " " + tx.symbol;
$("tx-detail-time").textContent =
isoDate(tx.timestamp) + " (" + timeAgo(tx.timestamp) + ")";
$("tx-detail-status").textContent = tx.isError ? "Failed" : "Success";
showView("transaction");
// Attach copy handlers
document
.getElementById("view-transaction")
.querySelectorAll("[data-copy]")
.forEach((el) => {
el.onclick = () => {
navigator.clipboard.writeText(el.dataset.copy);
showFlash("Copied!");
};
});
}
function init(_ctx) {
ctx = _ctx;
$("address-full").addEventListener("click", () => {
@@ -319,14 +252,6 @@ function init(_ctx) {
});
$("btn-add-token").addEventListener("click", ctx.showAddTokenView);
$("btn-tx-back").addEventListener("click", () => {
if (state.selectedToken) {
ctx.showAddressToken();
} else {
show();
}
});
}
module.exports = { init, show };

Zobrazit soubor

@@ -25,6 +25,8 @@ const { updateSendBalance, renderSendTokenSelect } = require("./send");
const { log } = require("../../shared/log");
const makeBlockie = require("ethereum-blockies-base64");
let ctx;
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">` +
@@ -36,10 +38,6 @@ function etherscanAddressLink(address) {
return `https://etherscan.io/address/${address}`;
}
function etherscanTxLink(hash) {
return `https://etherscan.io/tx/${hash}`;
}
function isoDate(timestamp) {
const d = new Date(timestamp * 1000);
const pad = (n) => String(n).padStart(2, "0");
@@ -233,78 +231,16 @@ function renderTransactions(txs) {
list.querySelectorAll(".tx-row").forEach((row) => {
row.addEventListener("click", () => {
const idx = parseInt(row.dataset.tx, 10);
showTxDetail(loadedTxs[idx]);
const tx = loadedTxs[idx];
tx.fromEns = ensNameMap.get(tx.from) || null;
tx.toEns = ensNameMap.get(tx.to) || null;
ctx.showTransactionDetail(tx);
});
});
}
function copyableHtml(text, extraClass) {
const cls =
"underline decoration-dashed cursor-pointer" +
(extraClass ? " " + extraClass : "");
return `<span class="${cls}" data-copy="${escapeHtml(text)}">${escapeHtml(text)}</span>`;
}
function blockieHtml(address) {
const src = makeBlockie(address);
return `<img src="${src}" width="48" height="48" style="image-rendering:pixelated;border-radius:50%;display:inline-block">`;
}
function txDetailAddressHtml(address) {
const ensName = ensNameMap.get(address) || null;
const blockie = blockieHtml(address);
const dot = addressDotHtml(address);
const link = etherscanAddressLink(address);
const extLink = `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;
let html = `<div class="mb-1">${blockie}</div>`;
if (ensName) {
html +=
`<div class="flex items-center">${dot}` +
copyableHtml(ensName, "") +
extLink +
`</div>` +
`<div class="break-all">` +
copyableHtml(address, "break-all") +
`</div>`;
} else {
html +=
`<div class="flex items-center">${dot}` +
copyableHtml(address, "break-all") +
extLink +
`</div>`;
}
return html;
}
function txDetailHashHtml(hash) {
const link = etherscanTxLink(hash);
const extLink = `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;
return copyableHtml(hash, "break-all") + extLink;
}
function showTxDetail(tx) {
$("tx-detail-hash").innerHTML = txDetailHashHtml(tx.hash);
$("tx-detail-from").innerHTML = txDetailAddressHtml(tx.from);
$("tx-detail-to").innerHTML = txDetailAddressHtml(tx.to);
$("tx-detail-value").textContent = tx.value + " " + tx.symbol;
$("tx-detail-time").textContent =
isoDate(tx.timestamp) + " (" + timeAgo(tx.timestamp) + ")";
$("tx-detail-status").textContent = tx.isError ? "Failed" : "Success";
showView("transaction");
// Attach copy handlers
document
.getElementById("view-transaction")
.querySelectorAll("[data-copy]")
.forEach((el) => {
el.onclick = () => {
navigator.clipboard.writeText(el.dataset.copy);
showFlash("Copied!");
};
});
}
function init(ctx) {
function init(_ctx) {
ctx = _ctx;
$("address-token-full").addEventListener("click", () => {
const addr = $("address-token-full").dataset.full;
if (addr) {

152
src/popup/views/transactionDetail.js Normální soubor
Zobrazit soubor

@@ -0,0 +1,152 @@
// Transaction detail view — shows full details for a single transaction.
// Shared by addressDetail and addressToken via ctx.showTransactionDetail().
const {
$,
showView,
showFlash,
addressDotHtml,
escapeHtml,
} = require("./helpers");
const { state } = require("../../shared/state");
const makeBlockie = require("ethereum-blockies-base64");
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;
function isoDate(timestamp) {
const d = new Date(timestamp * 1000);
const pad = (n) => String(n).padStart(2, "0");
return (
d.getFullYear() +
"-" +
pad(d.getMonth() + 1) +
"-" +
pad(d.getDate()) +
" " +
pad(d.getHours()) +
":" +
pad(d.getMinutes()) +
":" +
pad(d.getSeconds())
);
}
function timeAgo(timestamp) {
const seconds = Math.floor(Date.now() / 1000 - timestamp);
if (seconds < 60) return seconds + " seconds ago";
const minutes = Math.floor(seconds / 60);
if (minutes < 60)
return minutes + " minute" + (minutes !== 1 ? "s" : "") + " ago";
const hours = Math.floor(minutes / 60);
if (hours < 24) return hours + " hour" + (hours !== 1 ? "s" : "") + " ago";
const days = Math.floor(hours / 24);
if (days < 30) return days + " day" + (days !== 1 ? "s" : "") + " ago";
const months = Math.floor(days / 30);
if (months < 12)
return months + " month" + (months !== 1 ? "s" : "") + " ago";
const years = Math.floor(days / 365);
return years + " year" + (years !== 1 ? "s" : "") + " ago";
}
function copyableHtml(text, extraClass) {
const cls =
"underline decoration-dashed cursor-pointer" +
(extraClass ? " " + extraClass : "");
return `<span class="${cls}" data-copy="${escapeHtml(text)}">${escapeHtml(text)}</span>`;
}
function blockieHtml(address) {
const src = makeBlockie(address);
return `<img src="${src}" width="48" height="48" style="image-rendering:pixelated;border-radius:50%;display:inline-block">`;
}
function txAddressHtml(address, ensName) {
const blockie = blockieHtml(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>`;
let html = `<div class="mb-1">${blockie}</div>`;
if (ensName) {
html +=
`<div class="flex items-center">${dot}` +
copyableHtml(ensName, "") +
extLink +
`</div>` +
`<div class="break-all">` +
copyableHtml(address, "break-all") +
`</div>`;
} else {
html +=
`<div class="flex items-center">${dot}` +
copyableHtml(address, "break-all") +
extLink +
`</div>`;
}
return html;
}
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 copyableHtml(hash, "break-all") + extLink;
}
function show(tx) {
state.viewData = {
tx: {
hash: tx.hash,
from: tx.from,
to: tx.to,
value: tx.value,
symbol: tx.symbol,
timestamp: tx.timestamp,
isError: tx.isError,
fromEns: tx.fromEns || null,
toEns: tx.toEns || null,
},
};
render();
}
function render() {
const tx = state.viewData.tx;
if (!tx) return;
$("tx-detail-hash").innerHTML = txHashHtml(tx.hash);
$("tx-detail-from").innerHTML = txAddressHtml(tx.from, tx.fromEns);
$("tx-detail-to").innerHTML = txAddressHtml(tx.to, tx.toEns);
$("tx-detail-value").textContent = tx.value + " " + tx.symbol;
$("tx-detail-time").textContent =
isoDate(tx.timestamp) + " (" + timeAgo(tx.timestamp) + ")";
$("tx-detail-status").textContent = tx.isError ? "Failed" : "Success";
showView("transaction");
document
.getElementById("view-transaction")
.querySelectorAll("[data-copy]")
.forEach((el) => {
el.onclick = () => {
navigator.clipboard.writeText(el.dataset.copy);
showFlash("Copied!");
};
});
}
function init(_ctx) {
ctx = _ctx;
$("btn-tx-back").addEventListener("click", () => {
if (state.selectedToken) {
ctx.showAddressToken();
} else {
ctx.showAddressDetail();
}
});
}
module.exports = { init, show, render };

Zobrazit soubor

@@ -7,7 +7,7 @@ const {
addressDotHtml,
escapeHtml,
} = require("./helpers");
const { state } = require("../../shared/state");
const { state, saveState } = require("../../shared/state");
const { getProvider } = require("../../shared/balances");
const { log } = require("../../shared/log");
@@ -107,26 +107,51 @@ 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");
state.viewData = {
amount: txInfo.amount,
symbol: symbol,
to: txInfo.to,
hash: txHash,
blockNumber: blockNumber,
};
renderSuccess();
ctx.doRefreshAndRender();
}
function renderSuccess() {
const d = state.viewData;
if (!d || !d.hash) return;
$("success-tx-summary").textContent = d.amount + " " + d.symbol;
$("success-tx-to").innerHTML = toAddressHtml(d.to);
$("success-tx-block").textContent = String(d.blockNumber);
$("success-tx-hash").innerHTML = txHashHtml(d.hash);
attachCopyHandlers("view-success-tx");
showView("success-tx");
}
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;
state.viewData = {
amount: txInfo.amount,
symbol: symbol,
to: txInfo.to,
hash: txHash || null,
message: message,
};
renderError();
}
if (txHash) {
$("error-tx-hash").innerHTML = txHashHtml(txHash);
function renderError() {
const d = state.viewData;
if (!d || !d.message) return;
$("error-tx-summary").textContent = d.amount + " " + d.symbol;
$("error-tx-to").innerHTML = toAddressHtml(d.to);
$("error-tx-message").textContent = d.message;
if (d.hash) {
$("error-tx-hash").innerHTML = txHashHtml(d.hash);
$("error-tx-hash-section").classList.remove("hidden");
attachCopyHandlers("view-error-tx");
} else {
@@ -151,4 +176,4 @@ function init(_ctx) {
$("btn-error-tx-done").addEventListener("click", navigateBack);
}
module.exports = { init, showWait, showError };
module.exports = { init, showWait, showError, renderSuccess, renderError };

Zobrazit soubor

@@ -33,6 +33,7 @@ const state = {
selectedWallet: null,
selectedAddress: null,
selectedToken: null,
viewData: {},
};
async function saveState() {
@@ -58,6 +59,7 @@ async function saveState() {
selectedWallet: state.selectedWallet,
selectedAddress: state.selectedAddress,
selectedToken: state.selectedToken,
viewData: state.viewData,
};
await storageApi.set({ autistmask: persisted });
}
@@ -114,6 +116,7 @@ async function loadState() {
state.selectedAddress =
saved.selectedAddress !== undefined ? saved.selectedAddress : null;
state.selectedToken = saved.selectedToken || null;
state.viewData = saved.viewData || {};
}
}