diff --git a/src/app.cpp b/src/app.cpp index 6dfdbf3..098a427 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -4654,6 +4654,12 @@ void App::renderSeedBackupDialog() seed_backup_loading_ = false; if (ok) { seed_backup_phrase_ = phrase; // working copy — wiped on close + // The user is now looking at their seed via the dedicated backup flow — treat that as + // backed up, so the periodic nudge (maybeRemindSeedBackup) stops for good. + if (settings_ && !settings_->getSeedBackupReminded()) { + settings_->setSeedBackupReminded(true); + settings_->save(); + } } else if (noMnemonic) { seed_backup_no_mnemonic_ = true; } else { diff --git a/src/app.h b/src/app.h index 8083024..5c13184 100644 --- a/src/app.h +++ b/src/app.h @@ -1406,6 +1406,8 @@ private: bool seed_backup_loading_ = false; bool seed_backup_no_mnemonic_ = false; bool seed_backup_reminder_in_flight_ = false; // guards the one-time backup nudge probe + bool seed_backup_nudged_this_session_ = false; // nudged (or definitively classified) once this session; + // unlike the persisted "reminded" flag it re-nudges next run std::chrono::steady_clock::time_point seed_backup_reminder_retry_at_{}; // after an inconclusive probe bool large_wallet_checked_ = false; // gate: stat wallet.dat for the bloat nudge once per launch diff --git a/src/app_network.cpp b/src/app_network.cpp index 99b01c4..c9b5ad7 100644 --- a/src/app_network.cpp +++ b/src/app_network.cpp @@ -5833,7 +5833,8 @@ void App::maybeRemindSeedBackup() if (capture_mode_) return; // no live ops during a UI sweep if (window_background_) return; // when someone can see it if (lite_wallet_) return; // lite has its own seed UX - if (!settings_ || settings_->getSeedBackupReminded()) return; + if (!settings_ || settings_->getSeedBackupReminded()) return; // already backed up (saw the phrase) + if (seed_backup_nudged_this_session_) return; // already nudged/classified this run if (!state_.connected || !state_.encryption_state_known) return; if (state_.isLocked()) return; // wait until unlocked to read it if (seed_backup_reminder_in_flight_) return; @@ -5844,16 +5845,29 @@ void App::maybeRemindSeedBackup() exportSeedPhrase([this](bool ok, bool noMnemonic, const std::string& /*phrase*/, const std::string& /*error*/) { seed_backup_reminder_in_flight_ = false; - // Only mark "reminded" once we have a definitive answer, so a transient failure retries - // next session rather than silently suppressing the reminder forever. - if (ok || noMnemonic) { + if (ok) { + // The wallet HAS a mnemonic and the user hasn't backed it up yet. Don't mark "reminded" + // here — that permanent flag now means "the user has seen the phrase" (set from the backup + // dialog), so leaving it clear re-nudges next session until they actually do. Nudge once + // per session as an ACTIONABLE alert (survives in the bell panel with a "Back up now" link), + // not a 12s toast that's gone before it's read. Escalate to a warning once funds exist. + seed_backup_nudged_this_session_ = true; + const bool hasFunds = state_.totalBalance > 0.0; + ui::Notifications::instance().action( + TR("seed_backup_reminder"), + hasFunds ? ui::NotificationType::Warning : ui::NotificationType::Info, + [this]() { showSeedBackupDialog(); }, + TR("seed_backup_reminder_action"), + hasFunds ? 15.0f : 12.0f); + } else if (noMnemonic) { + // A legacy (non-mnemonic) wallet has nothing to back up this way — suppress for good. + seed_backup_nudged_this_session_ = true; settings_->setSeedBackupReminded(true); settings_->save(); } else { + // Inconclusive (busy/old daemon) — retry later this session rather than suppressing forever. seed_backup_reminder_retry_at_ = std::chrono::steady_clock::now() + std::chrono::minutes(5); } - if (ok) - ui::Notifications::instance().info(TR("seed_backup_reminder"), 12.0f); }); } diff --git a/src/ui/windows/console_tab.cpp b/src/ui/windows/console_tab.cpp index 217a9d2..bc65c1e 100644 --- a/src/ui/windows/console_tab.cpp +++ b/src/ui/windows/console_tab.cpp @@ -1537,6 +1537,25 @@ void ConsoleTab::renderInput(ConsoleCommandExecutor& exec) focus_input_pending_ = false; } +namespace { +// Is `firstLower` (a lowercased command name) flagged destructive in the command reference? The +// `destructive` flag was previously consulted only by the reference dialog's Run button; this lets the +// typed/pasted path honor it too. Scans the full-node or lite command set per the active executor. +bool IsDestructiveConsoleCommand(const std::string& firstLower, bool fullNode) +{ + const auto& cats = fullNode ? consoleCommandCategories() : liteConsoleCommandCategories(); + for (const auto& cat : cats) { + for (int i = 0; i < cat.count; ++i) { + std::string n = cat.commands[i].name ? cat.commands[i].name : ""; + std::transform(n.begin(), n.end(), n.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + if (n == firstLower) return cat.commands[i].destructive; + } + } + return false; +} +} // namespace + bool ConsoleTab::submitConsoleCommand(ConsoleCommandExecutor& exec, const std::string& cmd) { if (cmd.empty()) return false; @@ -1561,6 +1580,8 @@ bool ConsoleTab::submitConsoleCommand(ConsoleCommandExecutor& exec, const std::s // pending state. Member (not a function-local static) so clear() can also cancel it — otherwise a // toolbar/context-menu clear between the two 'stop's would leave a stale arm and skip the warning. if (first != "stop") stop_confirm_pending_ = false; + // A pending destructive-command confirmation is armed on the EXACT line; any different input cancels it. + if (destructive_confirm_cmd_ != cmd) destructive_confirm_cmd_.clear(); auto add = [this](const std::string& l, ConsoleChannel c) { addLine(l, c); }; if (first == "clear" || first == "cls") { // View-only clear — NEVER forwarded (the lite backend's `clear` wipes tx history). @@ -1586,7 +1607,15 @@ bool ConsoleTab::submitConsoleCommand(ConsoleCommandExecutor& exec, const std::s // Full node connects to a daemon; the lite backend opens a wallet — word the error per variant. addLine(exec.hasRpcReference() ? TR("console_not_connected") : TR("console_not_connected_lite"), ConsoleChannel::Error); + } else if (IsDestructiveConsoleCommand(first, exec.hasRpcReference()) && + destructive_confirm_cmd_ != cmd) { + // Fund-moving / key-revealing commands (dumpprivkey, z_sendmany, z_mergetoaddress, …) get the + // same two-step confirm as 'stop' when typed or PASTED directly — previously the destructive + // flag was honored only by the reference dialog, so a pasted command ran instantly. + destructive_confirm_cmd_ = cmd; + addLine(TR("console_destructive_confirm"), ConsoleChannel::Warning); } else { + destructive_confirm_cmd_.clear(); exec.submit(cmd); } return true; @@ -2185,6 +2214,7 @@ void ConsoleTab::clear() wrap_cache_.reset(); vis_flat_valid_ = false; stop_confirm_pending_ = false; // a pending 'stop' confirmation is cancelled by clearing + destructive_confirm_cmd_.clear(); // likewise cancel a pending destructive-command confirmation addLine(TR("console_cleared"), ConsoleChannel::Info); } diff --git a/src/ui/windows/console_tab.h b/src/ui/windows/console_tab.h index c4dbf7d..6a02515 100644 --- a/src/ui/windows/console_tab.h +++ b/src/ui/windows/console_tab.h @@ -161,6 +161,7 @@ private: int history_index_ = -1; char input_buffer_[4096] = {0}; bool stop_confirm_pending_ = false; // 'stop' typed once, awaiting a confirming second 'stop' + std::string destructive_confirm_cmd_; // a destructive command typed once, awaiting an identical re-entry bool focus_input_pending_ = false; // one-shot: focus the command input next render (tab-open auto-focus) // (log-ingestion cursors + result queue moved to the ConsoleCommandExecutor) diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index a1c1e6d..556504d 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -357,6 +357,7 @@ void I18n::loadBuiltinEnglish() strings_["seed_backup_save_failed"] = "Could not write "; strings_["seed_backup_close"] = "Close"; strings_["seed_backup_reminder"] = "Your wallet has a 24-word recovery seed phrase. Back it up now in Settings → Node & Security."; + strings_["seed_backup_reminder_action"] = "Back up now"; strings_["seed_migrate_button"] = "Migrate to seed…"; // Multi-wallet: wallet-files list + switcher strings_["wallets_button"] = "Wallets…"; @@ -1908,6 +1909,7 @@ void I18n::loadBuiltinEnglish() strings_["console_last_error"] = "Last error:"; strings_["console_not_connected_lite"] = "Error: no wallet open"; strings_["console_stop_confirm_node"] = "'stop' will shut down the node and disconnect the wallet. Type 'stop' again to confirm."; + strings_["console_destructive_confirm"] = "This command can move funds or reveal secret keys. Never run a command someone else gave you. Run the exact same command again to confirm."; strings_["console_capturing_output"] = "Capturing daemon output..."; strings_["console_clear"] = "Clear"; strings_["console_clear_console"] = "Clear Console";