From aa587d1d84dd997bf678ead7d5f27c1aa3db608e Mon Sep 17 00:00:00 2001 From: DanS Date: Fri, 10 Jul 2026 18:24:41 -0500 Subject: [PATCH] feat(seed): glow the Migrate-to-seed button for a legacy wallet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A pre-seed-phrase (legacy, non-mnemonic) wallet is exactly the one that benefits from migrating — so nudge the user by glowing the Settings "Migrate to seed" button with a soft pulsing accent halo. Adds a cached wallet mnemonic status (probeWalletSeedStatus), classified once per connect via z_exportmnemonic (the same signal the migration Intro pre-flight uses): NoMnemonic = legacy → glow; HasMnemonic / Incapable (old daemon) / while locked or on lite = no glow. Reset on disconnect so it re-probes after a wallet switch or a post-migration adopt, with a small attempt cap to avoid re-probing on a persistent transient error. The sweep forces the status so the glow is captured (restored after). Verified dark + light. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/app.cpp | 4 ++++ src/app.h | 13 +++++++++++++ src/app_network.cpp | 34 ++++++++++++++++++++++++++++++++++ src/app_sweep.cpp | 3 +++ src/ui/pages/settings_page.cpp | 16 +++++++++++++++- 5 files changed, 69 insertions(+), 1 deletion(-) diff --git a/src/app.cpp b/src/app.cpp index d14504f..2ecb6a8 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -742,6 +742,10 @@ void App::update() // One-time reminder to back up the wallet's seed phrase (mnemonic wallets only). maybeRemindSeedBackup(); + // Classify the wallet's mnemonic status (once per connect) so the Migrate-to-seed button can + // glow for a legacy, pre-seed-phrase wallet. + probeWalletSeedStatus(); + // Pick up progress/result from a running seed-wallet migration (create/sweep/adopt). pumpSeedMigration(); // While confirming the sweep, poll the tx confirmations + legacy balance every ~5s. diff --git a/src/app.h b/src/app.h index b74f72c..aa41c54 100644 --- a/src/app.h +++ b/src/app.h @@ -368,6 +368,9 @@ public: void showBackupDialog() { show_backup_ = true; } void showSeedBackupDialog() { show_seed_backup_ = true; } void showSeedMigrationDialog(); // opens the migration modal (resumes a pending one at Sweep) + // True when the current full-node wallet is a legacy, pre-seed-phrase wallet (no BIP39 mnemonic) + // that a capable daemon could migrate — the Migrate-to-seed button glows to nudge the user. + bool isPreSeedWallet() const { return wallet_seed_status_ == WalletSeedStatus::NoMnemonic; } void showAboutDialog() { show_about_ = true; } // Legacy tab compat — maps int to NavPage @@ -711,6 +714,16 @@ private: bool seed_backup_no_mnemonic_ = false; bool seed_backup_reminder_in_flight_ = false; // guards the one-time backup nudge probe + // Cached mnemonic status of the current wallet, driving the Migrate-to-seed button glow. Probed + // once per connect (probeWalletSeedStatus, via exportSeedPhrase); NoMnemonic = a legacy wallet a + // capable daemon can migrate; Incapable = the daemon lacks z_exportmnemonic (can't tell / can't + // migrate). Reset to Unknown on wallet switch so it re-probes the new wallet. + enum class WalletSeedStatus { Unknown, HasMnemonic, NoMnemonic, Incapable }; + WalletSeedStatus wallet_seed_status_ = WalletSeedStatus::Unknown; + bool wallet_seed_status_in_flight_ = false; + int wallet_seed_status_attempts_ = 0; // give up (Incapable) after a few transient probe failures + void probeWalletSeedStatus(); // one-shot per connect; classifies the wallet's mnemonic status + // --- Seed-wallet migration (Phase 1: create; Phase 2: sweep + adopt) --- enum class SeedMigrationStep { Intro, Working, ShowSeed, Sweep, Sweeping, Confirming, Adopting, Done, Error }; bool show_seed_migration_ = false; diff --git a/src/app_network.cpp b/src/app_network.cpp index 3e856b7..da01a66 100644 --- a/src/app_network.cpp +++ b/src/app_network.cpp @@ -579,6 +579,11 @@ void App::onDisconnected(const std::string& reason) state_.clear(); connection_status_ = reason; + // Re-classify the wallet's mnemonic status on the next connect (the active wallet may have + // changed — a switch or a post-migration adopt both disconnect here). + wallet_seed_status_ = WalletSeedStatus::Unknown; + wallet_seed_status_attempts_ = 0; + // Clear RPC result caches viewtx_cache_.clear(); confirmed_tx_cache_.clear(); @@ -2936,6 +2941,35 @@ void App::maybeRemindSeedBackup() }); } +// One-shot (per connect) probe of the current wallet's mnemonic status, so the Settings +// Migrate-to-seed button can glow for a legacy wallet without opening the migration dialog. Same +// classification as the migration Intro pre-flight, but proactive and cached. Reads no secret past +// the exportSeedPhrase callback (which wipes the phrase). Retries next tick on a transient failure. +void App::probeWalletSeedStatus() +{ + if (capture_mode_) return; // no live ops during a UI sweep + if (lite_wallet_) return; // lite has its own seed UX + if (wallet_seed_status_ != WalletSeedStatus::Unknown) return; // already classified + if (!state_.connected || !state_.encryption_state_known) return; + if (state_.isLocked()) return; // needs an unlocked wallet to read it + if (wallet_seed_status_in_flight_) return; + wallet_seed_status_in_flight_ = true; + exportSeedPhrase([this](bool ok, bool noMnemonic, const std::string& /*phrase*/, + const std::string& error) { + wallet_seed_status_in_flight_ = false; + if (ok) + wallet_seed_status_ = WalletSeedStatus::HasMnemonic; + else if (noMnemonic) + wallet_seed_status_ = WalletSeedStatus::NoMnemonic; // legacy → migratable → glow + else if (error.find("Method not found") != std::string::npos || + error.find("-32601") != std::string::npos) + wallet_seed_status_ = WalletSeedStatus::Incapable; // old daemon: can't migrate + else if (++wallet_seed_status_attempts_ >= 3) + wallet_seed_status_ = WalletSeedStatus::Incapable; // give up after a few transient errors + // else: transient error → stay Unknown, retry next tick + }); +} + void App::beginCreateSeedWallet() { if (seed_migration_in_flight_) return; diff --git a/src/app_sweep.cpp b/src/app_sweep.cpp index 840ec56..17b9f7f 100644 --- a/src/app_sweep.cpp +++ b/src/app_sweep.cpp @@ -113,6 +113,8 @@ void App::installDemoWalletData() applyHealthyDemoState(); state_.sync.blocks = state_.sync.headers = 3124322; state_.sync.verification_progress = 1.0; state_.sync.syncing = false; + // Legacy (pre-seed) wallet so the Settings "Migrate to seed" button glows in the sweep. + wallet_seed_status_ = WalletSeedStatus::NoMnemonic; state_.privateBalance = 12.50000000; state_.transparentBalance = 3.25000000; state_.totalBalance = 15.75000000; state_.unconfirmedBalance = 0.50000000; @@ -210,6 +212,7 @@ void App::clearDemoWalletData() { auto& s = sweep_state_snapshot_; if (!s.valid) return; + wallet_seed_status_ = WalletSeedStatus::Unknown; // re-probe on the next real connect state_.connected = s.connected; state_.warming_up = s.warming_up; state_.daemon_initializing = s.daemon_initializing; state_.encrypted = s.encrypted; state_.locked = s.locked; diff --git a/src/ui/pages/settings_page.cpp b/src/ui/pages/settings_page.cpp index fdcd8d9..ccc4c99 100644 --- a/src/ui/pages/settings_page.cpp +++ b/src/ui/pages/settings_page.cpp @@ -1336,10 +1336,24 @@ void RenderSettingsPage(App* app) { if (TactileButton(TR("seed_backup_button"), ImVec2(0, 0), btnFont)) app->showSeedBackupDialog(); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_seed_backup")); - // Migrate to a new seed-phrase wallet (create in isolation, then sweep funds). + // Migrate to a new seed-phrase wallet (create in isolation, then sweep funds). A + // legacy (pre-seed-phrase) wallet glows the button to nudge the user toward migrating. ImGui::SameLine(0, scaledSp); + const bool migrateGlow = app->isPreSeedWallet(); if (TactileButton(TR("seed_migrate_button"), ImVec2(0, 0), btnFont)) app->showSeedMigrationDialog(); + if (migrateGlow) { + const ImVec2 gmn = ImGui::GetItemRectMin(), gmx = ImGui::GetItemRectMax(); + const float gdp = Layout::dpiScale(); + const float pulse = 0.5f + 0.5f * std::sin((float)ImGui::GetTime() * 3.2f); // 0..1 + ImDrawList* gdl = ImGui::GetWindowDrawList(); + for (int g = 3; g >= 1; --g) { // soft, pulsing outward halo in the accent color + const float e = (float)g * 2.2f * gdp; + const int a = (int)((70.0f + pulse * 95.0f) / (float)g); + gdl->AddRect(ImVec2(gmn.x - e, gmn.y - e), ImVec2(gmx.x + e, gmx.y + e), + material::WithAlpha(material::Primary(), a), 6.0f * gdp + e, 0, 1.6f * gdp); + } + } if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_seed_migrate")); // Multi-wallet: list wallet files + switch the active one. ImGui::SameLine(0, scaledSp);