fix: an open popup moves to the recovery screen when its profile becomes unreadable (closes #373)
A popup already open when the stored profile became unreadable stayed on the last good profile until reopened. Every save already runs the check loadState() runs at open; a save refused by it now stops the ten-second refresh, runs the leave cleanup of the current screen, and raises the recovery screen. From then on showView() shows nothing else in that popup, so a transaction wait or a later save cannot take the user off it or clear an export or a typed confirmation. That is held in memory, never as the saved current view, so a popup opened after the record is erased elsewhere opens normally. Any other failed save keeps the "NOT SAVED" banner. The popup test harness now honours clearInterval(). Model: opus-5-5
This commit is contained in:
@@ -1114,16 +1114,18 @@ because bumping for one would send every older install to StateRecovery for
|
||||
nothing.
|
||||
|
||||
Every read of the record goes through `assertStateUsable()` first, on the raw
|
||||
bytes, before normalization: `loadState()` for the popup and `getState()` for
|
||||
the background. It refuses a record that is not an object, a `schemaVersion`
|
||||
this build does not understand (a newer one included), a `wallets` that is not a
|
||||
list of wallet records with address records in them, and a `networkId` that is
|
||||
not a network in `src/shared/networks.js`. Refusing is the whole point — a
|
||||
record the wallet cannot vouch for is never normalized, never written back, and
|
||||
never half-loaded. The popup shows StateRecovery; a dApp gets a specific error
|
||||
(`-32007`, an EIP-1474 server-error code the spec leaves unassigned) saying the
|
||||
saved data cannot be read and that nothing was signed or sent, rather than the
|
||||
generic `-32603` every request used to answer.
|
||||
bytes, before normalization: `loadState()` and every `saveState()` for the
|
||||
popup, and `getState()` for the background. It refuses a record that is not an
|
||||
object, a `schemaVersion` this build does not understand (a newer one included),
|
||||
a `wallets` that is not a list of wallet records with address records in them,
|
||||
and a `networkId` that is not a network in `src/shared/networks.js`. Refusing is
|
||||
the whole point — a record the wallet cannot vouch for is never normalized,
|
||||
never written back, and never half-loaded. The popup shows StateRecovery,
|
||||
whether it finds the record unreadable when it opens or at a save while it is
|
||||
open; a dApp gets a specific error (`-32007`, an EIP-1474 server-error code the
|
||||
spec leaves unassigned) saying the saved data cannot be read and that nothing
|
||||
was signed or sent, rather than the generic `-32603` every request used to
|
||||
answer.
|
||||
|
||||
Every other field of the record is floored in `normalizePersisted()` rather than
|
||||
gated, and the floor is not the same for every field. Some are type-checked as a
|
||||
@@ -1167,8 +1169,9 @@ now also reported rather than swallowed: `onSaveFailure()` in
|
||||
`src/shared/state.js` is called for every failed save, awaited or not, and the
|
||||
popup puts up a persistent "NOT SAVED" banner (`showSaveFailureBanner()` in
|
||||
`src/popup/views/helpers.js`). Storage can still fail for reasons no floor
|
||||
covers — a quota, a revoked permission, a record a newer build wrote — and the
|
||||
wallet must never look healthy while that is true.
|
||||
covers — a quota, a revoked permission — and the wallet must never look healthy
|
||||
while that is true. A save that fails because the stored record fails the gate,
|
||||
such as one a newer build wrote, gets StateRecovery instead of the banner.
|
||||
|
||||
The `networkId` check is not cosmetic: that value is an object KEY into
|
||||
`state.networkEndpoints`, so an unvalidated `"__proto__"` would set the map's
|
||||
@@ -1978,9 +1981,19 @@ view would leave a wallet one click from deletion.
|
||||
|
||||
#### StateRecovery (`state-recovery`)
|
||||
|
||||
- **When**: `loadState()` refused the stored profile, so the popup has no
|
||||
profile at all. It is the only screen reached without one, and the only one
|
||||
that never appears during ordinary use.
|
||||
- **When**: the stored profile fails `assertStateUsable()`. At open, that is
|
||||
`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
|
||||
the stored record and runs the same check before writing, so the popup finds
|
||||
it at the next navigation or ten-second refresh, whether or not the network
|
||||
answers ([#373](https://git.eeqj.de/sneak/AutistMask/issues/373)). A save that
|
||||
fails for any other reason, such as a storage read or write that errors, gets
|
||||
the "NOT SAVED" banner instead and leaves the screen as it is. The screen it
|
||||
replaces is left as any navigation leaves it, so a revealed phrase or key, or
|
||||
a typed password, is wiped. Once up, the screen stays until the popup closes
|
||||
or reloads: work still running in the popup, such as a transaction wait,
|
||||
cannot replace it, even after the record is erased in another window. 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
|
||||
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.
|
||||
@@ -2007,16 +2020,20 @@ view would leave a wallet one click from deletion.
|
||||
Nothing was erased." on the error line
|
||||
- **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
|
||||
could not be read, and `showView()` is not used to raise it for the same
|
||||
reason — it reads and writes the state singleton.
|
||||
could not be read. `showView()` is not used to raise it, for the same reason:
|
||||
it reads and writes the state singleton. Under an open popup the screen is
|
||||
passed to `showView()` only to run the replaced screen's cleanup; from then on
|
||||
`showView()` shows nothing else in that popup.
|
||||
- **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
|
||||
no export destroys the only copy of a record that may hold recoverable key
|
||||
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
|
||||
is no profile to check one against.
|
||||
- Not in `RESTORABLE_VIEWS`: it is never persisted as the current view, because
|
||||
nothing on this path writes state at all.
|
||||
- Not in `RESTORABLE_VIEWS`, and never recorded as the current view: the record
|
||||
can become readable again under an open popup, erased in another window, and
|
||||
the next save from that popup then succeeds. A popup opened after that opens
|
||||
normally.
|
||||
|
||||
### External Services
|
||||
|
||||
|
||||
Reference in New Issue
Block a user