From a22f33d511ab2ec43a52144d66ad3311b28a0ce6 Mon Sep 17 00:00:00 2001 From: clawbot Date: Mon, 2 Mar 2026 00:15:01 +0100 Subject: [PATCH] fix: implement proper view navigation stack (#146) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes the view stack pop bug where pressing Back in Settings (or any view) always returned to Main instead of the previous view. Closes [issue #134](https://git.eeqj.de/sneak/AutistMask/issues/134) ## Problem The popup UI had no navigation stack. Every back button was hardcoded to a specific destination (usually Main). The reported path: > Main → Address → Transaction → Settings (gear icon) → Back ...would go to Main instead of returning to the Transaction view. ## Solution Implemented a proper view navigation stack (like iOS) as already described in the README: - **`viewStack`** array added to persisted state — survives popup close/reopen - **`pushCurrentView()`** — pushes the current view name onto the stack before any forward navigation - **`goBack()`** — pops the stack and shows the previous view; falls back to Main if the stack is empty; re-renders the wallet list when returning to Main - **`clearViewStack()`** — resets the stack for root transitions (e.g., after adding/deleting a wallet) ### What Changed 1. **helpers.js** — Added navigation stack functions (`pushCurrentView`, `goBack`, `clearViewStack`, `setRenderMain`) 2. **state.js** — Added `viewStack` to persisted state 3. **index.js** — All `ctx.show*()` wrappers now push before navigating forward; gear button uses stack for toggle behavior 4. **All view back buttons** — Replaced hardcoded destinations with `goBack()` (settings, addressDetail, addressToken, transactionDetail, send, receive, addToken, confirmTx, addWallet, settingsAddToken, deleteWallet, export-privkey) 5. **Direct `showView()` forward navigations** — Added `pushCurrentView()` calls before `showView("send")` in addressDetail, addressToken, and home; before `showView("export-privkey")` in addressDetail; before `deleteWallet.show()` in settings 6. **Reset-to-root transitions** — `clearViewStack()` called after adding a wallet (all 3 import types), after deleting the last wallet, and after transaction completion (Done button) ### Navigation Paths Verified - **Main → Settings → Back** → returns to Main ✓ - **Main → Address → Settings → Back** → returns to Address ✓ - **Main → Address → Transaction → Settings → Back** → returns to Transaction ✓ (the reported bug) - **Main → Address → Token → Send → ConfirmTx → Back → Back → Back → Back** → unwinds correctly through each view back to Main ✓ - **Main → Address → Token → Transaction → Settings → Back** → returns to Transaction ✓ - **Settings → Add Wallet → (add) → Main** → stack cleared, fresh root ✓ - **Settings → Delete Wallet → Back** → returns to Settings ✓ - **Settings → Delete Wallet → (confirm)** → stack reset to [main], settings shown ✓ - **Address → Send → ConfirmTx → (broadcast) → SuccessTx → Done** → stack reset, returns to address context ✓ - **Popup close/reopen** → viewStack persisted, back navigation still works ✓ Co-authored-by: user Reviewed-on: https://git.eeqj.de/sneak/AutistMask/pulls/146 Co-authored-by: clawbot Co-committed-by: clawbot --- src/popup/index.js | 60 ++++++++++++++++++++++------ src/popup/views/addToken.js | 13 ++++-- src/popup/views/addWallet.js | 12 +++--- src/popup/views/addressDetail.js | 9 +++-- src/popup/views/addressToken.js | 5 ++- src/popup/views/confirmTx.js | 3 +- src/popup/views/deleteWallet.js | 13 ++++-- src/popup/views/helpers.js | 41 +++++++++++++++++++ src/popup/views/home.js | 2 + src/popup/views/receive.js | 7 +--- src/popup/views/send.js | 7 +--- src/popup/views/settings.js | 13 ++++-- src/popup/views/settingsAddToken.js | 4 +- src/popup/views/transactionDetail.js | 7 +--- src/popup/views/txStatus.js | 11 ++++- src/shared/state.js | 3 ++ 16 files changed, 158 insertions(+), 52 deletions(-) diff --git a/src/popup/index.js b/src/popup/index.js index 0a93870..6f8f6d2 100644 --- a/src/popup/index.js +++ b/src/popup/index.js @@ -10,7 +10,14 @@ const { } = require("../shared/state"); const { refreshPrices } = require("../shared/prices"); const { refreshBalances } = require("../shared/balances"); -const { $, showView } = require("./views/helpers"); +const { + $, + showView, + setRenderMain, + pushCurrentView, + goBack, + clearViewStack, +} = require("./views/helpers"); const { applyTheme } = require("./theme"); const home = require("./views/home"); @@ -58,15 +65,42 @@ async function doRefreshAndRender() { const ctx = { renderWalletList, doRefreshAndRender, - showAddWalletView: () => addWallet.show(), - showAddressDetail: () => addressDetail.show(), - showAddressToken: () => addressToken.show(), - showAddTokenView: () => addToken.show(), - showConfirmTx: (txInfo) => confirmTx.show(txInfo), - showReceive: () => receive.show(), - showTransactionDetail: (tx) => transactionDetail.show(tx), - showSettingsView: () => settings.show(), - showSettingsAddTokenView: () => settingsAddToken.show(), + showAddWalletView: () => { + pushCurrentView(); + addWallet.show(); + }, + showAddressDetail: () => { + pushCurrentView(); + addressDetail.show(); + }, + showAddressToken: () => { + pushCurrentView(); + addressToken.show(); + }, + showAddTokenView: () => { + pushCurrentView(); + addToken.show(); + }, + showConfirmTx: (txInfo) => { + pushCurrentView(); + confirmTx.show(txInfo); + }, + showReceive: () => { + pushCurrentView(); + receive.show(); + }, + showTransactionDetail: (tx) => { + pushCurrentView(); + transactionDetail.show(tx); + }, + showSettingsView: () => { + pushCurrentView(); + settings.show(); + }, + showSettingsAddTokenView: () => { + pushCurrentView(); + settingsAddToken.show(); + }, }; // Views that can be fully re-rendered from persisted state. @@ -220,13 +254,15 @@ async function init() { .getElementById("view-settings") .classList.contains("hidden") ) { - renderWalletList(); - showView("main"); + goBack(); return; } + pushCurrentView(); settings.show(); }); + setRenderMain(renderWalletList); + welcome.init(ctx); addWallet.init(ctx); home.init(ctx); diff --git a/src/popup/views/addToken.js b/src/popup/views/addToken.js index c908788..88c7b36 100644 --- a/src/popup/views/addToken.js +++ b/src/popup/views/addToken.js @@ -1,4 +1,4 @@ -const { $, showView, showFlash } = require("./helpers"); +const { $, showFlash, goBack } = require("./helpers"); const { getTopTokens } = require("../../shared/tokenList"); const { state, saveState } = require("../../shared/state"); const { lookupTokenInfo } = require("../../shared/balances"); @@ -59,7 +59,12 @@ function init(ctx) { }); await saveState(); ctx.doRefreshAndRender(); - ctx.showAddressDetail(); + // Pop the stack (back to address detail) and re-render it + // so the newly added token is visible immediately. + if (state.viewStack.length > 0) { + state.viewStack.pop(); + } + require("./addressDetail").show(); } catch (e) { const detail = e.shortMessage || e.message || String(e); log.errorf("Token lookup failed for", contractAddr, detail); @@ -69,7 +74,9 @@ function init(ctx) { } }); - $("btn-add-token-back").addEventListener("click", ctx.showAddressDetail); + $("btn-add-token-back").addEventListener("click", () => { + goBack(); + }); } module.exports = { init, show }; diff --git a/src/popup/views/addWallet.js b/src/popup/views/addWallet.js index c62a037..5a746e1 100644 --- a/src/popup/views/addWallet.js +++ b/src/popup/views/addWallet.js @@ -1,4 +1,4 @@ -const { $, showView, showFlash } = require("./helpers"); +const { $, showView, showFlash, goBack, clearViewStack } = require("./helpers"); const { generateMnemonic, hdWalletFromMnemonic, @@ -143,6 +143,7 @@ async function importMnemonic(ctx) { state.wallets.push(wallet); state.hasWallet = true; await saveState(); + clearViewStack(); ctx.renderWalletList(); showView("main"); @@ -198,6 +199,7 @@ async function importPrivateKey(ctx) { }); state.hasWallet = true; await saveState(); + clearViewStack(); ctx.renderWalletList(); showView("main"); @@ -249,6 +251,7 @@ async function importXprvKey(ctx) { state.wallets.push(wallet); state.hasWallet = true; await saveState(); + clearViewStack(); ctx.renderWalletList(); showView("main"); @@ -297,12 +300,7 @@ function init(ctx) { // Back button $("btn-add-wallet-back").addEventListener("click", () => { - if (!state.hasWallet) { - showView("welcome"); - } else { - ctx.renderWalletList(); - showView("main"); - } + goBack(); }); } diff --git a/src/popup/views/addressDetail.js b/src/popup/views/addressDetail.js index 51dfbc4..006e3ae 100644 --- a/src/popup/views/addressDetail.js +++ b/src/popup/views/addressDetail.js @@ -10,6 +10,8 @@ const { truncateMiddle, renderAddressHtml, attachCopyHandlers, + goBack, + pushCurrentView, } = require("./helpers"); const { state, currentAddress, saveState } = require("../../shared/state"); const { formatUsd, getAddressValueUsd } = require("../../shared/prices"); @@ -247,8 +249,7 @@ function init(_ctx) { ctx = _ctx; $("btn-address-back").addEventListener("click", () => { - ctx.renderWalletList(); - showView("main"); + goBack(); }); $("btn-send").addEventListener("click", () => { @@ -266,6 +267,7 @@ function init(_ctx) { $("send-token-static").classList.add("hidden"); updateSendBalance(); resetSendValidation(); + pushCurrentView(); showView("send"); }); @@ -295,6 +297,7 @@ function init(_ctx) { $("btn-export-privkey").addEventListener("click", () => { moreDropdown.classList.add("hidden"); moreBtn.classList.remove("bg-fg", "text-bg"); + pushCurrentView(); const wallet = state.wallets[state.selectedWallet]; const addr = wallet.addresses[state.selectedAddress]; const blockieEl = $("export-privkey-jazzicon"); @@ -367,7 +370,7 @@ function init(_ctx) { $("btn-export-privkey-back").addEventListener("click", () => { $("export-privkey-value").textContent = ""; $("export-privkey-password").value = ""; - show(); + goBack(); }); } diff --git a/src/popup/views/addressToken.js b/src/popup/views/addressToken.js index 7b9d58a..145e562 100644 --- a/src/popup/views/addressToken.js +++ b/src/popup/views/addressToken.js @@ -13,6 +13,8 @@ const { balanceLine, renderAddressHtml, attachCopyHandlers, + goBack, + pushCurrentView, } = require("./helpers"); const { state, currentAddress, saveState } = require("../../shared/state"); const { TOKEN_BY_ADDRESS, resolveSymbol } = require("../../shared/tokenList"); @@ -331,7 +333,7 @@ function init(_ctx) { }); $("btn-address-token-back").addEventListener("click", () => { - ctx.showAddressDetail(); + goBack(); }); $("btn-address-token-send").addEventListener("click", () => { @@ -365,6 +367,7 @@ function init(_ctx) { attachCopyHandlers($("send-token-static")); updateSendBalance(); resetSendValidation(); + pushCurrentView(); showView("send"); }); diff --git a/src/popup/views/confirmTx.js b/src/popup/views/confirmTx.js index cfd8960..af3a50f 100644 --- a/src/popup/views/confirmTx.js +++ b/src/popup/views/confirmTx.js @@ -20,6 +20,7 @@ const { escapeHtml, renderAddressHtml, attachCopyHandlers, + goBack, } = require("./helpers"); const { state, currentNetwork } = require("../../shared/state"); const { getSignerForAddress } = require("../../shared/wallet"); @@ -355,7 +356,7 @@ function init(ctx) { }); $("btn-confirm-back").addEventListener("click", () => { - showView("send"); + goBack(); }); } diff --git a/src/popup/views/deleteWallet.js b/src/popup/views/deleteWallet.js index 213a24d..f682e23 100644 --- a/src/popup/views/deleteWallet.js +++ b/src/popup/views/deleteWallet.js @@ -1,4 +1,4 @@ -const { $, showView, showFlash } = require("./helpers"); +const { $, showView, showFlash, goBack, clearViewStack } = require("./helpers"); const { state, saveState } = require("../../shared/state"); const { decryptWithPassword } = require("../../shared/vault"); @@ -21,7 +21,7 @@ function init(_ctx) { $("btn-delete-wallet-back").addEventListener("click", () => { deleteWalletIndex = null; - ctx.showSettingsView(); + goBack(); }); $("btn-delete-wallet-confirm").addEventListener("click", async () => { @@ -77,6 +77,7 @@ function init(_ctx) { state.selectedWallet = null; state.selectedAddress = null; state.activeAddress = null; + clearViewStack(); await saveState(); showView("welcome"); } else { @@ -86,8 +87,14 @@ function init(_ctx) { state.activeAddress = state.wallets[0].addresses[0]?.address || null; await saveState(); + // Reset stack to [main] so Settings back goes home. + // Use require() lazily to avoid circular dependency + // (settings.js requires deleteWallet.js). + clearViewStack(); + state.viewStack.push("main"); ctx.renderWalletList(); - ctx.showSettingsView(); + const settings = require("./settings"); + settings.show(); showFlash("Wallet deleted."); } }); diff --git a/src/popup/views/helpers.js b/src/popup/views/helpers.js index 6560ee5..e4744ef 100644 --- a/src/popup/views/helpers.js +++ b/src/popup/views/helpers.js @@ -75,6 +75,43 @@ function showView(name) { } } +// Callback to re-render the main/home view when navigating back to it. +// Set once by index.js via setRenderMain(). +let _renderMain = null; + +function setRenderMain(fn) { + _renderMain = fn; +} + +// Push the current view onto the navigation stack so goBack() can +// return to it. Call this before any forward navigation. +function pushCurrentView() { + if (state.currentView) { + state.viewStack.push(state.currentView); + } +} + +// Pop the navigation stack and show the previous view. If the stack +// is empty, fall back to the main (home) view. +function goBack() { + let target; + if (state.viewStack.length > 0) { + target = state.viewStack.pop(); + } else { + target = "main"; + } + if (target === "main" && _renderMain) { + _renderMain(); + } + showView(target); +} + +// Clear the entire navigation stack (used when resetting to root, +// e.g. after adding or deleting a wallet). +function clearViewStack() { + state.viewStack = []; +} + let flashTimer = null; function clearFlash() { @@ -380,6 +417,10 @@ module.exports = { showError, hideError, showView, + setRenderMain, + pushCurrentView, + goBack, + clearViewStack, showFlash, flashCopyFeedback, balanceLine, diff --git a/src/popup/views/home.js b/src/popup/views/home.js index e6b0706..32d1d64 100644 --- a/src/popup/views/home.js +++ b/src/popup/views/home.js @@ -12,6 +12,7 @@ const { truncateMiddle, renderAddressHtml, attachCopyHandlers, + pushCurrentView, } = require("./helpers"); const { state, saveState, currentAddress } = require("../../shared/state"); const { @@ -381,6 +382,7 @@ function init(ctx) { renderSendTokenSelect(addr); updateSendBalance(); resetSendValidation(); + pushCurrentView(); showView("send"); }); diff --git a/src/popup/views/receive.js b/src/popup/views/receive.js index 4fdb930..25d71ae 100644 --- a/src/popup/views/receive.js +++ b/src/popup/views/receive.js @@ -6,6 +6,7 @@ const { formatAddressHtml, addressTitle, attachCopyHandlers, + goBack, } = require("./helpers"); const { state, currentAddress, currentNetwork } = require("../../shared/state"); const QRCode = require("qrcode"); @@ -67,11 +68,7 @@ function init(ctx) { }); $("btn-receive-back").addEventListener("click", () => { - if (state.selectedToken) { - ctx.showAddressToken(); - } else { - ctx.showAddressDetail(); - } + goBack(); }); } diff --git a/src/popup/views/send.js b/src/popup/views/send.js index e5db64e..f91654f 100644 --- a/src/popup/views/send.js +++ b/src/popup/views/send.js @@ -7,6 +7,7 @@ const { escapeHtml, renderAddressHtml, attachCopyHandlers, + goBack, } = require("./helpers"); const { state, currentAddress } = require("../../shared/state"); let ctx; @@ -250,11 +251,7 @@ function init(_ctx) { $("btn-send-back").addEventListener("click", () => { $("send-token").classList.remove("hidden"); $("send-token-static").classList.add("hidden"); - if (state.selectedToken) { - ctx.showAddressToken(); - } else { - ctx.showAddressDetail(); - } + goBack(); }); } diff --git a/src/popup/views/settings.js b/src/popup/views/settings.js index 07b42ac..aa42a7a 100644 --- a/src/popup/views/settings.js +++ b/src/popup/views/settings.js @@ -1,4 +1,11 @@ -const { $, showView, showFlash, escapeHtml } = require("./helpers"); +const { + $, + showView, + showFlash, + escapeHtml, + goBack, + pushCurrentView, +} = require("./helpers"); const { applyTheme } = require("../theme"); const { state, saveState, currentNetwork } = require("../../shared/state"); const { NETWORKS, SUPPORTED_CHAIN_IDS } = require("../../shared/networks"); @@ -86,6 +93,7 @@ function renderWalletListSettings() { container.querySelectorAll(".btn-delete-wallet").forEach((btn) => { btn.addEventListener("click", () => { const idx = parseInt(btn.dataset.idx, 10); + pushCurrentView(); deleteWallet.show(idx); }); }); @@ -282,8 +290,7 @@ function init(ctx) { ); $("btn-settings-back").addEventListener("click", () => { - ctx.renderWalletList(); - showView("main"); + goBack(); }); } diff --git a/src/popup/views/settingsAddToken.js b/src/popup/views/settingsAddToken.js index 0344f8b..09a26e2 100644 --- a/src/popup/views/settingsAddToken.js +++ b/src/popup/views/settingsAddToken.js @@ -1,4 +1,4 @@ -const { $, showView, showFlash } = require("./helpers"); +const { $, showView, showFlash, goBack } = require("./helpers"); const { getTopTokens } = require("../../shared/tokenList"); const { state, saveState } = require("../../shared/state"); const { lookupTokenInfo } = require("../../shared/balances"); @@ -84,7 +84,7 @@ function init(_ctx) { ctx = _ctx; $("btn-settings-addtoken-back").addEventListener("click", () => { - ctx.showSettingsView(); + goBack(); }); $("btn-settings-addtoken-select").addEventListener("click", async () => { diff --git a/src/popup/views/transactionDetail.js b/src/popup/views/transactionDetail.js index 13c2c51..2cb4437 100644 --- a/src/popup/views/transactionDetail.js +++ b/src/popup/views/transactionDetail.js @@ -14,6 +14,7 @@ const { attachCopyHandlers, copyableHtml, etherscanLinkHtml, + goBack, } = require("./helpers"); const { state, currentNetwork } = require("../../shared/state"); const { formatEther, formatUnits } = require("ethers"); @@ -350,11 +351,7 @@ async function loadFullTxDetails(txHash, toAddress, isContractCall) { function init(_ctx) { ctx = _ctx; $("btn-tx-back").addEventListener("click", () => { - if (state.selectedToken) { - ctx.showAddressToken(); - } else { - ctx.showAddressDetail(); - } + goBack(); }); } diff --git a/src/popup/views/txStatus.js b/src/popup/views/txStatus.js index e8cf421..d45a67d 100644 --- a/src/popup/views/txStatus.js +++ b/src/popup/views/txStatus.js @@ -9,6 +9,7 @@ const { attachCopyHandlers, copyableHtml, etherscanLinkHtml, + clearViewStack, } = require("./helpers"); const { TOKEN_BY_ADDRESS } = require("../../shared/tokenList"); const { state, saveState, currentNetwork } = require("../../shared/state"); @@ -221,10 +222,16 @@ function navigateBack() { window.close(); return; } + // After a completed transaction, reset the navigation stack + // and go directly to the address view (token or detail). + // Use require() lazily to call show() without the ctx push wrapper. + clearViewStack(); + state.viewStack.push("main"); if (state.selectedToken) { - ctx.showAddressToken(); + state.viewStack.push("address"); + require("./addressToken").show(); } else { - ctx.showAddressDetail(); + require("./addressDetail").show(); } } diff --git a/src/shared/state.js b/src/shared/state.js index b39faed..e8ee1b1 100644 --- a/src/shared/state.js +++ b/src/shared/state.js @@ -38,6 +38,7 @@ const state = { selectedAddress: null, selectedToken: null, viewData: {}, + viewStack: [], }; // Return the network configuration for the currently selected network. @@ -72,6 +73,7 @@ async function saveState() { selectedAddress: state.selectedAddress, selectedToken: state.selectedToken, viewData: state.viewData, + viewStack: state.viewStack, }; await storageApi.set({ autistmask: persisted }); } @@ -133,6 +135,7 @@ async function loadState() { saved.selectedAddress !== undefined ? saved.selectedAddress : null; state.selectedToken = saved.selectedToken || null; state.viewData = saved.viewData || {}; + state.viewStack = Array.isArray(saved.viewStack) ? saved.viewStack : []; } }