feat(wallet): live progress modal for wallet switching
The "Stop the running node?" confirm modal now stays open through the entire switch — stop → wait-for-exit → start → reconnect — showing a live phase, and auto-closes the moment the new node connects. This turns the up-to-a-minute graceful-shutdown wait from an apparent freeze into visible progress. - WalletSwitchPhase (Stopping/Starting/Reconnecting/Failed) + atomic phase and dialog-open flags; the worker advances the phase, onConnected closes the modal, and a failed switch shows the accurate reason with a Close button. - Owned switches (no confirm) also show the progress modal directly. - "Continue in background" escape hatch so a long rescan / a hung startup never traps the user (the switch keeps running; a toast reports the result). - isWalletSwitchInProgress() keeps the frame loop redrawing so the phase text and spinner animate while otherwise idle. - i18n (EN + 8 languages, additive) + a modal-switch-progress sweep surface. State machine adversarially verified 6/6 (thread-safety, no stuck modal, confirm→progress transition, owned/unowned, no phase leak, redraw scoping). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -504,7 +504,13 @@ void App::tryConnect()
|
||||
void App::onConnected()
|
||||
{
|
||||
state_.connected = true;
|
||||
wallet_switch_pending_confirm_.store(false); // a successful connect confirms the switch
|
||||
const bool completedSwitch = wallet_switch_pending_confirm_.exchange(false); // this connect confirms a switch
|
||||
// A successful connect completes a switch — close/clear the progress modal (kept open through the
|
||||
// stop → start → reconnect sequence, or already hidden via "continue in background").
|
||||
if (completedSwitch || wallet_switch_dialog_open_.load()) {
|
||||
wallet_switch_dialog_open_.store(false);
|
||||
wallet_switch_phase_.store(static_cast<int>(WalletSwitchPhase::None));
|
||||
}
|
||||
state_.daemon_initializing = false; // RPC is answering now; clear the "initializing" overlay
|
||||
daemon_wait_attempts_ = 0; // re-arm the port-busy / start-failure notifications
|
||||
daemon_start_error_shown_ = false;
|
||||
@@ -1104,9 +1110,11 @@ void App::switchToWallet(const std::string& walletFile, bool stopDaemonConfirmed
|
||||
// can't surface (or sign outgoing chat) under the new wallet. It re-provisions from the new
|
||||
// wallet's seed once it connects. onDisconnected alone doesn't cover chat.
|
||||
resetChatSession();
|
||||
ui::Notifications::instance().info(
|
||||
"Switching wallet — stopping the node and restarting on the new wallet. "
|
||||
"A graceful shutdown can take up to a minute.", 60.0f);
|
||||
// Open the switch progress modal — it stays up through stop → wait-for-exit → start → reconnect and
|
||||
// auto-closes when the new node connects (onConnected). The worker advances the phase from here.
|
||||
wallet_switch_error_.clear();
|
||||
wallet_switch_phase_.store(static_cast<int>(WalletSwitchPhase::Stopping));
|
||||
wallet_switch_dialog_open_.store(true);
|
||||
|
||||
async_tasks_.submit("Switch wallet", [this, needRescan](const util::AsyncTaskManager::Token&) {
|
||||
bool ok = false;
|
||||
@@ -1126,6 +1134,7 @@ void App::switchToWallet(const std::string& walletFile, bool stopDaemonConfirmed
|
||||
} else {
|
||||
// The relaunch is ours — clear the adopted-external latch so the fresh process is treated
|
||||
// as owned (stop/isRunning/exit behave normally for the next switch and app exit).
|
||||
wallet_switch_phase_.store(static_cast<int>(WalletSwitchPhase::Starting));
|
||||
if (daemon_controller_) daemon_controller_->clearExternalDaemonDetected();
|
||||
if (needRescan && daemon_controller_) daemon_controller_->setRescanOnNextStart(true);
|
||||
ok = shutting_down_ ? true : startEmbeddedDaemon();
|
||||
@@ -1135,6 +1144,8 @@ void App::switchToWallet(const std::string& walletFile, bool stopDaemonConfirmed
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1500));
|
||||
if (!isEmbeddedDaemonRunning()) ok = false;
|
||||
}
|
||||
// Process is up and staying up — now waiting for RPC to answer (onConnected closes the modal).
|
||||
if (ok) wallet_switch_phase_.store(static_cast<int>(WalletSwitchPhase::Reconnecting));
|
||||
}
|
||||
} catch (...) {
|
||||
ok = false; // a throw during stop/start is a failed switch — revert rather than wedge
|
||||
@@ -1167,12 +1178,17 @@ void App::processWalletSwitchRevert()
|
||||
if (daemon_controller_) daemon_controller_->resetCrashCount();
|
||||
// Accurate reason: "port wouldn't free" (the running node kept the connection) vs. a genuinely bad
|
||||
// target wallet. The former is the adopted-external-daemon case; the latter a daemon that exited in init.
|
||||
if (switch_stop_failed_.exchange(false)) {
|
||||
ui::Notifications::instance().error(
|
||||
"Couldn't switch wallets — the running node didn't release its connection in time. "
|
||||
"It's still on the previous wallet.");
|
||||
const std::string reason = switch_stop_failed_.exchange(false)
|
||||
? std::string("Couldn't switch wallets — the running node didn't release its connection in time. "
|
||||
"It's still on the previous wallet.")
|
||||
: std::string("Couldn't open that wallet — reverted to the previous one.");
|
||||
// Surface it in the progress modal if it's still up (keep it open on Failed until the user closes);
|
||||
// otherwise (the user chose "continue in background") fall back to a toast.
|
||||
if (wallet_switch_dialog_open_.load()) {
|
||||
wallet_switch_error_ = reason;
|
||||
wallet_switch_phase_.store(static_cast<int>(WalletSwitchPhase::Failed));
|
||||
} else {
|
||||
ui::Notifications::instance().error("Couldn't open that wallet — reverted to the previous one.");
|
||||
ui::Notifications::instance().error(reason);
|
||||
}
|
||||
daemon_restarting_ = false; // the connect loop now restarts the previous wallet's daemon
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user