Persist navigation state across popup close/reopen
All checks were successful
check / check (push) Successful in 17s

The current view, selected wallet, selected address, and selected
token are now saved to extension storage. When the popup reopens,
it restores to the last visited view instead of always returning
to the home screen.

Restorable views: main, address detail, address-token, receive,
settings. Non-restorable views (send, confirm, tx status, forms)
fall back to the nearest parent. Stored indices are validated
against current wallet data to handle stale references.

Also refactors receive view setup into a centralized receive.show()
function, eliminating duplicate QR/address/warning code from
addressDetail.js, addressToken.js, and home.js. Adds settings.show()
to centralize settings field population.
This commit is contained in:
2026-02-27 12:12:07 +07:00
parent 75cbbea035
commit 034253077c
8 changed files with 137 additions and 72 deletions

View File

@@ -52,8 +52,67 @@ const ctx = {
showAddressToken: () => addressToken.show(), showAddressToken: () => addressToken.show(),
showAddTokenView: () => addToken.show(), showAddTokenView: () => addToken.show(),
showConfirmTx: (txInfo) => confirmTx.show(txInfo), showConfirmTx: (txInfo) => confirmTx.show(txInfo),
showReceive: () => receive.show(),
}; };
// Views that can be fully re-rendered from persisted state.
// All others fall back to the nearest restorable parent.
const RESTORABLE_VIEWS = new Set([
"main",
"address",
"address-token",
"receive",
"settings",
]);
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 (view === "address-token" && !state.selectedToken) {
return fallbackView();
}
switch (view) {
case "address":
addressDetail.show();
break;
case "address-token":
addressToken.show();
break;
case "receive":
receive.show();
break;
case "settings":
settings.show();
break;
default:
fallbackView();
break;
}
}
function fallbackView() {
renderWalletList();
showView("main");
}
async function init() { async function init() {
if (DEBUG) { if (DEBUG) {
const banner = document.createElement("div"); const banner = document.createElement("div");
@@ -98,10 +157,7 @@ async function init() {
showView("main"); showView("main");
return; return;
} }
$("settings-rpc").value = state.rpcUrl; settings.show();
$("settings-blockscout").value = state.blockscoutUrl;
settings.renderSiteLists();
showView("settings");
}); });
welcome.init(ctx); welcome.init(ctx);
@@ -121,7 +177,7 @@ async function init() {
showView("welcome"); showView("welcome");
} else { } else {
renderWalletList(); renderWalletList();
showView("main"); restoreView();
doRefreshAndRender(); doRefreshAndRender();
setInterval(doRefreshAndRender, 10000); setInterval(doRefreshAndRender, 10000);
} }

View File

@@ -17,7 +17,6 @@ const {
const { resolveEnsNames } = require("../../shared/ens"); const { resolveEnsNames } = require("../../shared/ens");
const { updateSendBalance, renderSendTokenSelect } = require("./send"); const { updateSendBalance, renderSendTokenSelect } = require("./send");
const { log } = require("../../shared/log"); const { log } = require("../../shared/log");
const QRCode = require("qrcode");
const makeBlockie = require("ethereum-blockies-base64"); const makeBlockie = require("ethereum-blockies-base64");
let ctx; let ctx;
@@ -316,23 +315,7 @@ function init(_ctx) {
}); });
$("btn-receive").addEventListener("click", () => { $("btn-receive").addEventListener("click", () => {
const addr = currentAddress(); ctx.showReceive();
const address = addr ? addr.address : "";
$("receive-dot").innerHTML = address ? addressDotHtml(address) : "";
$("receive-address").textContent = address;
const link = address ? etherscanAddressLink(address) : "";
$("receive-etherscan-link").innerHTML = link
? `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`
: "";
if (address) {
QRCode.toCanvas($("receive-qr"), address, {
width: 200,
margin: 2,
color: { dark: "#000000", light: "#ffffff" },
});
}
$("receive-erc20-warning").classList.add("hidden");
showView("receive");
}); });
$("btn-add-token").addEventListener("click", ctx.showAddTokenView); $("btn-add-token").addEventListener("click", ctx.showAddTokenView);

View File

@@ -23,7 +23,6 @@ const {
const { resolveEnsNames } = require("../../shared/ens"); const { resolveEnsNames } = require("../../shared/ens");
const { updateSendBalance, renderSendTokenSelect } = require("./send"); const { updateSendBalance, renderSendTokenSelect } = require("./send");
const { log } = require("../../shared/log"); const { log } = require("../../shared/log");
const QRCode = require("qrcode");
const makeBlockie = require("ethereum-blockies-base64"); const makeBlockie = require("ethereum-blockies-base64");
const EXT_ICON = const EXT_ICON =
@@ -366,32 +365,7 @@ function init(ctx) {
}); });
$("btn-address-token-receive").addEventListener("click", () => { $("btn-address-token-receive").addEventListener("click", () => {
const addr = currentAddress(); ctx.showReceive();
const address = addr ? addr.address : "";
$("receive-dot").innerHTML = address ? addressDotHtml(address) : "";
$("receive-address").textContent = address;
const addrLink = address ? etherscanAddressLink(address) : "";
$("receive-etherscan-link").innerHTML = addrLink
? `<a href="${addrLink}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`
: "";
if (address) {
QRCode.toCanvas($("receive-qr"), address, {
width: 200,
margin: 2,
color: { dark: "#000000", light: "#ffffff" },
});
}
const warningEl = $("receive-erc20-warning");
if (state.selectedToken && state.selectedToken !== "ETH") {
warningEl.textContent =
"This is an ERC-20 token. Only send " +
currentSymbol +
" on the Ethereum network to this address. Sending tokens on other networks will result in permanent loss.";
warningEl.classList.remove("hidden");
} else {
warningEl.classList.add("hidden");
}
showView("receive");
}); });
} }

View File

@@ -6,6 +6,7 @@ const {
getPrice, getPrice,
getAddressValueUsd, getAddressValueUsd,
} = require("../../shared/prices"); } = require("../../shared/prices");
const { state, saveState } = require("../../shared/state");
const VIEWS = [ const VIEWS = [
"welcome", "welcome",
@@ -49,6 +50,8 @@ function showView(name) {
} }
} }
clearFlash(); clearFlash();
state.currentView = name;
saveState();
if (DEBUG) { if (DEBUG) {
const banner = document.getElementById("debug-banner"); const banner = document.getElementById("debug-banner");
if (banner) { if (banner) {

View File

@@ -9,7 +9,6 @@ const {
} = require("./helpers"); } = require("./helpers");
const { state, saveState, currentAddress } = require("../../shared/state"); const { state, saveState, currentAddress } = require("../../shared/state");
const { updateSendBalance, renderSendTokenSelect } = require("./send"); const { updateSendBalance, renderSendTokenSelect } = require("./send");
const QRCode = require("qrcode");
const { deriveAddressFromXpub } = require("../../shared/wallet"); const { deriveAddressFromXpub } = require("../../shared/wallet");
const { const {
formatUsd, formatUsd,
@@ -415,23 +414,7 @@ function init(ctx) {
showFlash("No active address selected."); showFlash("No active address selected.");
return; return;
} }
const addr = currentAddress(); ctx.showReceive();
const address = addr ? addr.address : "";
$("receive-dot").innerHTML = address ? addressDotHtml(address) : "";
$("receive-address").textContent = address;
const link = address ? `https://etherscan.io/address/${address}` : "";
$("receive-etherscan-link").innerHTML = link
? `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`
: "";
if (address) {
QRCode.toCanvas($("receive-qr"), address, {
width: 200,
margin: 2,
color: { dark: "#000000", light: "#ffffff" },
});
}
$("receive-erc20-warning").classList.add("hidden");
showView("receive");
}); });
} }

View File

@@ -1,5 +1,53 @@
const { $, showFlash } = require("./helpers"); const { $, showView, showFlash, addressDotHtml } = require("./helpers");
const { state } = require("../../shared/state"); const { state, currentAddress } = require("../../shared/state");
const QRCode = require("qrcode");
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>`;
function show() {
const addr = currentAddress();
const address = addr ? addr.address : "";
$("receive-dot").innerHTML = address ? addressDotHtml(address) : "";
$("receive-address").textContent = address;
const link = address ? `https://etherscan.io/address/${address}` : "";
$("receive-etherscan-link").innerHTML = link
? `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`
: "";
if (address) {
QRCode.toCanvas($("receive-qr"), address, {
width: 200,
margin: 2,
color: { dark: "#000000", light: "#ffffff" },
});
}
const warningEl = $("receive-erc20-warning");
if (state.selectedToken && state.selectedToken !== "ETH") {
// Look up symbol from address token balances
const addrObj = currentAddress();
let symbol = state.selectedToken;
if (addrObj) {
const tb = (addrObj.tokenBalances || []).find(
(t) =>
t.address.toLowerCase() ===
state.selectedToken.toLowerCase(),
);
if (tb) symbol = tb.symbol;
}
warningEl.textContent =
"This is an ERC-20 token. Only send " +
symbol +
" on the Ethereum network to this address. Sending tokens on other networks will result in permanent loss.";
warningEl.classList.remove("hidden");
} else {
warningEl.classList.add("hidden");
}
showView("receive");
}
function init(ctx) { function init(ctx) {
$("btn-receive-copy").addEventListener("click", () => { $("btn-receive-copy").addEventListener("click", () => {
@@ -19,4 +67,4 @@ function init(ctx) {
}); });
} }
module.exports = { init }; module.exports = { init, show };

View File

@@ -38,6 +38,13 @@ function renderSiteList(containerId, siteMap, stateKey) {
}); });
} }
function show() {
$("settings-rpc").value = state.rpcUrl;
$("settings-blockscout").value = state.blockscoutUrl;
renderSiteLists();
showView("settings");
}
function renderSiteLists() { function renderSiteLists() {
renderSiteList( renderSiteList(
"settings-allowed-sites", "settings-allowed-sites",
@@ -154,4 +161,4 @@ function init(ctx) {
}); });
} }
module.exports = { init, renderSiteLists }; module.exports = { init, show, renderSiteLists };

View File

@@ -29,6 +29,7 @@ const DEFAULT_STATE = {
const state = { const state = {
...DEFAULT_STATE, ...DEFAULT_STATE,
currentView: null,
selectedWallet: null, selectedWallet: null,
selectedAddress: null, selectedAddress: null,
selectedToken: null, selectedToken: null,
@@ -53,6 +54,10 @@ async function saveState() {
dustThresholdGwei: state.dustThresholdGwei, dustThresholdGwei: state.dustThresholdGwei,
fraudContracts: state.fraudContracts, fraudContracts: state.fraudContracts,
tokenHolderCache: state.tokenHolderCache, tokenHolderCache: state.tokenHolderCache,
currentView: state.currentView,
selectedWallet: state.selectedWallet,
selectedAddress: state.selectedAddress,
selectedToken: state.selectedToken,
}; };
await storageApi.set({ autistmask: persisted }); await storageApi.set({ autistmask: persisted });
} }
@@ -103,6 +108,12 @@ async function loadState() {
: 100000; : 100000;
state.fraudContracts = saved.fraudContracts || []; state.fraudContracts = saved.fraudContracts || [];
state.tokenHolderCache = saved.tokenHolderCache || {}; state.tokenHolderCache = saved.tokenHolderCache || {};
state.currentView = saved.currentView || null;
state.selectedWallet =
saved.selectedWallet !== undefined ? saved.selectedWallet : null;
state.selectedAddress =
saved.selectedAddress !== undefined ? saved.selectedAddress : null;
state.selectedToken = saved.selectedToken || null;
} }
} }