From 034253077c5e74463dd710adf64d7a3f7d125cd8 Mon Sep 17 00:00:00 2001 From: sneak Date: Fri, 27 Feb 2026 12:12:07 +0700 Subject: [PATCH] Persist navigation state across popup close/reopen 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. --- src/popup/index.js | 66 +++++++++++++++++++++++++++++--- src/popup/views/addressDetail.js | 19 +-------- src/popup/views/addressToken.js | 28 +------------- src/popup/views/helpers.js | 3 ++ src/popup/views/home.js | 19 +-------- src/popup/views/receive.js | 54 ++++++++++++++++++++++++-- src/popup/views/settings.js | 9 ++++- src/shared/state.js | 11 ++++++ 8 files changed, 137 insertions(+), 72 deletions(-) diff --git a/src/popup/index.js b/src/popup/index.js index b28bc23..c2e9069 100644 --- a/src/popup/index.js +++ b/src/popup/index.js @@ -52,8 +52,67 @@ const ctx = { showAddressToken: () => addressToken.show(), showAddTokenView: () => addToken.show(), 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() { if (DEBUG) { const banner = document.createElement("div"); @@ -98,10 +157,7 @@ async function init() { showView("main"); return; } - $("settings-rpc").value = state.rpcUrl; - $("settings-blockscout").value = state.blockscoutUrl; - settings.renderSiteLists(); - showView("settings"); + settings.show(); }); welcome.init(ctx); @@ -121,7 +177,7 @@ async function init() { showView("welcome"); } else { renderWalletList(); - showView("main"); + restoreView(); doRefreshAndRender(); setInterval(doRefreshAndRender, 10000); } diff --git a/src/popup/views/addressDetail.js b/src/popup/views/addressDetail.js index dfffec8..b3de59f 100644 --- a/src/popup/views/addressDetail.js +++ b/src/popup/views/addressDetail.js @@ -17,7 +17,6 @@ const { const { resolveEnsNames } = require("../../shared/ens"); const { updateSendBalance, renderSendTokenSelect } = require("./send"); const { log } = require("../../shared/log"); -const QRCode = require("qrcode"); const makeBlockie = require("ethereum-blockies-base64"); let ctx; @@ -316,23 +315,7 @@ function init(_ctx) { }); $("btn-receive").addEventListener("click", () => { - const addr = currentAddress(); - 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 - ? `${EXT_ICON}` - : ""; - if (address) { - QRCode.toCanvas($("receive-qr"), address, { - width: 200, - margin: 2, - color: { dark: "#000000", light: "#ffffff" }, - }); - } - $("receive-erc20-warning").classList.add("hidden"); - showView("receive"); + ctx.showReceive(); }); $("btn-add-token").addEventListener("click", ctx.showAddTokenView); diff --git a/src/popup/views/addressToken.js b/src/popup/views/addressToken.js index cba0ee3..855bb88 100644 --- a/src/popup/views/addressToken.js +++ b/src/popup/views/addressToken.js @@ -23,7 +23,6 @@ const { const { resolveEnsNames } = require("../../shared/ens"); const { updateSendBalance, renderSendTokenSelect } = require("./send"); const { log } = require("../../shared/log"); -const QRCode = require("qrcode"); const makeBlockie = require("ethereum-blockies-base64"); const EXT_ICON = @@ -366,32 +365,7 @@ function init(ctx) { }); $("btn-address-token-receive").addEventListener("click", () => { - const addr = currentAddress(); - 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 - ? `${EXT_ICON}` - : ""; - 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"); + ctx.showReceive(); }); } diff --git a/src/popup/views/helpers.js b/src/popup/views/helpers.js index 0d37291..82f92d4 100644 --- a/src/popup/views/helpers.js +++ b/src/popup/views/helpers.js @@ -6,6 +6,7 @@ const { getPrice, getAddressValueUsd, } = require("../../shared/prices"); +const { state, saveState } = require("../../shared/state"); const VIEWS = [ "welcome", @@ -49,6 +50,8 @@ function showView(name) { } } clearFlash(); + state.currentView = name; + saveState(); if (DEBUG) { const banner = document.getElementById("debug-banner"); if (banner) { diff --git a/src/popup/views/home.js b/src/popup/views/home.js index a8267eb..13aa6f6 100644 --- a/src/popup/views/home.js +++ b/src/popup/views/home.js @@ -9,7 +9,6 @@ const { } = require("./helpers"); const { state, saveState, currentAddress } = require("../../shared/state"); const { updateSendBalance, renderSendTokenSelect } = require("./send"); -const QRCode = require("qrcode"); const { deriveAddressFromXpub } = require("../../shared/wallet"); const { formatUsd, @@ -415,23 +414,7 @@ function init(ctx) { showFlash("No active address selected."); return; } - 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 - ? `${EXT_ICON}` - : ""; - if (address) { - QRCode.toCanvas($("receive-qr"), address, { - width: 200, - margin: 2, - color: { dark: "#000000", light: "#ffffff" }, - }); - } - $("receive-erc20-warning").classList.add("hidden"); - showView("receive"); + ctx.showReceive(); }); } diff --git a/src/popup/views/receive.js b/src/popup/views/receive.js index 40d5b8b..c986fef 100644 --- a/src/popup/views/receive.js +++ b/src/popup/views/receive.js @@ -1,5 +1,53 @@ -const { $, showFlash } = require("./helpers"); -const { state } = require("../../shared/state"); +const { $, showView, showFlash, addressDotHtml } = require("./helpers"); +const { state, currentAddress } = require("../../shared/state"); +const QRCode = require("qrcode"); + +const EXT_ICON = + `` + + `` + + `` + + `` + + ``; + +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 + ? `${EXT_ICON}` + : ""; + 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) { $("btn-receive-copy").addEventListener("click", () => { @@ -19,4 +67,4 @@ function init(ctx) { }); } -module.exports = { init }; +module.exports = { init, show }; diff --git a/src/popup/views/settings.js b/src/popup/views/settings.js index 7c7fb27..01e0340 100644 --- a/src/popup/views/settings.js +++ b/src/popup/views/settings.js @@ -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() { renderSiteList( "settings-allowed-sites", @@ -154,4 +161,4 @@ function init(ctx) { }); } -module.exports = { init, renderSiteLists }; +module.exports = { init, show, renderSiteLists }; diff --git a/src/shared/state.js b/src/shared/state.js index 1f47b00..86d2728 100644 --- a/src/shared/state.js +++ b/src/shared/state.js @@ -29,6 +29,7 @@ const DEFAULT_STATE = { const state = { ...DEFAULT_STATE, + currentView: null, selectedWallet: null, selectedAddress: null, selectedToken: null, @@ -53,6 +54,10 @@ async function saveState() { dustThresholdGwei: state.dustThresholdGwei, fraudContracts: state.fraudContracts, tokenHolderCache: state.tokenHolderCache, + currentView: state.currentView, + selectedWallet: state.selectedWallet, + selectedAddress: state.selectedAddress, + selectedToken: state.selectedToken, }; await storageApi.set({ autistmask: persisted }); } @@ -103,6 +108,12 @@ async function loadState() { : 100000; state.fraudContracts = saved.fraudContracts || []; 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; } }