Compare commits
1 Commits
issue-280-
...
e36d83627a
| Author | SHA1 | Date | |
|---|---|---|---|
| e36d83627a |
6
TODO.md
6
TODO.md
@@ -44,6 +44,12 @@ undefined identifiers, which is how
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
|
- 2026-08-12: The restored navigation stack is filtered against
|
||||||
|
`RESTORABLE_VIEWS` on load, truncated at the first entry the popup would not
|
||||||
|
render so that every surviving entry keeps the Back target it had. Reopening
|
||||||
|
the popup can no longer put Back onto a screen whose content is never
|
||||||
|
re-rendered, such as `export-privkey` or `show-phrase`
|
||||||
|
([#224](https://git.eeqj.de/sneak/AutistMask/issues/224)).
|
||||||
- 2026-08-12: The dust threshold field now explains a rejection instead of
|
- 2026-08-12: The dust threshold field now explains a rejection instead of
|
||||||
snapping back in silence, with the parse in a pure, unit-tested module that
|
snapping back in silence, with the parse in a pure, unit-tested module that
|
||||||
accepts plain decimal digits only — hex and exponent notation are refused
|
accepts plain decimal digits only — hex and exponent notation are refused
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
const { DEFAULT_RPC_URL, DEFAULT_BLOCKSCOUT_URL } = require("./constants");
|
const { DEFAULT_RPC_URL, DEFAULT_BLOCKSCOUT_URL } = require("./constants");
|
||||||
const { networkById } = require("./networks");
|
const { networkById } = require("./networks");
|
||||||
|
// Dependency-free constant module; safe to pull into a background bundle.
|
||||||
|
const { RESTORABLE_VIEWS } = require("../popup/restorableViews");
|
||||||
|
|
||||||
const storageApi =
|
const storageApi =
|
||||||
typeof browser !== "undefined"
|
typeof browser !== "undefined"
|
||||||
@@ -43,6 +45,37 @@ const state = {
|
|||||||
viewStack: [],
|
viewStack: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Keep only the leading run of stored views the popup is willing to render.
|
||||||
|
//
|
||||||
|
// restoreView() refuses to reopen ONTO a non-restorable view, but the stack
|
||||||
|
// behind it used to be restored verbatim, so Back could walk onto a screen
|
||||||
|
// whose content is deliberately never re-rendered — and "show-phrase" has no
|
||||||
|
// Back control to leave by. Truncating at the first such entry instead of
|
||||||
|
// splicing it out keeps the result a prefix of the stored stack, so every
|
||||||
|
// surviving entry's Back target is exactly the one it had; splicing would
|
||||||
|
// silently re-point the entry above the hole at a different screen.
|
||||||
|
//
|
||||||
|
// Filtering happens here on load rather than in saveState(): the live
|
||||||
|
// in-session stack is legitimate (the screen really is rendered while the
|
||||||
|
// popup is open), and only a load-side filter also repairs the stacks
|
||||||
|
// already in storage, including ones written before a view left the set.
|
||||||
|
function restorableStack(stored, currentView) {
|
||||||
|
if (!Array.isArray(stored)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const cut = stored.findIndex((view) => !RESTORABLE_VIEWS.has(view));
|
||||||
|
const kept = cut === -1 ? stored.slice() : stored.slice(0, cut);
|
||||||
|
// A view restored below the root still needs somewhere for Back to go.
|
||||||
|
if (
|
||||||
|
kept.length === 0 &&
|
||||||
|
currentView !== "main" &&
|
||||||
|
RESTORABLE_VIEWS.has(currentView)
|
||||||
|
) {
|
||||||
|
return ["main"];
|
||||||
|
}
|
||||||
|
return kept;
|
||||||
|
}
|
||||||
|
|
||||||
// Return the network configuration for the currently selected network.
|
// Return the network configuration for the currently selected network.
|
||||||
function currentNetwork() {
|
function currentNetwork() {
|
||||||
return networkById(state.networkId);
|
return networkById(state.networkId);
|
||||||
@@ -150,7 +183,7 @@ async function loadState() {
|
|||||||
saved.selectedAddress !== undefined ? saved.selectedAddress : null;
|
saved.selectedAddress !== undefined ? saved.selectedAddress : null;
|
||||||
state.selectedToken = saved.selectedToken || null;
|
state.selectedToken = saved.selectedToken || null;
|
||||||
state.viewData = saved.viewData || {};
|
state.viewData = saved.viewData || {};
|
||||||
state.viewStack = Array.isArray(saved.viewStack) ? saved.viewStack : [];
|
state.viewStack = restorableStack(saved.viewStack, state.currentView);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -159,3 +159,92 @@ describe("hideSpoofedSymbols persistence", () => {
|
|||||||
expect(second.mod.state.hideSpoofedSymbols).toBe(true);
|
expect(second.mod.state.hideSpoofedSymbols).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// restoreView() refuses to reopen ONTO a non-restorable view, but the stack
|
||||||
|
// behind it was restored verbatim, so Back could still walk onto a screen
|
||||||
|
// whose content is deliberately never re-rendered — and "show-phrase" has no
|
||||||
|
// Back control of its own to leave by. The stack is filtered on load, at the
|
||||||
|
// first entry the popup would not render, and everything above it goes too:
|
||||||
|
// those entries were reached THROUGH the dropped one.
|
||||||
|
describe("restored viewStack is filtered against RESTORABLE_VIEWS", () => {
|
||||||
|
const NON_RESTORABLE = ["export-privkey", "show-phrase"];
|
||||||
|
|
||||||
|
function restoredStack(viewStack, currentView = "settings") {
|
||||||
|
return loadModuleWith({
|
||||||
|
wallets: oneWallet(),
|
||||||
|
currentView,
|
||||||
|
viewStack,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test("a non-restorable view at the top of the stack is dropped", async () => {
|
||||||
|
const { mod } = restoredStack(["main", "address", "export-privkey"]);
|
||||||
|
await mod.loadState();
|
||||||
|
expect(mod.state.viewStack).toEqual(["main", "address"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a non-restorable view in the middle truncates the stack there", async () => {
|
||||||
|
const { mod } = restoredStack(["main", "show-phrase", "address"]);
|
||||||
|
await mod.loadState();
|
||||||
|
expect(mod.state.viewStack).toEqual(["main"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Truncating a stack rooted at a non-restorable view leaves nothing, and
|
||||||
|
// the restored view still needs somewhere for Back to go.
|
||||||
|
test("a non-restorable view at the bottom leaves main to go back to", async () => {
|
||||||
|
const { mod } = restoredStack(["export-privkey", "address", "receive"]);
|
||||||
|
await mod.loadState();
|
||||||
|
expect(mod.state.viewStack).toEqual(["main"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("no restored stack retains a secret-bearing view", async () => {
|
||||||
|
for (const view of NON_RESTORABLE) {
|
||||||
|
const { mod } = restoredStack(["main", "address", view, "receive"]);
|
||||||
|
await mod.loadState();
|
||||||
|
expect(mod.state.viewStack).not.toContain(view);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// The rule is "views the popup will render", not a blocklist of the two
|
||||||
|
// secret screens: a name no longer in the set (or never a view at all)
|
||||||
|
// has to go the same way.
|
||||||
|
test("a name that is not a restorable view at all is dropped", async () => {
|
||||||
|
const { mod } = restoredStack(["main", "welcome", "address"]);
|
||||||
|
await mod.loadState();
|
||||||
|
expect(mod.state.viewStack).toEqual(["main"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("an ordinary restorable stack is restored unchanged", async () => {
|
||||||
|
const stack = ["main", "address", "address-token"];
|
||||||
|
const { mod } = restoredStack(stack);
|
||||||
|
await mod.loadState();
|
||||||
|
expect(mod.state.viewStack).toEqual(stack);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("restoring onto main keeps the stack empty", async () => {
|
||||||
|
const { mod } = restoredStack(["show-phrase"], "main");
|
||||||
|
await mod.loadState();
|
||||||
|
expect(mod.state.viewStack).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a stack that is not an array still loads as empty", async () => {
|
||||||
|
const { mod } = restoredStack("main");
|
||||||
|
await mod.loadState();
|
||||||
|
expect(mod.state.viewStack).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filtering belongs on load, not on save: the live in-session stack is
|
||||||
|
// legitimate — the user really is one Back away from a screen that is
|
||||||
|
// rendered right now — and only a load-side filter also cleans the
|
||||||
|
// stacks already sitting in storage.
|
||||||
|
test("saveState persists the live stack verbatim", async () => {
|
||||||
|
const { mod, set } = loadModuleWith(null);
|
||||||
|
mod.state.viewStack = ["main", "address", "export-privkey"];
|
||||||
|
await mod.saveState();
|
||||||
|
expect(set).toHaveBeenCalledWith({
|
||||||
|
autistmask: expect.objectContaining({
|
||||||
|
viewStack: ["main", "address", "export-privkey"],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user