Files
AutistMask/src/popup/views/addWallet.js
clawbot a22f33d511
All checks were successful
check / check (push) Successful in 25s
fix: implement proper view navigation stack (#146)
## 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>
2026-03-02 00:15:01 +01:00

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 };