Compare commits
1
Commits
next
...
d5d48d35f8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5d48d35f8 |
@@ -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.
|
||||||
|
- Error line
|
||||||
- "Import" button
|
- "Import" button
|
||||||
- **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**:
|
||||||
|
|||||||
@@ -45,6 +45,18 @@ but the review is broader than any of them.
|
|||||||
|
|
||||||
# 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. 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: The README says that the wallet never clears the clipboard after
|
- 2026-10-07: The README says that the wallet never clears the clipboard after
|
||||||
the private key or the recovery phrase is copied, and why
|
the private key or the recovery phrase is copied, and why
|
||||||
([#492](https://git.eeqj.de/sneak/AutistMask/issues/492)): the clipboard is
|
([#492](https://git.eeqj.de/sneak/AutistMask/issues/492)): the clipboard is
|
||||||
|
|||||||
+18
-14
@@ -207,6 +207,10 @@
|
|||||||
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>
|
||||||
|
<div
|
||||||
|
id="add-wallet-password-error"
|
||||||
|
class="text-xs mb-2 min-h-[1.25rem] invisible"
|
||||||
|
></div>
|
||||||
<button
|
<button
|
||||||
id="btn-add-wallet-confirm"
|
id="btn-add-wallet-confirm"
|
||||||
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
|
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
|
||||||
@@ -396,10 +400,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 +408,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 +1120,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 +1129,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 +1277,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 +1285,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.",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
+7
-5
@@ -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 },
|
||||||
);
|
);
|
||||||
@@ -1997,9 +1998,10 @@ test("an over-long flash message keeps to one line (#252)", async (env) => {
|
|||||||
const PASSWORD_ERROR_CONTAINERS = [
|
const PASSWORD_ERROR_CONTAINERS = [
|
||||||
"approve-tx-error",
|
"approve-tx-error",
|
||||||
"approve-sign-error",
|
"approve-sign-error",
|
||||||
"export-privkey-flash",
|
"add-wallet-password-error",
|
||||||
"show-phrase-flash",
|
"export-privkey-password-error",
|
||||||
"delete-wallet-flash",
|
"show-phrase-password-error",
|
||||||
|
"delete-wallet-password-error",
|
||||||
"confirm-tx-password-error",
|
"confirm-tx-password-error",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -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