feat(seed): glow the Migrate-to-seed button for a legacy wallet

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) <noreply@anthropic.com>
This commit is contained in:
2026-07-10 18:24:41 -05:00
parent 3a9edf2200
commit aa587d1d84
5 changed files with 69 additions and 1 deletions

View File

@@ -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.

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);