v1.2.0: UX audit — security fixes, accessibility, and polish

Security (P0):
- Fix sidebar remaining interactive behind lock screen
- Extend auto-lock idle detection to include active widget interactions
- Distinguish missing PIN vault from wrong PIN; auto-switch to passphrase

Blocking UX (P1):
- Add 15s timeout for encryption state check to prevent indefinite loading
- Show restart reason in loading overlay after wallet encryption
- Add Force Quit button on shutdown screen after 10s
- Warn user if embedded daemon fails to start during wizard completion

Polish (P2):
- Use configured explorer URL in Receive tab instead of hardcoded URL
- Increase request memo buffer from 256 to 512 bytes to match Send tab
- Extend notification duration to 5s for critical operations (tx sent,
  wallet encrypted, key import, backup, export)
- Add Reduce Motion accessibility setting (disables page fade + balance lerp)
- Show estimated remaining time during mining thread benchmark
- Add staleness indicator to market price data (warning after 5 min)

New i18n keys: incorrect_pin, incorrect_passphrase, pin_not_set,
restarting_after_encryption, force_quit, reduce_motion, tt_reduce_motion,
ago, wizard_daemon_start_failed
This commit is contained in:
2026-04-04 19:10:58 -05:00
parent bbf53a130c
commit 3ff62ca248
22 changed files with 173 additions and 47 deletions

View File

@@ -69,10 +69,13 @@ void App::encryptWalletWithPassphrase(const std::string& passphrase) {
}
ui::Notifications::instance().info(
"Wallet encrypted successfully");
"Wallet encrypted successfully", 5.0f);
// The daemon shuts itself down after encryptwallet
// The daemon shuts itself down after encryptwallet.
// Update connection_status_ so the loading overlay
// explains why the daemon is restarting.
if (isUsingEmbeddedDaemon()) {
connection_status_ = TR("restarting_after_encryption");
// Give daemon a moment to shut down, then restart
// (do this off the main thread to avoid stalling the UI)
std::thread([this]() {
@@ -164,13 +167,13 @@ void App::processDeferredEncryption() {
if (pinStored) {
settings_->setPinEnabled(true);
settings_->save();
ui::Notifications::instance().info("Wallet encrypted & PIN set");
ui::Notifications::instance().info("Wallet encrypted & PIN set", 5.0f);
} else {
ui::Notifications::instance().warning(
"Wallet encrypted but PIN vault failed");
}
} else {
ui::Notifications::instance().info("Wallet encrypted successfully");
ui::Notifications::instance().info("Wallet encrypted successfully", 5.0f);
}
// Securely clear deferred state
@@ -264,7 +267,7 @@ void App::unlockWallet(const std::string& passphrase, int timeout) {
state_.unlocked_until = std::time(nullptr) + timeout;
} else {
lock_attempts_++;
lock_error_msg_ = "Incorrect passphrase";
lock_error_msg_ = TR("incorrect_passphrase");
lock_error_timer_ = 3.0f;
memset(lock_passphrase_buf_, 0, sizeof(lock_passphrase_buf_));
@@ -788,10 +791,17 @@ void App::renderLockScreen() {
bool vaultOk = vault_ && vault_->retrieve(pin, passphrase);
if (!vaultOk) {
return [this]() {
bool noVault = !vault_ || !vault_->hasVault();
return [this, noVault]() {
lock_unlock_in_progress_ = false;
lock_attempts_++;
lock_error_msg_ = "Incorrect PIN";
if (noVault) {
// Vault file missing — switch to passphrase mode
lock_error_msg_ = TR("pin_not_set");
lock_use_pin_ = false;
} else {
lock_attempts_++;
lock_error_msg_ = TR("incorrect_pin");
}
lock_error_timer_ = 3.0f;
float baseDelay = ui::schema::UI().drawElement("security", "lockout-base-delay").sizeOr(2.0f);