fix: implement proper view navigation stack (#146)
All checks were successful
check / check (push) Successful in 25s
All checks were successful
check / check (push) Successful in 25s
## 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](#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 <user@Mac.lan guest wan> Reviewed-on: #146 Co-authored-by: clawbot <clawbot@noreply.example.org> Co-committed-by: clawbot <clawbot@noreply.example.org>
This commit is contained in:
parent
39db06c83d
commit
a22f33d511
@ -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);
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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");
|
||||
});
|
||||
|
||||
|
||||
@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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.");
|
||||
}
|
||||
});
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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");
|
||||
});
|
||||
|
||||
|
||||
@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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 : [];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user