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>
308 lines
9.5 KiB
JavaScript
308 lines
9.5 KiB
JavaScript
const { $, showView, showFlash, goBack, clearViewStack } = require("./helpers");
|
|
const {
|
|
generateMnemonic,
|
|
hdWalletFromMnemonic,
|
|
isValidMnemonic,
|
|
addressFromPrivateKey,
|
|
hdWalletFromXprv,
|
|
isValidXprv,
|
|
} = require("../../shared/wallet");
|
|
const { encryptWithPassword } = require("../../shared/vault");
|
|
const { state, saveState } = require("../../shared/state");
|
|
const { scanForAddresses } = require("../../shared/balances");
|
|
|
|
/**
|
|
* Check if an address already exists in ANY wallet (hd, xprv, or key).
|
|
* Returns the wallet object if found, or undefined.
|
|
*/
|
|
function findWalletByAddress(addr) {
|
|
const lower = addr.toLowerCase();
|
|
return state.wallets.find((w) =>
|
|
w.addresses.some((a) => a.address.toLowerCase() === lower),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Check if an xpub already exists in any HD-type wallet (hd or xprv).
|
|
* Returns the wallet object if found, or undefined.
|
|
*/
|
|
function findWalletByXpub(xpub) {
|
|
return state.wallets.find((w) => w.xpub && w.xpub === xpub);
|
|
}
|
|
|
|
let currentMode = "mnemonic";
|
|
|
|
const MODES = ["mnemonic", "privkey", "xprv"];
|
|
|
|
const PASSWORD_HINTS = {
|
|
mnemonic:
|
|
"This password encrypts your recovery phrase on this device. You will need it to send funds.",
|
|
privkey:
|
|
"This password encrypts your private key on this device. You will need it to send funds.",
|
|
xprv: "This password encrypts your key on this device. You will need it to send funds.",
|
|
};
|
|
|
|
function switchMode(mode) {
|
|
currentMode = mode;
|
|
for (const m of MODES) {
|
|
$("add-wallet-section-" + m).classList.toggle("hidden", m !== mode);
|
|
const tab = $("tab-" + m);
|
|
const isActive = m === mode;
|
|
// Active: bold, solid border on top/sides, no bottom border (connects to content)
|
|
tab.classList.toggle("font-bold", isActive);
|
|
tab.classList.toggle("border-solid", isActive);
|
|
tab.classList.toggle("border-border", isActive);
|
|
tab.classList.toggle("border-b-bg", isActive);
|
|
tab.classList.toggle("bg-bg", isActive);
|
|
// Inactive: muted text, dashed border on top/sides, transparent bottom, hover invert
|
|
tab.classList.toggle("text-muted", !isActive);
|
|
tab.classList.toggle("border-dashed", !isActive);
|
|
tab.classList.toggle("border-border-light", !isActive);
|
|
tab.classList.toggle("border-b-transparent", !isActive);
|
|
tab.classList.toggle("hover:bg-fg", !isActive);
|
|
tab.classList.toggle("hover:text-bg", !isActive);
|
|
}
|
|
$("add-wallet-password-hint").textContent = PASSWORD_HINTS[mode];
|
|
}
|
|
|
|
function show() {
|
|
$("wallet-mnemonic").value = "";
|
|
$("import-private-key").value = "";
|
|
$("import-xprv-key").value = "";
|
|
$("add-wallet-password").value = "";
|
|
$("add-wallet-password-confirm").value = "";
|
|
$("add-wallet-phrase-warning").style.visibility = "hidden";
|
|
switchMode("mnemonic");
|
|
showView("add-wallet");
|
|
}
|
|
|
|
function validatePassword() {
|
|
const pw = $("add-wallet-password").value;
|
|
const pw2 = $("add-wallet-password-confirm").value;
|
|
if (!pw) {
|
|
showFlash("Please choose a password.");
|
|
return null;
|
|
}
|
|
if (pw.length < 12) {
|
|
showFlash("Password must be at least 12 characters.");
|
|
return null;
|
|
}
|
|
if (pw !== pw2) {
|
|
showFlash("Passwords do not match.");
|
|
return null;
|
|
}
|
|
return pw;
|
|
}
|
|
|
|
async function importMnemonic(ctx) {
|
|
const mnemonic = $("wallet-mnemonic").value.trim();
|
|
if (!mnemonic) {
|
|
showFlash("Enter a recovery phrase or press the die to generate one.");
|
|
return;
|
|
}
|
|
const words = mnemonic.split(/\s+/);
|
|
if (words.length !== 12 && words.length !== 24) {
|
|
showFlash(
|
|
"Recovery phrase must be 12 or 24 words. You entered " +
|
|
words.length +
|
|
".",
|
|
);
|
|
return;
|
|
}
|
|
if (!isValidMnemonic(mnemonic)) {
|
|
showFlash("Invalid recovery phrase. Check for typos.");
|
|
return;
|
|
}
|
|
const pw = validatePassword();
|
|
if (!pw) return;
|
|
const { xpub, firstAddress } = hdWalletFromMnemonic(mnemonic);
|
|
const xpubDup = findWalletByXpub(xpub);
|
|
if (xpubDup) {
|
|
showFlash(
|
|
"This recovery phrase is already added (" + xpubDup.name + ").",
|
|
);
|
|
return;
|
|
}
|
|
const addrDup = findWalletByAddress(firstAddress);
|
|
if (addrDup) {
|
|
showFlash("Address already exists in wallet (" + addrDup.name + ").");
|
|
return;
|
|
}
|
|
const encrypted = await encryptWithPassword(mnemonic, pw);
|
|
const walletNum = state.wallets.length + 1;
|
|
const wallet = {
|
|
type: "hd",
|
|
name: "Wallet " + walletNum,
|
|
xpub: xpub,
|
|
encryptedSecret: encrypted,
|
|
nextIndex: 1,
|
|
addresses: [
|
|
{ address: firstAddress, balance: "0.0000", tokenBalances: [] },
|
|
],
|
|
};
|
|
state.wallets.push(wallet);
|
|
state.hasWallet = true;
|
|
await saveState();
|
|
clearViewStack();
|
|
ctx.renderWalletList();
|
|
showView("main");
|
|
|
|
// Scan for used HD addresses beyond index 0.
|
|
showFlash("Scanning for addresses...", 30000);
|
|
const scan = await scanForAddresses(xpub, state.rpcUrl);
|
|
if (scan.addresses.length > 1) {
|
|
wallet.addresses = scan.addresses.map((a) => ({
|
|
address: a.address,
|
|
balance: "0.0000",
|
|
tokenBalances: [],
|
|
}));
|
|
wallet.nextIndex = scan.nextIndex;
|
|
await saveState();
|
|
ctx.renderWalletList();
|
|
showFlash("Found " + scan.addresses.length + " addresses.");
|
|
} else {
|
|
showFlash("Ready.", 1000);
|
|
}
|
|
|
|
ctx.doRefreshAndRender();
|
|
}
|
|
|
|
async function importPrivateKey(ctx) {
|
|
const key = $("import-private-key").value.trim();
|
|
if (!key) {
|
|
showFlash("Please enter your private key.");
|
|
return;
|
|
}
|
|
let addr;
|
|
try {
|
|
addr = addressFromPrivateKey(key);
|
|
} catch (e) {
|
|
showFlash("Invalid private key.");
|
|
return;
|
|
}
|
|
const pw = validatePassword();
|
|
if (!pw) return;
|
|
const duplicate = findWalletByAddress(addr);
|
|
if (duplicate) {
|
|
showFlash(
|
|
"This address already exists in wallet (" + duplicate.name + ").",
|
|
);
|
|
return;
|
|
}
|
|
const encrypted = await encryptWithPassword(key, pw);
|
|
const walletNum = state.wallets.length + 1;
|
|
state.wallets.push({
|
|
type: "key",
|
|
name: "Wallet " + walletNum,
|
|
encryptedSecret: encrypted,
|
|
addresses: [{ address: addr, balance: "0.0000", tokenBalances: [] }],
|
|
});
|
|
state.hasWallet = true;
|
|
await saveState();
|
|
clearViewStack();
|
|
ctx.renderWalletList();
|
|
showView("main");
|
|
|
|
ctx.doRefreshAndRender();
|
|
}
|
|
|
|
async function importXprvKey(ctx) {
|
|
const xprv = $("import-xprv-key").value.trim();
|
|
if (!xprv) {
|
|
showFlash("Please enter your extended private key.");
|
|
return;
|
|
}
|
|
if (!isValidXprv(xprv)) {
|
|
showFlash("Invalid extended private key.");
|
|
return;
|
|
}
|
|
let result;
|
|
try {
|
|
result = hdWalletFromXprv(xprv);
|
|
} catch (e) {
|
|
showFlash("Invalid extended private key.");
|
|
return;
|
|
}
|
|
const { xpub, firstAddress } = result;
|
|
const xpubDup = findWalletByXpub(xpub);
|
|
if (xpubDup) {
|
|
showFlash("This key is already added (" + xpubDup.name + ").");
|
|
return;
|
|
}
|
|
const addrDup = findWalletByAddress(firstAddress);
|
|
if (addrDup) {
|
|
showFlash("Address already exists in wallet (" + addrDup.name + ").");
|
|
return;
|
|
}
|
|
const pw = validatePassword();
|
|
if (!pw) return;
|
|
const encrypted = await encryptWithPassword(xprv, pw);
|
|
const walletNum = state.wallets.length + 1;
|
|
const wallet = {
|
|
type: "xprv",
|
|
name: "Wallet " + walletNum,
|
|
xpub: xpub,
|
|
encryptedSecret: encrypted,
|
|
nextIndex: 1,
|
|
addresses: [
|
|
{ address: firstAddress, balance: "0.0000", tokenBalances: [] },
|
|
],
|
|
};
|
|
state.wallets.push(wallet);
|
|
state.hasWallet = true;
|
|
await saveState();
|
|
clearViewStack();
|
|
ctx.renderWalletList();
|
|
showView("main");
|
|
|
|
// Scan for used HD addresses beyond index 0.
|
|
showFlash("Scanning for addresses...", 30000);
|
|
const scan = await scanForAddresses(xpub, state.rpcUrl);
|
|
if (scan.addresses.length > 1) {
|
|
wallet.addresses = scan.addresses.map((a) => ({
|
|
address: a.address,
|
|
balance: "0.0000",
|
|
tokenBalances: [],
|
|
}));
|
|
wallet.nextIndex = scan.nextIndex;
|
|
await saveState();
|
|
ctx.renderWalletList();
|
|
showFlash("Found " + scan.addresses.length + " addresses.");
|
|
} else {
|
|
showFlash("Ready.", 1000);
|
|
}
|
|
|
|
ctx.doRefreshAndRender();
|
|
}
|
|
|
|
function init(ctx) {
|
|
// Tab click handlers
|
|
$("tab-mnemonic").addEventListener("click", () => switchMode("mnemonic"));
|
|
$("tab-privkey").addEventListener("click", () => switchMode("privkey"));
|
|
$("tab-xprv").addEventListener("click", () => switchMode("xprv"));
|
|
|
|
// Generate mnemonic
|
|
$("btn-generate-phrase").addEventListener("click", () => {
|
|
$("wallet-mnemonic").value = generateMnemonic();
|
|
$("add-wallet-phrase-warning").style.visibility = "visible";
|
|
});
|
|
|
|
// Import / confirm
|
|
$("btn-add-wallet-confirm").addEventListener("click", async () => {
|
|
if (currentMode === "mnemonic") {
|
|
await importMnemonic(ctx);
|
|
} else if (currentMode === "privkey") {
|
|
await importPrivateKey(ctx);
|
|
} else if (currentMode === "xprv") {
|
|
await importXprvKey(ctx);
|
|
}
|
|
});
|
|
|
|
// Back button
|
|
$("btn-add-wallet-back").addEventListener("click", () => {
|
|
goBack();
|
|
});
|
|
}
|
|
|
|
module.exports = { init, show };
|