Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76c52f4ac6 |
@@ -1985,16 +1985,15 @@ view would leave a wallet one click from deletion.
|
|||||||
`loadState()` refusing it, so the popup has no profile at all. While the popup
|
`loadState()` refusing it, so the popup has no profile at all. While the popup
|
||||||
is open, on any screen, it is a save refusing it: every `saveState()` reads
|
is open, on any screen, it is a save refusing it: every `saveState()` reads
|
||||||
the stored record and runs the same check before writing, so the popup finds
|
the stored record and runs the same check before writing, so the popup finds
|
||||||
it at the next navigation, or at the next ten-second balance refresh that
|
it at the next navigation or ten-second refresh, whether or not the network
|
||||||
reaches the network ([#373](https://git.eeqj.de/sneak/AutistMask/issues/373)).
|
answers ([#373](https://git.eeqj.de/sneak/AutistMask/issues/373)). A save that
|
||||||
A save that fails for any other reason, such as a storage read or write that
|
fails for any other reason, such as a storage read or write that errors, gets
|
||||||
errors, gets the "NOT SAVED" banner instead and leaves the screen as it is.
|
the "NOT SAVED" banner instead and leaves the screen as it is. The screen it
|
||||||
The screen it replaces is left as any navigation leaves it, so a revealed
|
replaces is left as any navigation leaves it, so a revealed phrase or key, or
|
||||||
phrase or key, or a typed password, is wiped. Once up, the screen stays until
|
a typed password, is wiped. Once up, the screen stays until the popup closes
|
||||||
the popup closes or the record is erased: work still running in the popup,
|
or reloads: work still running in the popup, such as a transaction wait,
|
||||||
such as a transaction wait, cannot replace it, and nothing in AutistMask
|
cannot replace it, even after the record is erased in another window. It is
|
||||||
writes over a record that fails the check, so it cannot become readable again
|
the only screen that never appears during ordinary use.
|
||||||
underneath. It is the only screen that never appears during ordinary use.
|
|
||||||
- **Why it exists**: a record the wallet cannot read used to render nothing — no
|
- **Why it exists**: a record the wallet cannot read used to render nothing — no
|
||||||
view, no message, no control — while every dApp call answered a generic
|
view, no message, no control — while every dApp call answered a generic
|
||||||
internal error, and no reset or wipe control existed anywhere in the product.
|
internal error, and no reset or wipe control existed anywhere in the product.
|
||||||
@@ -2021,20 +2020,20 @@ view would leave a wallet one click from deletion.
|
|||||||
Nothing was erased." on the error line
|
Nothing was erased." on the error line
|
||||||
- **No other control is reachable.** The Settings gear is hidden while this
|
- **No other control is reachable.** The Settings gear is hidden while this
|
||||||
screen is up, because every screen behind it renders from the profile that
|
screen is up, because every screen behind it renders from the profile that
|
||||||
could not be read. At open `showView()` is not used to raise it for the same
|
could not be read. `showView()` is not used to raise it, for the same reason:
|
||||||
reason — it reads and writes the state singleton. Under an open popup it is
|
it reads and writes the state singleton. Under an open popup the screen is
|
||||||
also passed to `showView()`, which runs the replaced screen's cleanup and
|
passed to `showView()` only to run the replaced screen's cleanup; from then on
|
||||||
records it as the current view, and `showView()` shows nothing else after
|
`showView()` shows nothing else in that popup.
|
||||||
that.
|
|
||||||
- **Both controls are required.** An export with no reset leaves the user
|
- **Both controls are required.** An export with no reset leaves the user
|
||||||
looking at a broken profile with no way to use the wallet again; a reset with
|
looking at a broken profile with no way to use the wallet again; a reset with
|
||||||
no export destroys the only copy of a record that may hold recoverable key
|
no export destroys the only copy of a record that may hold recoverable key
|
||||||
material. The typed phrase is the same barrier DeleteWalletLostPassword uses,
|
material. The typed phrase is the same barrier DeleteWalletLostPassword uses,
|
||||||
and for the same reason: there is no password to gate this with, since there
|
and for the same reason: there is no password to gate this with, since there
|
||||||
is no profile to check one against.
|
is no profile to check one against.
|
||||||
- Not in `RESTORABLE_VIEWS`, and not persisted as the current view: at open
|
- Not in `RESTORABLE_VIEWS`, and never recorded as the current view: the record
|
||||||
nothing on this path writes state at all, and under an open popup the check
|
can become readable again under an open popup, erased in another window, and
|
||||||
that raised the screen refuses the save that would record it.
|
the next save from that popup then succeeds. A popup opened after that shows
|
||||||
|
**Welcome** or **Home** as usual.
|
||||||
|
|
||||||
### External Services
|
### External Services
|
||||||
|
|
||||||
|
|||||||
@@ -49,14 +49,16 @@ but the review is broader than any of them.
|
|||||||
unreadable moves to the recovery screen
|
unreadable moves to the recovery screen
|
||||||
([#373](https://git.eeqj.de/sneak/AutistMask/issues/373)). It used to stay on
|
([#373](https://git.eeqj.de/sneak/AutistMask/issues/373)). It used to stay on
|
||||||
the last good profile, with the "NOT SAVED" banner at most, until reopened.
|
the last good profile, with the "NOT SAVED" banner at most, until reopened.
|
||||||
Every save already ran the check the popup runs at open; a save that fails
|
Every save already ran the check the popup runs at open, so the popup finds
|
||||||
that check now raises the recovery screen and stops the ten-second refresh, so
|
the record at the next navigation or ten-second refresh, whether or not the
|
||||||
the popup finds the record at the next navigation or refresh. The screen it
|
network answers; a save that fails that check now raises the recovery screen
|
||||||
replaces is left as any navigation leaves it, so a revealed phrase or key or a
|
and stops the refresh. The screen it replaces is left as any navigation leaves
|
||||||
typed password is wiped. Once up, nothing else can replace it, and a later
|
it, so a revealed phrase or key or a typed password is wiped. Once up, nothing
|
||||||
save or a transaction wait that ends does not clear an export or a typed
|
else in that popup can replace it, and a later save or a transaction wait that
|
||||||
confirmation. Any other failed save still gets the banner and leaves the
|
ends does not clear an export or a typed confirmation. It is never saved as
|
||||||
screen alone.
|
the current view, so a popup opened after the record is erased in another
|
||||||
|
window opens normally. Any other failed save still gets the banner and leaves
|
||||||
|
the screen alone.
|
||||||
- 2026-10-04: The signature screen shows a personal message as the bytes that
|
- 2026-10-04: The signature screen shows a personal message as the bytes that
|
||||||
are signed ([#403](https://git.eeqj.de/sneak/AutistMask/issues/403)). It
|
are signed ([#403](https://git.eeqj.de/sneak/AutistMask/issues/403)). It
|
||||||
showed only the decoded text, with bidirectional and zero-width characters
|
showed only the decoded text, with bidirectional and zero-width characters
|
||||||
|
|||||||
+9
-9
@@ -164,19 +164,19 @@ async function init() {
|
|||||||
// check loadState() runs below. So a record that becomes unreadable while
|
// check loadState() runs below. So a record that becomes unreadable while
|
||||||
// the popup is open is found by the next save, a navigation or the
|
// the popup is open is found by the next save, a navigation or the
|
||||||
// ten-second refresh, and gets the screen it would get at open
|
// ten-second refresh, and gets the screen it would get at open
|
||||||
// (https://git.eeqj.de/sneak/AutistMask/issues/373). The recovery screen
|
// (https://git.eeqj.de/sneak/AutistMask/issues/373). Passing the recovery
|
||||||
// is then also passed to showView(), which runs the leave cleanup of the
|
// screen to showView() first leaves the current screen as any navigation
|
||||||
// screen it replaces, so a phrase, key or password on it is wiped, and
|
// does, so a phrase, key or password on it is wiped, and from then on
|
||||||
// records it as the current view, after which showView() shows nothing
|
// showView() shows nothing else. A later save that fails the same way,
|
||||||
// else. The save showView() fires fails too and comes back here, where
|
// such as a refresh already in flight, comes back here, where both calls
|
||||||
// both calls see the screen already up and do nothing. Any other failed
|
// see the screen already up and do nothing. Any other failed save is a
|
||||||
// save is a read or write that failed, and gets the banner without
|
// read or write that failed, and gets the banner without changing the
|
||||||
// changing the screen.
|
// screen.
|
||||||
onSaveFailure((e) => {
|
onSaveFailure((e) => {
|
||||||
if (e instanceof StateUnusableError) {
|
if (e instanceof StateUnusableError) {
|
||||||
clearInterval(refreshTimer);
|
clearInterval(refreshTimer);
|
||||||
stateRecovery.show(e);
|
|
||||||
showView("state-recovery");
|
showView("state-recovery");
|
||||||
|
stateRecovery.show(e);
|
||||||
} else {
|
} else {
|
||||||
showSaveFailureBanner(e);
|
showSaveFailureBanner(e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,10 +52,8 @@ const VIEWS = [
|
|||||||
"export-privkey",
|
"export-privkey",
|
||||||
"show-phrase",
|
"show-phrase",
|
||||||
// Shown by src/popup/views/stateRecovery.js when the stored profile
|
// Shown by src/popup/views/stateRecovery.js when the stored profile
|
||||||
// cannot be read. At open it is not reached through showView(), since the
|
// cannot be read, never by showView() (see there), but listed so that
|
||||||
// state singleton refuses to be read; under an open popup,
|
// every view-hiding loop covers it.
|
||||||
// src/popup/index.js also passes it to showView() so the screen it
|
|
||||||
// replaces is left like any other.
|
|
||||||
"state-recovery",
|
"state-recovery",
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -86,17 +84,28 @@ function hideError(id) {
|
|||||||
el.style.visibility = "hidden";
|
el.style.visibility = "hidden";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set when src/popup/index.js passes the recovery screen to showView(), and
|
||||||
|
// never cleared. Kept in memory for this popup's life, never in
|
||||||
|
// state.currentView, which is saved: a popup opened later must not inherit it.
|
||||||
|
let stateRecoveryShown = false;
|
||||||
|
|
||||||
function showView(name) {
|
function showView(name) {
|
||||||
// The recovery screen, once up, is never replaced: work still running
|
// The recovery screen, once up, is never replaced: work still running
|
||||||
// when it went up, such as a transaction wait, must not take the user off
|
// when it went up, such as a transaction wait, must not take the user off
|
||||||
// it or clear what they exported or typed there
|
// it or clear what they exported or typed there
|
||||||
// (https://git.eeqj.de/sneak/AutistMask/issues/373).
|
// (https://git.eeqj.de/sneak/AutistMask/issues/373).
|
||||||
if (state.currentView === "state-recovery") return;
|
if (stateRecoveryShown) return;
|
||||||
const leaving = state.currentView;
|
const leaving = state.currentView;
|
||||||
if (leaving && leaving !== name) {
|
if (leaving && leaving !== name) {
|
||||||
const onLeave = viewLeaveHandlers.get(leaving);
|
const onLeave = viewLeaveHandlers.get(leaving);
|
||||||
if (onLeave) onLeave();
|
if (onLeave) onLeave();
|
||||||
}
|
}
|
||||||
|
// Passed here only so the screen it replaces is left like any other;
|
||||||
|
// stateRecovery.show() raises it, and it is never the current view.
|
||||||
|
if (name === "state-recovery") {
|
||||||
|
stateRecoveryShown = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
for (const v of VIEWS) {
|
for (const v of VIEWS) {
|
||||||
const el = document.getElementById(`view-${v}`);
|
const el = document.getElementById(`view-${v}`);
|
||||||
if (el) {
|
if (el) {
|
||||||
|
|||||||
@@ -6,8 +6,7 @@
|
|||||||
// the time this runs, the stored record has been REFUSED, deliberately: at
|
// the time this runs, the stored record has been REFUSED, deliberately: at
|
||||||
// open loadState() refused it and reading the singleton throws
|
// open loadState() refused it and reading the singleton throws
|
||||||
// (https://git.eeqj.de/sneak/AutistMask/issues/311), and under an open popup
|
// (https://git.eeqj.de/sneak/AutistMask/issues/311), and under an open popup
|
||||||
// a save refused it, so every further save fails too
|
// a save refused it (https://git.eeqj.de/sneak/AutistMask/issues/373).
|
||||||
// (https://git.eeqj.de/sneak/AutistMask/issues/373).
|
|
||||||
//
|
//
|
||||||
// So this module talks to the DOM directly and touches no state at all. It is
|
// So this module talks to the DOM directly and touches no state at all. It is
|
||||||
// the one screen that must work when nothing else can, which is also why it
|
// the one screen that must work when nothing else can, which is also why it
|
||||||
|
|||||||
@@ -195,6 +195,29 @@ describe("a popup already open when the stored profile becomes unreadable", () =
|
|||||||
expect(env.value("state-recovery-reset-input")).toBe("erase my");
|
expect(env.value("state-recovery-reset-input")).toBe("erase my");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// The record can become readable again under this popup, erased from the
|
||||||
|
// recovery screen of another window, so a save from this one can succeed.
|
||||||
|
test("a popup opened after the record is erased elsewhere shows a screen", async () => {
|
||||||
|
const env = await bootPopup(unversionedValidProfile());
|
||||||
|
env.storage.write("autistmask", CORRUPT_BLOBS[0].blob);
|
||||||
|
await env.tick();
|
||||||
|
expect(env.visibleViews()).toEqual(["state-recovery"]);
|
||||||
|
|
||||||
|
await env.storage.remove("autistmask");
|
||||||
|
const { saveState } = require("../src/shared/state");
|
||||||
|
await saveState();
|
||||||
|
|
||||||
|
const reopened = await bootPopup(env.storage.read("autistmask"));
|
||||||
|
expect(reopened.visibleViews()).toEqual(["welcome"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a stored current view of the recovery screen does not blank the popup", async () => {
|
||||||
|
const env = await bootPopup(
|
||||||
|
unversionedValidProfile({ currentView: "state-recovery" }),
|
||||||
|
);
|
||||||
|
expect(env.visibleViews()).toEqual(["main"]);
|
||||||
|
});
|
||||||
|
|
||||||
test("a transaction wait that ends under it does not replace it", async () => {
|
test("a transaction wait that ends under it does not replace it", async () => {
|
||||||
const env = await bootPopup(
|
const env = await bootPopup(
|
||||||
unversionedValidProfile({
|
unversionedValidProfile({
|
||||||
|
|||||||
Reference in New Issue
Block a user