fix: show every password error in its own line below the field #496
@@ -1444,14 +1444,17 @@ view would leave a wallet one click from deletion.
|
|||||||
without it, the lost-password route on DeleteWallet is the first they
|
without it, the lost-password route on DeleteWallet is the first they
|
||||||
would hear of it. The hint line reserves its height, so switching tabs
|
would hear of it. The hint line reserves its height, so switching tabs
|
||||||
cannot move the password fields under the pointer.
|
cannot move the password fields under the pointer.
|
||||||
- "Import" button
|
- "Import" button, with the error line beside it so that it adds no height
|
||||||
|
to a screen whose button already starts near the bottom of the popup
|
||||||
- **Transitions**:
|
- **Transitions**:
|
||||||
- "Import" with a valid entry and a matching password of at least 12
|
- "Import" with a valid entry and a matching password of at least 12
|
||||||
characters → creates the wallet, clears the navigation stack, and →
|
characters → creates the wallet, clears the navigation stack, and →
|
||||||
**Home**. The phrase and xprv modes then scan for further used addresses
|
**Home**. The phrase and xprv modes then scan for further used addresses
|
||||||
and report the count as a flash message.
|
and report the count as a flash message.
|
||||||
- "Import" with an invalid entry, a duplicate wallet or address, or a short
|
- "Import" with a missing, short or mismatched password → full-sentence
|
||||||
or mismatched password → flash message, no screen change
|
error on the error line, no screen change
|
||||||
|
- "Import" with an invalid entry or a duplicate wallet or address → flash
|
||||||
|
message, no screen change
|
||||||
- "Back" → previous screen (Welcome, Home, or Settings)
|
- "Back" → previous screen (Welcome, Home, or Settings)
|
||||||
|
|
||||||
#### AddressDetail (`address`)
|
#### AddressDetail (`address`)
|
||||||
@@ -1490,8 +1493,8 @@ view would leave a wallet one click from deletion.
|
|||||||
copy)
|
copy)
|
||||||
- Warning that anyone holding the private key can transfer all funds from
|
- Warning that anyone holding the private key can transfer all funds from
|
||||||
the address
|
the address
|
||||||
- Error line
|
- Password input, error line and "Reveal" button, shown until the key is
|
||||||
- Password input and "Reveal" button, shown until the key is revealed
|
revealed
|
||||||
- The private key on a highlighted background, tap to copy, shown only after
|
- The private key on a highlighted background, tap to copy, shown only after
|
||||||
the password has been accepted
|
the password has been accepted
|
||||||
- **Transitions**:
|
- **Transitions**:
|
||||||
@@ -1829,8 +1832,8 @@ view would leave a wallet one click from deletion.
|
|||||||
- Wallet name
|
- Wallet name
|
||||||
- Warning box stating that anyone holding these words can take everything in
|
- Warning box stating that anyone holding these words can take everything in
|
||||||
the wallet, from any device, without the password
|
the wallet, from any device, without the password
|
||||||
- Error line
|
- Password input, error line and "Reveal" button, shown until the password
|
||||||
- Password input + "Reveal" button, shown until the password is accepted
|
is accepted
|
||||||
- The recovery phrase itself, in full and click-to-copy, shown only after a
|
- The recovery phrase itself, in full and click-to-copy, shown only after a
|
||||||
correct password and in place of the password prompt
|
correct password and in place of the password prompt
|
||||||
- **Transitions**:
|
- **Transitions**:
|
||||||
@@ -1859,8 +1862,8 @@ view would leave a wallet one click from deletion.
|
|||||||
- "Back" button, "Delete Wallet" heading
|
- "Back" button, "Delete Wallet" heading
|
||||||
- Warning naming the wallet and stating that deletion is permanent and any
|
- Warning naming the wallet and stating that deletion is permanent and any
|
||||||
funds are unrecoverable without the recovery phrase
|
funds are unrecoverable without the recovery phrase
|
||||||
- Error line
|
|
||||||
- Password input
|
- Password input
|
||||||
|
- Error line
|
||||||
- "Confirm Delete" button
|
- "Confirm Delete" button
|
||||||
- An underlined "I have lost my password" control
|
- An underlined "I have lost my password" control
|
||||||
- **Transitions**:
|
- **Transitions**:
|
||||||
|
|||||||
@@ -44,6 +44,19 @@ then continue tagging as milestones land.
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
|
- 2026-10-07: Every password error in the popup is shown the same way
|
||||||
|
([#493](https://git.eeqj.de/sneak/AutistMask/issues/493)): with `showError()`
|
||||||
|
and `hideError()` in a fixed-height error line below the password field, as
|
||||||
|
the send confirmation and approval screens already did. The add wallet screen
|
||||||
|
showed a missing, short or mismatched password in the flash line, and the
|
||||||
|
private key export, recovery phrase and delete wallet screens each had a line
|
||||||
|
of their own above the field. On the add wallet screen the line sits beside
|
||||||
|
the Import button, so the button stays where it was at 360x600. Each line
|
||||||
|
clears when the screen is shown again and when the password is tried again.
|
||||||
|
The add wallet screen's other messages, such as an invalid recovery phrase, a
|
||||||
|
duplicate wallet and the address scan, stay in the flash line.
|
||||||
|
`tests/passwordErrorLines.test.js` drives all four screens in the popup.
|
||||||
|
|
||||||
- 2026-10-07: Pre-1.0 security review of the extension
|
- 2026-10-07: Pre-1.0 security review of the extension
|
||||||
([#383](https://git.eeqj.de/sneak/AutistMask/issues/383)), reading the tree at
|
([#383](https://git.eeqj.de/sneak/AutistMask/issues/383)), reading the tree at
|
||||||
`99292b9` for key handling, the DEBUG mode policy, and what the background
|
`99292b9` for key handling, the DEBUG mode policy, and what the background
|
||||||
|
|||||||
+30
-20
@@ -207,12 +207,22 @@
|
|||||||
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
|
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<!-- The error line sits beside Import, not above it: at
|
||||||
id="btn-add-wallet-confirm"
|
360x600 the button already starts near the bottom of
|
||||||
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
|
the popup, and a line of its own would push it below
|
||||||
>
|
the fold. The longest error fits on one line here. -->
|
||||||
Import
|
<div class="flex items-center gap-2">
|
||||||
</button>
|
<button
|
||||||
|
id="btn-add-wallet-confirm"
|
||||||
|
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
|
||||||
|
>
|
||||||
|
Import
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
id="add-wallet-password-error"
|
||||||
|
class="text-xs min-h-[1.25rem] invisible"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ============ MAIN VIEW: ALL WALLETS & ADDRESSES ============ -->
|
<!-- ============ MAIN VIEW: ALL WALLETS & ADDRESSES ============ -->
|
||||||
@@ -396,10 +406,6 @@
|
|||||||
Warning: anyone with this private key can access and
|
Warning: anyone with this private key can access and
|
||||||
transfer all funds from this address. Never share it.
|
transfer all funds from this address. Never share it.
|
||||||
</p>
|
</p>
|
||||||
<div
|
|
||||||
id="export-privkey-flash"
|
|
||||||
class="text-xs mb-2 min-h-[1.25rem] invisible"
|
|
||||||
></div>
|
|
||||||
<div id="export-privkey-password-section" class="mb-2">
|
<div id="export-privkey-password-section" class="mb-2">
|
||||||
<label class="block mb-1">Password</label>
|
<label class="block mb-1">Password</label>
|
||||||
<input
|
<input
|
||||||
@@ -408,9 +414,13 @@
|
|||||||
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
|
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
|
||||||
placeholder="Enter your password to continue"
|
placeholder="Enter your password to continue"
|
||||||
/>
|
/>
|
||||||
|
<div
|
||||||
|
id="export-privkey-password-error"
|
||||||
|
class="text-xs mt-2 mb-2 min-h-[1.25rem] invisible"
|
||||||
|
></div>
|
||||||
<button
|
<button
|
||||||
id="btn-export-privkey-confirm"
|
id="btn-export-privkey-confirm"
|
||||||
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer mt-2"
|
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
|
||||||
>
|
>
|
||||||
Reveal
|
Reveal
|
||||||
</button>
|
</button>
|
||||||
@@ -1116,10 +1126,6 @@
|
|||||||
<strong id="delete-wallet-name"></strong> is permanent. Any
|
<strong id="delete-wallet-name"></strong> is permanent. Any
|
||||||
funds will be unrecoverable without your recovery phrase.
|
funds will be unrecoverable without your recovery phrase.
|
||||||
</p>
|
</p>
|
||||||
<div
|
|
||||||
id="delete-wallet-flash"
|
|
||||||
class="text-xs text-red-500 mb-2 min-h-[1.25rem] invisible"
|
|
||||||
></div>
|
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<label class="block mb-1">Password</label>
|
<label class="block mb-1">Password</label>
|
||||||
<input
|
<input
|
||||||
@@ -1129,6 +1135,10 @@
|
|||||||
placeholder="Enter your password to confirm"
|
placeholder="Enter your password to confirm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
id="delete-wallet-password-error"
|
||||||
|
class="text-xs mb-2 min-h-[1.25rem] invisible"
|
||||||
|
></div>
|
||||||
<button
|
<button
|
||||||
id="btn-delete-wallet-confirm"
|
id="btn-delete-wallet-confirm"
|
||||||
class="border border-border text-red-500 px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
|
class="border border-border text-red-500 px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
|
||||||
@@ -1273,10 +1283,6 @@
|
|||||||
this wallet, from any device, without your password. Never
|
this wallet, from any device, without your password. Never
|
||||||
type them into a website and never show them to anyone.
|
type them into a website and never show them to anyone.
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
id="show-phrase-flash"
|
|
||||||
class="text-xs text-red-500 mb-2 min-h-[1.25rem] invisible"
|
|
||||||
></div>
|
|
||||||
<div id="show-phrase-password-section" class="mb-2">
|
<div id="show-phrase-password-section" class="mb-2">
|
||||||
<label class="block mb-1">Password</label>
|
<label class="block mb-1">Password</label>
|
||||||
<input
|
<input
|
||||||
@@ -1285,9 +1291,13 @@
|
|||||||
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
|
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
|
||||||
placeholder="Enter your password to continue"
|
placeholder="Enter your password to continue"
|
||||||
/>
|
/>
|
||||||
|
<div
|
||||||
|
id="show-phrase-password-error"
|
||||||
|
class="text-xs mt-2 mb-2 min-h-[1.25rem] invisible"
|
||||||
|
></div>
|
||||||
<button
|
<button
|
||||||
id="btn-show-phrase-reveal"
|
id="btn-show-phrase-reveal"
|
||||||
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer mt-2"
|
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
|
||||||
>
|
>
|
||||||
Reveal
|
Reveal
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ const {
|
|||||||
$,
|
$,
|
||||||
showView,
|
showView,
|
||||||
showFlash,
|
showFlash,
|
||||||
|
showError,
|
||||||
|
hideError,
|
||||||
goBack,
|
goBack,
|
||||||
clearViewStack,
|
clearViewStack,
|
||||||
onViewLeave,
|
onViewLeave,
|
||||||
@@ -98,6 +100,7 @@ function clear() {
|
|||||||
$("add-wallet-password").value = "";
|
$("add-wallet-password").value = "";
|
||||||
$("add-wallet-password-confirm").value = "";
|
$("add-wallet-password-confirm").value = "";
|
||||||
$("add-wallet-phrase-warning").style.visibility = "hidden";
|
$("add-wallet-phrase-warning").style.visibility = "hidden";
|
||||||
|
hideError("add-wallet-password-error");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Each wallet has its own password (its own encryptedSecret), so adding a
|
// Each wallet has its own password (its own encryptedSecret), so adding a
|
||||||
@@ -125,15 +128,18 @@ function validatePassword() {
|
|||||||
const pw = $("add-wallet-password").value;
|
const pw = $("add-wallet-password").value;
|
||||||
const pw2 = $("add-wallet-password-confirm").value;
|
const pw2 = $("add-wallet-password-confirm").value;
|
||||||
if (!pw) {
|
if (!pw) {
|
||||||
showFlash("Please choose a password.");
|
showError("add-wallet-password-error", "Please choose a password.");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (pw.length < 12) {
|
if (pw.length < 12) {
|
||||||
showFlash("Password must be at least 12 characters.");
|
showError(
|
||||||
|
"add-wallet-password-error",
|
||||||
|
"Password must be at least 12 characters.",
|
||||||
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (pw !== pw2) {
|
if (pw !== pw2) {
|
||||||
showFlash("Passwords do not match.");
|
showError("add-wallet-password-error", "Passwords do not match.");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return pw;
|
return pw;
|
||||||
@@ -342,8 +348,10 @@ function init(ctx) {
|
|||||||
$("add-wallet-phrase-warning").style.visibility = "visible";
|
$("add-wallet-phrase-warning").style.visibility = "visible";
|
||||||
});
|
});
|
||||||
|
|
||||||
// Import / confirm
|
// Import / confirm. Each press starts with no password error on screen:
|
||||||
|
// validatePassword() puts it back if the password is still wrong.
|
||||||
$("btn-add-wallet-confirm").addEventListener("click", async () => {
|
$("btn-add-wallet-confirm").addEventListener("click", async () => {
|
||||||
|
hideError("add-wallet-password-error");
|
||||||
if (currentMode === "mnemonic") {
|
if (currentMode === "mnemonic") {
|
||||||
await importMnemonic(ctx);
|
await importMnemonic(ctx);
|
||||||
} else if (currentMode === "privkey") {
|
} else if (currentMode === "privkey") {
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ const {
|
|||||||
$,
|
$,
|
||||||
showView,
|
showView,
|
||||||
showFlash,
|
showFlash,
|
||||||
|
showError,
|
||||||
|
hideError,
|
||||||
goBack,
|
goBack,
|
||||||
clearViewStack,
|
clearViewStack,
|
||||||
onViewLeave,
|
onViewLeave,
|
||||||
@@ -55,8 +57,7 @@ function confirmKey(name) {
|
|||||||
function clear() {
|
function clear() {
|
||||||
deleteWalletIndex = null;
|
deleteWalletIndex = null;
|
||||||
$("delete-wallet-password").value = "";
|
$("delete-wallet-password").value = "";
|
||||||
$("delete-wallet-flash").textContent = "";
|
hideError("delete-wallet-password-error");
|
||||||
$("delete-wallet-flash").style.visibility = "hidden";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The lost-password screen holds no secret — a wallet name is not one —
|
// The lost-password screen holds no secret — a wallet name is not one —
|
||||||
@@ -232,19 +233,22 @@ function init(_ctx) {
|
|||||||
$("btn-delete-wallet-confirm").addEventListener("click", async () => {
|
$("btn-delete-wallet-confirm").addEventListener("click", async () => {
|
||||||
const pw = $("delete-wallet-password").value;
|
const pw = $("delete-wallet-password").value;
|
||||||
if (!pw) {
|
if (!pw) {
|
||||||
$("delete-wallet-flash").textContent =
|
showError(
|
||||||
"Please enter your password.";
|
"delete-wallet-password-error",
|
||||||
$("delete-wallet-flash").style.visibility = "visible";
|
"Please enter your password.",
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (deleteWalletIndex === null) {
|
if (deleteWalletIndex === null) {
|
||||||
$("delete-wallet-flash").textContent =
|
showError(
|
||||||
"No wallet selected for deletion.";
|
"delete-wallet-password-error",
|
||||||
$("delete-wallet-flash").style.visibility = "visible";
|
"No wallet selected for deletion.",
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hideError("delete-wallet-password-error");
|
||||||
const btn = $("btn-delete-wallet-confirm");
|
const btn = $("btn-delete-wallet-confirm");
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.classList.add("text-muted");
|
btn.classList.add("text-muted");
|
||||||
@@ -256,9 +260,10 @@ function init(_ctx) {
|
|||||||
try {
|
try {
|
||||||
await decryptWithPassword(wallet.encryptedSecret, pw);
|
await decryptWithPassword(wallet.encryptedSecret, pw);
|
||||||
} catch {
|
} catch {
|
||||||
$("delete-wallet-flash").textContent =
|
showError(
|
||||||
"That password is incorrect. Please try again.";
|
"delete-wallet-password-error",
|
||||||
$("delete-wallet-flash").style.visibility = "visible";
|
"That password is incorrect. Please try again.",
|
||||||
|
);
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.classList.remove("text-muted");
|
btn.classList.remove("text-muted");
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ const {
|
|||||||
$,
|
$,
|
||||||
showView,
|
showView,
|
||||||
showFlash,
|
showFlash,
|
||||||
|
showError,
|
||||||
|
hideError,
|
||||||
flashCopyFeedback,
|
flashCopyFeedback,
|
||||||
goBack,
|
goBack,
|
||||||
onViewLeave,
|
onViewLeave,
|
||||||
@@ -56,11 +58,6 @@ function isCurrentReveal(generation) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function fail(message) {
|
|
||||||
$("export-privkey-flash").textContent = message;
|
|
||||||
$("export-privkey-flash").style.visibility = "visible";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wipe every trace of the key and drop the address selection. Safe to call
|
// Wipe every trace of the key and drop the address selection. Safe to call
|
||||||
// when nothing was ever revealed, and safe to call twice.
|
// when nothing was ever revealed, and safe to call twice.
|
||||||
function clear() {
|
function clear() {
|
||||||
@@ -71,8 +68,7 @@ function clear() {
|
|||||||
$("export-privkey-password").value = "";
|
$("export-privkey-password").value = "";
|
||||||
$("export-privkey-result").classList.add("hidden");
|
$("export-privkey-result").classList.add("hidden");
|
||||||
$("export-privkey-password-section").classList.remove("hidden");
|
$("export-privkey-password-section").classList.remove("hidden");
|
||||||
$("export-privkey-flash").textContent = "";
|
hideError("export-privkey-password-error");
|
||||||
$("export-privkey-flash").style.visibility = "hidden";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function show(walletIdx, addrIdx) {
|
function show(walletIdx, addrIdx) {
|
||||||
@@ -112,15 +108,19 @@ function show(walletIdx, addrIdx) {
|
|||||||
async function reveal() {
|
async function reveal() {
|
||||||
const password = $("export-privkey-password").value;
|
const password = $("export-privkey-password").value;
|
||||||
if (!password) {
|
if (!password) {
|
||||||
fail("Please enter your password.");
|
showError(
|
||||||
|
"export-privkey-password-error",
|
||||||
|
"Please enter your password.",
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (walletIndex === null) {
|
if (walletIndex === null) {
|
||||||
fail("No address is selected.");
|
showError("export-privkey-password-error", "No address is selected.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const wallet = state.wallets[walletIndex];
|
const wallet = state.wallets[walletIndex];
|
||||||
|
|
||||||
|
hideError("export-privkey-password-error");
|
||||||
const btn = $("btn-export-privkey-confirm");
|
const btn = $("btn-export-privkey-confirm");
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.classList.add("text-muted");
|
btn.classList.add("text-muted");
|
||||||
@@ -140,11 +140,12 @@ async function reveal() {
|
|||||||
$("export-privkey-password-section").classList.add("hidden");
|
$("export-privkey-password-section").classList.add("hidden");
|
||||||
$("export-privkey-value").textContent = signer.privateKey;
|
$("export-privkey-value").textContent = signer.privateKey;
|
||||||
$("export-privkey-result").classList.remove("hidden");
|
$("export-privkey-result").classList.remove("hidden");
|
||||||
$("export-privkey-flash").textContent = "";
|
|
||||||
$("export-privkey-flash").style.visibility = "hidden";
|
|
||||||
} catch {
|
} catch {
|
||||||
if (!isCurrentReveal(generation)) return;
|
if (!isCurrentReveal(generation)) return;
|
||||||
fail("That password is incorrect. Please try again.");
|
showError(
|
||||||
|
"export-privkey-password-error",
|
||||||
|
"That password is incorrect. Please try again.",
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.classList.remove("text-muted");
|
btn.classList.remove("text-muted");
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ const {
|
|||||||
$,
|
$,
|
||||||
showView,
|
showView,
|
||||||
showFlash,
|
showFlash,
|
||||||
|
showError,
|
||||||
|
hideError,
|
||||||
flashCopyFeedback,
|
flashCopyFeedback,
|
||||||
goBack,
|
goBack,
|
||||||
onViewLeave,
|
onViewLeave,
|
||||||
@@ -52,11 +54,6 @@ function isCurrentReveal(generation) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function fail(message) {
|
|
||||||
$("show-phrase-flash").textContent = message;
|
|
||||||
$("show-phrase-flash").style.visibility = "visible";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wipe every trace of the phrase and drop the wallet selection. Safe to
|
// Wipe every trace of the phrase and drop the wallet selection. Safe to
|
||||||
// call when nothing was ever revealed, and safe to call twice.
|
// call when nothing was ever revealed, and safe to call twice.
|
||||||
function clear() {
|
function clear() {
|
||||||
@@ -66,8 +63,7 @@ function clear() {
|
|||||||
$("show-phrase-password").value = "";
|
$("show-phrase-password").value = "";
|
||||||
$("show-phrase-result").classList.add("hidden");
|
$("show-phrase-result").classList.add("hidden");
|
||||||
$("show-phrase-password-section").classList.remove("hidden");
|
$("show-phrase-password-section").classList.remove("hidden");
|
||||||
$("show-phrase-flash").textContent = "";
|
hideError("show-phrase-password-error");
|
||||||
$("show-phrase-flash").style.visibility = "hidden";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function show(walletIdx) {
|
function show(walletIdx) {
|
||||||
@@ -90,19 +86,23 @@ function show(walletIdx) {
|
|||||||
async function reveal() {
|
async function reveal() {
|
||||||
const password = $("show-phrase-password").value;
|
const password = $("show-phrase-password").value;
|
||||||
if (!password) {
|
if (!password) {
|
||||||
fail("Please enter your password.");
|
showError("show-phrase-password-error", "Please enter your password.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (walletIndex === null) {
|
if (walletIndex === null) {
|
||||||
fail("No wallet is selected.");
|
showError("show-phrase-password-error", "No wallet is selected.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const wallet = state.wallets[walletIndex];
|
const wallet = state.wallets[walletIndex];
|
||||||
if (!walletHasRecoveryPhrase(wallet)) {
|
if (!walletHasRecoveryPhrase(wallet)) {
|
||||||
fail("This wallet does not have a recovery phrase.");
|
showError(
|
||||||
|
"show-phrase-password-error",
|
||||||
|
"This wallet does not have a recovery phrase.",
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hideError("show-phrase-password-error");
|
||||||
const btn = $("btn-show-phrase-reveal");
|
const btn = $("btn-show-phrase-reveal");
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.classList.add("text-muted");
|
btn.classList.add("text-muted");
|
||||||
@@ -120,13 +120,14 @@ async function reveal() {
|
|||||||
$("show-phrase-password-section").classList.add("hidden");
|
$("show-phrase-password-section").classList.add("hidden");
|
||||||
$("show-phrase-value").textContent = phrase;
|
$("show-phrase-value").textContent = phrase;
|
||||||
$("show-phrase-result").classList.remove("hidden");
|
$("show-phrase-result").classList.remove("hidden");
|
||||||
$("show-phrase-flash").textContent = "";
|
|
||||||
$("show-phrase-flash").style.visibility = "hidden";
|
|
||||||
} catch {
|
} catch {
|
||||||
if (!isCurrentReveal(generation)) return;
|
if (!isCurrentReveal(generation)) return;
|
||||||
// Deliberately not the caught error: the message is fixed so that
|
// Deliberately not the caught error: the message is fixed so that
|
||||||
// nothing derived from the ciphertext or the attempt can surface.
|
// nothing derived from the ciphertext or the attempt can surface.
|
||||||
fail("That password is incorrect. Please try again.");
|
showError(
|
||||||
|
"show-phrase-password-error",
|
||||||
|
"That password is incorrect. Please try again.",
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.classList.remove("text-muted");
|
btn.classList.remove("text-muted");
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ jest.doMock("../src/popup/views/helpers", () => ({
|
|||||||
$: element,
|
$: element,
|
||||||
showView: () => {},
|
showView: () => {},
|
||||||
showFlash: () => {},
|
showFlash: () => {},
|
||||||
|
showError: () => {},
|
||||||
|
hideError: () => {},
|
||||||
goBack: () => {},
|
goBack: () => {},
|
||||||
clearViewStack: () => {},
|
clearViewStack: () => {},
|
||||||
onViewLeave: () => {},
|
onViewLeave: () => {},
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ describe("reaching the screen", () => {
|
|||||||
const { decryptWithPassword } = require("../src/shared/vault");
|
const { decryptWithPassword } = require("../src/shared/vault");
|
||||||
decryptWithPassword.mockRejectedValue(new Error("nope"));
|
decryptWithPassword.mockRejectedValue(new Error("nope"));
|
||||||
await click("btn-delete-wallet-confirm");
|
await click("btn-delete-wallet-confirm");
|
||||||
expect(node("delete-wallet-flash").textContent).toBe(
|
expect(node("delete-wallet-password-error").textContent).toBe(
|
||||||
"That password is incorrect. Please try again.",
|
"That password is incorrect. Please try again.",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
+109
-6
@@ -809,7 +809,7 @@ async function secretScreenState(page, view) {
|
|||||||
return page.evaluate(
|
return page.evaluate(
|
||||||
(v) => ({
|
(v) => ({
|
||||||
value: document.getElementById(v + "-value").textContent,
|
value: document.getElementById(v + "-value").textContent,
|
||||||
error: document.getElementById(v + "-flash").textContent,
|
error: document.getElementById(v + "-password-error").textContent,
|
||||||
html: document.getElementById("view-" + v).innerHTML,
|
html: document.getElementById("view-" + v).innerHTML,
|
||||||
resultHidden: document
|
resultHidden: document
|
||||||
.getElementById(v + "-result")
|
.getElementById(v + "-result")
|
||||||
@@ -906,7 +906,8 @@ test("a wrong password reveals nothing (#161)", async (env) => {
|
|||||||
await env.page.click("#btn-show-phrase-reveal");
|
await env.page.click("#btn-show-phrase-reveal");
|
||||||
await env.page.waitForFunction(
|
await env.page.waitForFunction(
|
||||||
() =>
|
() =>
|
||||||
document.getElementById("show-phrase-flash").textContent.length > 0,
|
document.getElementById("show-phrase-password-error").textContent
|
||||||
|
.length > 0,
|
||||||
null,
|
null,
|
||||||
{ timeout: 60000 },
|
{ timeout: 60000 },
|
||||||
);
|
);
|
||||||
@@ -1993,13 +1994,14 @@ test("an over-long flash message keeps to one line (#252)", async (env) => {
|
|||||||
|
|
||||||
// Every screen that asks for a password reserves room for one line of error.
|
// Every screen that asks for a password reserves room for one line of error.
|
||||||
// The two on the dApp approval screens also have a border and padding, which
|
// The two on the dApp approval screens also have a border and padding, which
|
||||||
// that reserved height has to cover too.
|
// that reserved height has to cover too. The add wallet screen's line sits
|
||||||
|
// beside its button rather than above it, and has its own test below.
|
||||||
const PASSWORD_ERROR_CONTAINERS = [
|
const PASSWORD_ERROR_CONTAINERS = [
|
||||||
"approve-tx-error",
|
"approve-tx-error",
|
||||||
"approve-sign-error",
|
"approve-sign-error",
|
||||||
"export-privkey-flash",
|
"export-privkey-password-error",
|
||||||
"show-phrase-flash",
|
"show-phrase-password-error",
|
||||||
"delete-wallet-flash",
|
"delete-wallet-password-error",
|
||||||
"confirm-tx-password-error",
|
"confirm-tx-password-error",
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -2064,6 +2066,107 @@ test("a password error moves nothing on any screen (#297)", async (env) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ------------------------------------------ add wallet Import button (#493)
|
||||||
|
|
||||||
|
// At 360x600 the add wallet screen's Import button already starts near the
|
||||||
|
// bottom of the popup, so its password error line sits beside the button
|
||||||
|
// rather than above it. Measures the button and the line empty and again
|
||||||
|
// filled with the longest error addWallet.js puts there. Runs in the page.
|
||||||
|
function measureImportButton() {
|
||||||
|
const button = document.getElementById("btn-add-wallet-confirm");
|
||||||
|
const line = document.getElementById("add-wallet-password-error");
|
||||||
|
const measure = () => {
|
||||||
|
const b = button.getBoundingClientRect();
|
||||||
|
const l = line.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
buttonTop: b.top + window.scrollY,
|
||||||
|
buttonBottom: b.bottom + window.scrollY,
|
||||||
|
lineTop: l.top + window.scrollY,
|
||||||
|
lineBottom: l.bottom + window.scrollY,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const empty = measure();
|
||||||
|
line.textContent = "Password must be at least 12 characters.";
|
||||||
|
line.style.visibility = "visible";
|
||||||
|
const filled = measure();
|
||||||
|
line.textContent = "";
|
||||||
|
line.style.visibility = "hidden";
|
||||||
|
return { empty, filled };
|
||||||
|
}
|
||||||
|
|
||||||
|
test("the add wallet password error leaves Import where it was (#493)", async (env) => {
|
||||||
|
const page = await openPopup(env.ctx, env.popupUrl);
|
||||||
|
try {
|
||||||
|
await page.setViewportSize(POPUP_VIEWPORT);
|
||||||
|
// Brought up by toggling classes, as in the test above. The note
|
||||||
|
// addWallet.js shows once a wallet exists is the only part of the
|
||||||
|
// screen that differs between the first wallet and a later one.
|
||||||
|
await page.evaluate(() => {
|
||||||
|
const screen = document.getElementById("view-add-wallet");
|
||||||
|
for (const view of document.querySelectorAll(".view")) {
|
||||||
|
view.classList.toggle("hidden", view !== screen);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
for (const walletExists of [false, true]) {
|
||||||
|
await page.evaluate(
|
||||||
|
(shown) =>
|
||||||
|
document
|
||||||
|
.getElementById("add-wallet-separate-password-note")
|
||||||
|
.classList.toggle("hidden", !shown),
|
||||||
|
walletExists,
|
||||||
|
);
|
||||||
|
for (const tab of ["tab-mnemonic", "tab-privkey", "tab-xprv"]) {
|
||||||
|
await page.click("#" + tab);
|
||||||
|
const { empty, filled } =
|
||||||
|
await page.evaluate(measureImportButton);
|
||||||
|
const where =
|
||||||
|
"#" +
|
||||||
|
tab +
|
||||||
|
(walletExists
|
||||||
|
? " with a wallet already added"
|
||||||
|
: " for the first wallet");
|
||||||
|
// Printed pass or fail, as the dust threshold test does.
|
||||||
|
console.log(
|
||||||
|
"# add wallet Import top, " +
|
||||||
|
where +
|
||||||
|
": " +
|
||||||
|
empty.buttonTop +
|
||||||
|
"px",
|
||||||
|
);
|
||||||
|
for (const m of [empty, filled]) {
|
||||||
|
assert(
|
||||||
|
m.lineTop >= m.buttonTop &&
|
||||||
|
m.lineBottom <= m.buttonBottom,
|
||||||
|
"the error line on " +
|
||||||
|
where +
|
||||||
|
" does not fit beside Import, so it adds height: " +
|
||||||
|
JSON.stringify(m),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert(
|
||||||
|
filled.buttonTop === empty.buttonTop,
|
||||||
|
"Import moved " +
|
||||||
|
(filled.buttonTop - empty.buttonTop) +
|
||||||
|
"px when the error appeared on " +
|
||||||
|
where,
|
||||||
|
);
|
||||||
|
if (!walletExists) {
|
||||||
|
assert(
|
||||||
|
empty.buttonTop < POPUP_VIEWPORT.height,
|
||||||
|
"Import starts at " +
|
||||||
|
empty.buttonTop +
|
||||||
|
"px on " +
|
||||||
|
where +
|
||||||
|
", below the fold",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await page.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// --------------------------------------------- confirmation screen (#238)
|
// --------------------------------------------- confirmation screen (#238)
|
||||||
//
|
//
|
||||||
// The screen that decides what gets signed. The arithmetic underneath it
|
// The screen that decides what gets signed. The arithmetic underneath it
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ describe("a decrypt still running when the screen is left", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Same hole on the failure path: a wrong-password error written after
|
// Same hole on the failure path: a wrong-password error written after
|
||||||
// the wipe would restore the flash line on a screen the user has left.
|
// the wipe would restore the error line on a screen the user has left.
|
||||||
test("never writes the failure message either", async () => {
|
test("never writes the failure message either", async () => {
|
||||||
const { helpers, vault, exportPrivkey } = load();
|
const { helpers, vault, exportPrivkey } = load();
|
||||||
exportPrivkey.show(0, 0);
|
exportPrivkey.show(0, 0);
|
||||||
@@ -217,8 +217,10 @@ describe("a decrypt still running when the screen is left", () => {
|
|||||||
reveal.reject(new Error("decryption failed"));
|
reveal.reject(new Error("decryption failed"));
|
||||||
await reveal.pending;
|
await reveal.pending;
|
||||||
|
|
||||||
expect(node("export-privkey-flash").textContent).toBe("");
|
expect(node("export-privkey-password-error").textContent).toBe("");
|
||||||
expect(node("export-privkey-flash").style.visibility).toBe("hidden");
|
expect(node("export-privkey-password-error").style.visibility).toBe(
|
||||||
|
"hidden",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -260,7 +262,7 @@ describe("a reveal that is not interrupted", () => {
|
|||||||
await reveal.pending;
|
await reveal.pending;
|
||||||
|
|
||||||
expect(node("export-privkey-value").textContent).toBe("");
|
expect(node("export-privkey-value").textContent).toBe("");
|
||||||
expect(node("export-privkey-flash").textContent).toBe(
|
expect(node("export-privkey-password-error").textContent).toBe(
|
||||||
"That password is incorrect. Please try again.",
|
"That password is incorrect. Please try again.",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,159 @@
|
|||||||
|
// Every screen that asks for a password shows a password error the same way:
|
||||||
|
// through showError() and hideError() in src/popup/views/helpers.js, in a
|
||||||
|
// fixed-height error line below the password field, and never in the flash
|
||||||
|
// line at the top of the popup
|
||||||
|
// (https://git.eeqj.de/sneak/AutistMask/issues/493). These boot the real popup
|
||||||
|
// over src/popup/index.html, so each error line has to exist in the markup,
|
||||||
|
// and check that the error appears in it and clears again.
|
||||||
|
|
||||||
|
jest.mock("../src/shared/vault", () => ({
|
||||||
|
decryptWithPassword: jest.fn(),
|
||||||
|
encryptWithPassword: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const {
|
||||||
|
bootPopup,
|
||||||
|
cleanupPopup,
|
||||||
|
unversionedValidProfile,
|
||||||
|
} = require("./support/popupBoot");
|
||||||
|
|
||||||
|
const PASSWORD = "correct horse battery staple";
|
||||||
|
const WRONG_PASSWORD = "That password is incorrect. Please try again.";
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanupPopup();
|
||||||
|
});
|
||||||
|
|
||||||
|
// The error line as the user sees it.
|
||||||
|
function errorLine(page, id) {
|
||||||
|
return {
|
||||||
|
inMarkup: page.document.authoredIds.has(id),
|
||||||
|
text: page.text(id),
|
||||||
|
visibility: page.node(id).style.visibility,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function shown(text) {
|
||||||
|
return { inMarkup: true, text, visibility: "visible" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleared = { inMarkup: true, text: "", visibility: "hidden" };
|
||||||
|
|
||||||
|
describe("the add wallet screen", () => {
|
||||||
|
const ERROR = "add-wallet-password-error";
|
||||||
|
|
||||||
|
// First run: Welcome, "Add wallet", then the die for a valid phrase.
|
||||||
|
async function openAddWallet() {
|
||||||
|
const page = await bootPopup(undefined);
|
||||||
|
await page.click("btn-welcome-add");
|
||||||
|
await page.click("btn-generate-phrase");
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPasswords(page, password, confirm) {
|
||||||
|
page.node("add-wallet-password").value = password;
|
||||||
|
page.node("add-wallet-password-confirm").value = confirm;
|
||||||
|
}
|
||||||
|
|
||||||
|
test("shows a password problem below the password fields, and clears it on the next press", async () => {
|
||||||
|
const page = await openAddWallet();
|
||||||
|
setPasswords(page, "short", "short");
|
||||||
|
await page.click("btn-add-wallet-confirm");
|
||||||
|
expect(errorLine(page, ERROR)).toEqual(
|
||||||
|
shown("Password must be at least 12 characters."),
|
||||||
|
);
|
||||||
|
expect(page.text("flash-msg")).toBe("");
|
||||||
|
|
||||||
|
// The password is fixed and the phrase emptied: the password error
|
||||||
|
// goes, and the phrase problem is still reported in the flash line.
|
||||||
|
setPasswords(page, PASSWORD, PASSWORD);
|
||||||
|
page.node("wallet-mnemonic").value = "";
|
||||||
|
await page.click("btn-add-wallet-confirm");
|
||||||
|
expect(errorLine(page, ERROR)).toEqual(cleared);
|
||||||
|
expect(page.text("flash-msg")).toBe(
|
||||||
|
"Enter a recovery phrase, or press the die.",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Leaving clears the flash line and stops its timer, which would
|
||||||
|
// otherwise fire after this page is gone.
|
||||||
|
await page.click("btn-add-wallet-back");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("clears the error when the screen is shown again", async () => {
|
||||||
|
const page = await openAddWallet();
|
||||||
|
setPasswords(page, PASSWORD, PASSWORD + " typo");
|
||||||
|
await page.click("btn-add-wallet-confirm");
|
||||||
|
expect(errorLine(page, ERROR)).toEqual(
|
||||||
|
shown("Passwords do not match."),
|
||||||
|
);
|
||||||
|
|
||||||
|
await page.click("btn-add-wallet-back");
|
||||||
|
await page.click("btn-welcome-add");
|
||||||
|
expect(errorLine(page, ERROR)).toEqual(cleared);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe.each([
|
||||||
|
{
|
||||||
|
screen: "the private key export screen",
|
||||||
|
open: () => require("../src/popup/views/exportPrivkey").show(0, 0),
|
||||||
|
field: "export-privkey-password",
|
||||||
|
button: "btn-export-privkey-confirm",
|
||||||
|
error: "export-privkey-password-error",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
screen: "the recovery phrase screen",
|
||||||
|
open: () => require("../src/popup/views/showPhrase").show(0),
|
||||||
|
field: "show-phrase-password",
|
||||||
|
button: "btn-show-phrase-reveal",
|
||||||
|
error: "show-phrase-password-error",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
screen: "the delete wallet screen",
|
||||||
|
open: () => require("../src/popup/views/deleteWallet").show(0),
|
||||||
|
field: "delete-wallet-password",
|
||||||
|
button: "btn-delete-wallet-confirm",
|
||||||
|
error: "delete-wallet-password-error",
|
||||||
|
},
|
||||||
|
])("$screen", ({ open, field, button, error }) => {
|
||||||
|
// Opens the screen and enters a password the vault rejects.
|
||||||
|
async function failedAttempt() {
|
||||||
|
const page = await bootPopup(unversionedValidProfile());
|
||||||
|
open();
|
||||||
|
const { decryptWithPassword } = require("../src/shared/vault");
|
||||||
|
decryptWithPassword.mockRejectedValue(new Error("wrong password"));
|
||||||
|
page.node(field).value = "not the password";
|
||||||
|
await page.click(button);
|
||||||
|
return { page, decryptWithPassword };
|
||||||
|
}
|
||||||
|
|
||||||
|
test("shows a wrong password below the password field", async () => {
|
||||||
|
const { page } = await failedAttempt();
|
||||||
|
expect(errorLine(page, error)).toEqual(shown(WRONG_PASSWORD));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("clears the error while the next password is checked", async () => {
|
||||||
|
const { page, decryptWithPassword } = await failedAttempt();
|
||||||
|
let rejectDecrypt;
|
||||||
|
decryptWithPassword.mockReturnValue(
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
rejectDecrypt = reject;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
page.node(field).value = "another guess";
|
||||||
|
const pressed = page.click(button);
|
||||||
|
await page.settle();
|
||||||
|
expect(errorLine(page, error)).toEqual(cleared);
|
||||||
|
|
||||||
|
rejectDecrypt(new Error("wrong password"));
|
||||||
|
await pressed;
|
||||||
|
expect(errorLine(page, error)).toEqual(shown(WRONG_PASSWORD));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("clears the error when the screen is shown again", async () => {
|
||||||
|
const { page } = await failedAttempt();
|
||||||
|
open();
|
||||||
|
expect(errorLine(page, error)).toEqual(cleared);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user