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:
2026-07-15 20:56:53 -05:00
parent 02839dc415
commit 88091bd77c
14 changed files with 161 additions and 29 deletions

View File

@@ -1294,6 +1294,13 @@
"sweep_to": "Gefegt an:", "sweep_to": "Gefegt an:",
"sweep_toggle": "In meine Wallet fegen (Schlüssel nicht behalten)", "sweep_toggle": "In meine Wallet fegen (Schlüssel nicht behalten)",
"sweep_tx": "Transaktion:", "sweep_tx": "Transaktion:",
"switch_progress_background": "Im Hintergrund fortsetzen",
"switch_progress_failed_title": "Wallet-Wechsel fehlgeschlagen",
"switch_progress_hint": "Ein sauberes Herunterfahren kann bis zu einer Minute dauern.",
"switch_progress_reconnecting": "Neu verbinden",
"switch_progress_starting": "Knoten wird mit der neuen Wallet gestartet",
"switch_progress_stopping": "Aktueller Knoten wird gestoppt",
"switch_progress_title": "Wallet wird gewechselt",
"switch_stopnode_body": "Beim Wallet-Wechsel wird der Knoten mit der ausgewählten Wallet neu gestartet. Der laufende Knoten wird gestoppt und mit der neuen Wallet neu gestartet wenn Sie ihn absichtlich laufen ließen, startet er automatisch wieder.", "switch_stopnode_body": "Beim Wallet-Wechsel wird der Knoten mit der ausgewählten Wallet neu gestartet. Der laufende Knoten wird gestoppt und mit der neuen Wallet neu gestartet wenn Sie ihn absichtlich laufen ließen, startet er automatisch wieder.",
"switch_stopnode_confirm": "Knoten stoppen & wechseln", "switch_stopnode_confirm": "Knoten stoppen & wechseln",
"switch_stopnode_title": "Laufenden Knoten stoppen?", "switch_stopnode_title": "Laufenden Knoten stoppen?",

View File

@@ -1294,6 +1294,13 @@
"sweep_to": "Barrido a:", "sweep_to": "Barrido a:",
"sweep_toggle": "Barrer a mi monedero (no conservar la clave)", "sweep_toggle": "Barrer a mi monedero (no conservar la clave)",
"sweep_tx": "Transacción:", "sweep_tx": "Transacción:",
"switch_progress_background": "Continuar en segundo plano",
"switch_progress_failed_title": "Error al cambiar de cartera",
"switch_progress_hint": "Un apagado ordenado puede tardar hasta un minuto.",
"switch_progress_reconnecting": "Reconectando",
"switch_progress_starting": "Iniciando el nodo con la nueva cartera",
"switch_progress_stopping": "Deteniendo el nodo actual",
"switch_progress_title": "Cambiando de cartera",
"switch_stopnode_body": "Cambiar de cartera reinicia el nodo con la cartera seleccionada. El nodo en ejecución se detendrá y se reiniciará con la nueva cartera; si lo dejaste en marcha a propósito, volverá a iniciarse automáticamente.", "switch_stopnode_body": "Cambiar de cartera reinicia el nodo con la cartera seleccionada. El nodo en ejecución se detendrá y se reiniciará con la nueva cartera; si lo dejaste en marcha a propósito, volverá a iniciarse automáticamente.",
"switch_stopnode_confirm": "Detener nodo y cambiar", "switch_stopnode_confirm": "Detener nodo y cambiar",
"switch_stopnode_title": "¿Detener el nodo en ejecución?", "switch_stopnode_title": "¿Detener el nodo en ejecución?",

View File

@@ -1294,6 +1294,13 @@
"sweep_to": "Balayé vers :", "sweep_to": "Balayé vers :",
"sweep_toggle": "Balayer vers mon portefeuille (ne pas conserver la clé)", "sweep_toggle": "Balayer vers mon portefeuille (ne pas conserver la clé)",
"sweep_tx": "Transaction :", "sweep_tx": "Transaction :",
"switch_progress_background": "Continuer en arrière-plan",
"switch_progress_failed_title": "Échec du changement de portefeuille",
"switch_progress_hint": "Un arrêt propre peut prendre jusqu'à une minute.",
"switch_progress_reconnecting": "Reconnexion",
"switch_progress_starting": "Démarrage du nœud sur le nouveau portefeuille",
"switch_progress_stopping": "Arrêt du nœud actuel",
"switch_progress_title": "Changement de portefeuille",
"switch_stopnode_body": "Changer de portefeuille redémarre le nœud sur le portefeuille sélectionné. Le nœud en cours sera arrêté puis relancé sur le nouveau portefeuille — si vous l'avez laissé tourner exprès, il redémarre automatiquement.", "switch_stopnode_body": "Changer de portefeuille redémarre le nœud sur le portefeuille sélectionné. Le nœud en cours sera arrêté puis relancé sur le nouveau portefeuille — si vous l'avez laissé tourner exprès, il redémarre automatiquement.",
"switch_stopnode_confirm": "Arrêter le nœud et changer", "switch_stopnode_confirm": "Arrêter le nœud et changer",
"switch_stopnode_title": "Arrêter le nœud en cours d'exécution ?", "switch_stopnode_title": "Arrêter le nœud en cours d'exécution ?",

View File

@@ -1294,6 +1294,13 @@
"sweep_to": "集約先:", "sweep_to": "集約先:",
"sweep_toggle": "ウォレットに集約(鍵は保持しない)", "sweep_toggle": "ウォレットに集約(鍵は保持しない)",
"sweep_tx": "取引:", "sweep_tx": "取引:",
"switch_progress_background": "バックグラウンドで続行",
"switch_progress_failed_title": "ウォレットの切り替えに失敗しました",
"switch_progress_hint": "正常なシャットダウンには最大1分かかることがあります。",
"switch_progress_reconnecting": "再接続しています",
"switch_progress_starting": "新しいウォレットでノードを起動しています",
"switch_progress_stopping": "現在のノードを停止しています",
"switch_progress_title": "ウォレットを切り替え中",
"switch_stopnode_body": "ウォレットを切り替えると、選択したウォレットでノードが再起動します。実行中のノードは停止され、新しいウォレットで再起動されます。意図的に起動したままにしていた場合は、自動的に復帰します。", "switch_stopnode_body": "ウォレットを切り替えると、選択したウォレットでノードが再起動します。実行中のノードは停止され、新しいウォレットで再起動されます。意図的に起動したままにしていた場合は、自動的に復帰します。",
"switch_stopnode_confirm": "ノードを停止して切り替え", "switch_stopnode_confirm": "ノードを停止して切り替え",
"switch_stopnode_title": "実行中のノードを停止しますか?", "switch_stopnode_title": "実行中のノードを停止しますか?",

View File

@@ -1294,6 +1294,13 @@
"sweep_to": "쓸어담은 주소:", "sweep_to": "쓸어담은 주소:",
"sweep_toggle": "내 지갑으로 쓸어담기 (키 보관 안 함)", "sweep_toggle": "내 지갑으로 쓸어담기 (키 보관 안 함)",
"sweep_tx": "거래:", "sweep_tx": "거래:",
"switch_progress_background": "백그라운드에서 계속",
"switch_progress_failed_title": "지갑 전환 실패",
"switch_progress_hint": "정상 종료에는 최대 1분이 걸릴 수 있습니다.",
"switch_progress_reconnecting": "다시 연결하는 중",
"switch_progress_starting": "새 지갑으로 노드를 시작하는 중",
"switch_progress_stopping": "현재 노드를 중지하는 중",
"switch_progress_title": "지갑 전환 중",
"switch_stopnode_body": "지갑을 전환하면 선택한 지갑으로 노드가 다시 시작됩니다. 실행 중인 노드가 중지되고 새 지갑으로 다시 시작됩니다. 일부러 계속 실행해 두었다면 자동으로 다시 켜집니다.", "switch_stopnode_body": "지갑을 전환하면 선택한 지갑으로 노드가 다시 시작됩니다. 실행 중인 노드가 중지되고 새 지갑으로 다시 시작됩니다. 일부러 계속 실행해 두었다면 자동으로 다시 켜집니다.",
"switch_stopnode_confirm": "노드 중지 후 전환", "switch_stopnode_confirm": "노드 중지 후 전환",
"switch_stopnode_title": "실행 중인 노드를 중지할까요?", "switch_stopnode_title": "실행 중인 노드를 중지할까요?",

View File

@@ -1294,6 +1294,13 @@
"sweep_to": "Varrido para:", "sweep_to": "Varrido para:",
"sweep_toggle": "Varrer para minha carteira (não manter a chave)", "sweep_toggle": "Varrer para minha carteira (não manter a chave)",
"sweep_tx": "Transação:", "sweep_tx": "Transação:",
"switch_progress_background": "Continuar em segundo plano",
"switch_progress_failed_title": "Falha ao trocar de carteira",
"switch_progress_hint": "Um desligamento normal pode levar até um minuto.",
"switch_progress_reconnecting": "Reconectando",
"switch_progress_starting": "Iniciando o nó com a nova carteira",
"switch_progress_stopping": "Parando o nó atual",
"switch_progress_title": "Trocando de carteira",
"switch_stopnode_body": "Trocar de carteira reinicia o nó com a carteira selecionada. O nó em execução será parado e reiniciado com a nova carteira — se você o deixou em execução de propósito, ele volta automaticamente.", "switch_stopnode_body": "Trocar de carteira reinicia o nó com a carteira selecionada. O nó em execução será parado e reiniciado com a nova carteira — se você o deixou em execução de propósito, ele volta automaticamente.",
"switch_stopnode_confirm": "Parar nó e trocar", "switch_stopnode_confirm": "Parar nó e trocar",
"switch_stopnode_title": "Parar o nó em execução?", "switch_stopnode_title": "Parar o nó em execução?",

View File

@@ -1294,6 +1294,13 @@
"sweep_to": "Переведено на:", "sweep_to": "Переведено на:",
"sweep_toggle": "Перевести в мой кошелёк (не сохранять ключ)", "sweep_toggle": "Перевести в мой кошелёк (не сохранять ключ)",
"sweep_tx": "Транзакция:", "sweep_tx": "Транзакция:",
"switch_progress_background": "Продолжить в фоне",
"switch_progress_failed_title": "Не удалось переключить кошелёк",
"switch_progress_hint": "Корректное завершение работы может занять до минуты.",
"switch_progress_reconnecting": "Переподключение",
"switch_progress_starting": "Запуск узла с новым кошельком",
"switch_progress_stopping": "Остановка текущего узла",
"switch_progress_title": "Переключение кошелька",
"switch_stopnode_body": "Смена кошелька перезапускает узел с выбранным кошельком. Запущенный узел будет остановлен и перезапущен с новым кошельком — если вы намеренно оставили его работать, он запустится снова автоматически.", "switch_stopnode_body": "Смена кошелька перезапускает узел с выбранным кошельком. Запущенный узел будет остановлен и перезапущен с новым кошельком — если вы намеренно оставили его работать, он запустится снова автоматически.",
"switch_stopnode_confirm": "Остановить узел и переключить", "switch_stopnode_confirm": "Остановить узел и переключить",
"switch_stopnode_title": "Остановить запущенный узел?", "switch_stopnode_title": "Остановить запущенный узел?",

View File

@@ -1294,6 +1294,13 @@
"sweep_to": "归集到:", "sweep_to": "归集到:",
"sweep_toggle": "归集到我的钱包(不保留密钥)", "sweep_toggle": "归集到我的钱包(不保留密钥)",
"sweep_tx": "交易:", "sweep_tx": "交易:",
"switch_progress_background": "在后台继续",
"switch_progress_failed_title": "切换钱包失败",
"switch_progress_hint": "正常关闭最多可能需要一分钟。",
"switch_progress_reconnecting": "正在重新连接",
"switch_progress_starting": "正在以新钱包启动节点",
"switch_progress_stopping": "正在停止当前节点",
"switch_progress_title": "正在切换钱包",
"switch_stopnode_body": "切换钱包会以所选钱包重启节点。正在运行的节点将被停止并以新钱包重新启动——如果你是特意让它保持运行的,它会自动重新启动。", "switch_stopnode_body": "切换钱包会以所选钱包重启节点。正在运行的节点将被停止并以新钱包重新启动——如果你是特意让它保持运行的,它会自动重新启动。",
"switch_stopnode_confirm": "停止节点并切换", "switch_stopnode_confirm": "停止节点并切换",
"switch_stopnode_title": "停止正在运行的节点?", "switch_stopnode_title": "停止正在运行的节点?",

View File

@@ -3863,35 +3863,70 @@ void App::renderAntivirusHelpDialog()
void App::renderSwitchStopDaemonDialog() void App::renderSwitchStopDaemonDialog()
{ {
if (!show_switch_stop_daemon_confirm_) return; const bool confirm = show_switch_stop_daemon_confirm_;
const bool progress = wallet_switch_dialog_open_.load();
if (!confirm && !progress) return;
const auto phase = static_cast<WalletSwitchPhase>(wallet_switch_phase_.load());
const bool failed = progress && phase == WalletSwitchPhase::Failed;
ui::material::OverlayDialogSpec ov; ui::material::OverlayDialogSpec ov;
ov.title = TR("switch_stopnode_title"); ov.title = confirm ? TR("switch_stopnode_title")
ov.p_open = &show_switch_stop_daemon_confirm_; // the X / backdrop click acts as Cancel : failed ? TR("switch_progress_failed_title")
: TR("switch_progress_title");
// A confirm can be dismissed via X/backdrop (= cancel); a running switch cannot (stay until it's up).
ov.p_open = confirm ? &show_switch_stop_daemon_confirm_ : nullptr;
ov.style = ui::material::OverlayStyle::BlurFloat; ov.style = ui::material::OverlayStyle::BlurFloat;
ov.cardWidth = 520.0f; ov.idSuffix = "switchstopnode"; ov.cardWidth = 520.0f; ov.idSuffix = "switchstopnode";
if (!ui::material::BeginOverlayDialog(ov)) { if (!ui::material::BeginOverlayDialog(ov)) {
// Closed via X/backdrop — treat as cancel: drop the pending target. if (confirm && !show_switch_stop_daemon_confirm_) pending_switch_wallet_file_.clear(); // X = cancel
if (!show_switch_stop_daemon_confirm_) pending_switch_wallet_file_.clear();
return; return;
} }
const float dp = ui::Layout::dpiScale(); const float dp = ui::Layout::dpiScale();
ui::material::DialogWarningHeader(TR("switch_stopnode_warn")); if (confirm) {
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm())); ui::material::DialogWarningHeader(TR("switch_stopnode_warn"));
ImGui::TextWrapped("%s", TR("switch_stopnode_body")); ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
ImGui::Dummy(ImVec2(0, ui::Layout::spacingMd())); ImGui::TextWrapped("%s", TR("switch_stopnode_body"));
ImGui::Dummy(ImVec2(0, ui::Layout::spacingMd()));
if (ui::material::TactileButton(TR("switch_stopnode_confirm"), ImVec2(200.0f * dp, 0))) { if (ui::material::TactileButton(TR("switch_stopnode_confirm"), ImVec2(200.0f * dp, 0))) {
const std::string w = pending_switch_wallet_file_; const std::string w = pending_switch_wallet_file_;
pending_switch_wallet_file_.clear(); pending_switch_wallet_file_.clear();
show_switch_stop_daemon_confirm_ = false; show_switch_stop_daemon_confirm_ = false;
switchToWallet(w, /*stopDaemonConfirmed=*/true); // now proceeds to stop + wait + restart switchToWallet(w, /*stopDaemonConfirmed=*/true); // opens the live-progress phase below
} }
ImGui::SameLine(); ImGui::SameLine();
if (ui::material::TactileButton(TR("cancel"), ImVec2(110.0f * dp, 0))) { if (ui::material::TactileButton(TR("cancel"), ImVec2(110.0f * dp, 0))) {
pending_switch_wallet_file_.clear(); pending_switch_wallet_file_.clear();
show_switch_stop_daemon_confirm_ = false; show_switch_stop_daemon_confirm_ = false;
}
} else if (failed) {
ui::material::DialogWarningHeader(TR("switch_progress_failed_title"));
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
ImGui::TextWrapped("%s", wallet_switch_error_.c_str());
ImGui::Dummy(ImVec2(0, ui::Layout::spacingMd()));
if (ui::material::TactileButton(TR("close"), ImVec2(110.0f * dp, 0))) {
wallet_switch_dialog_open_.store(false);
wallet_switch_phase_.store(static_cast<int>(WalletSwitchPhase::None));
wallet_switch_error_.clear();
}
} else {
// Live progress: an animated phase line, kept redrawing by isWalletSwitchInProgress().
const char* status =
phase == WalletSwitchPhase::Starting ? TR("switch_progress_starting")
: phase == WalletSwitchPhase::Reconnecting ? TR("switch_progress_reconnecting")
: TR("switch_progress_stopping");
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
ui::material::Type().text(ui::material::TypeStyle::Subtitle1,
(std::string(status) + ui::material::LoadingDots()).c_str());
if (phase == WalletSwitchPhase::Stopping) {
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
ImGui::TextWrapped("%s", TR("switch_progress_hint"));
}
ImGui::Dummy(ImVec2(0, ui::Layout::spacingMd()));
// Escape hatch — the switch keeps running in the background (a toast reports the result).
if (ui::material::TactileButton(TR("switch_progress_background"), ImVec2(220.0f * dp, 0)))
wallet_switch_dialog_open_.store(false);
} }
ui::material::EndOverlayDialog(); ui::material::EndOverlayDialog();

View File

@@ -139,6 +139,9 @@ public:
* @brief Whether we are in the shutdown phase * @brief Whether we are in the shutdown phase
*/ */
bool isShuttingDown() const { return shutting_down_; } bool isShuttingDown() const { return shutting_down_; }
// True while the wallet-switch progress modal is open — keeps the frame loop redrawing so its live
// phase/spinner update in real time even when the app is otherwise idle.
bool isWalletSwitchInProgress() const { return wallet_switch_dialog_open_.load(); }
wallet::WalletCapabilities walletCapabilities() const { return wallet::currentWalletCapabilities(); } wallet::WalletCapabilities walletCapabilities() const { return wallet::currentWalletCapabilities(); }
bool isLiteBuild() const { return wallet::isLiteBuild(walletCapabilities()); } bool isLiteBuild() const { return wallet::isLiteBuild(walletCapabilities()); }
bool supportsEmbeddedDaemon() const { return wallet::supportsEmbeddedDaemon(walletCapabilities()); } bool supportsEmbeddedDaemon() const { return wallet::supportsEmbeddedDaemon(walletCapabilities()); }
@@ -768,6 +771,14 @@ private:
// sets these and defers to renderSwitchStopDaemonDialog; confirming re-calls switchToWallet(w, true). // sets these and defers to renderSwitchStopDaemonDialog; confirming re-calls switchToWallet(w, true).
bool show_switch_stop_daemon_confirm_ = false; bool show_switch_stop_daemon_confirm_ = false;
std::string pending_switch_wallet_file_; std::string pending_switch_wallet_file_;
// Live progress for the switch modal: it stays open from confirm through stop → wait-for-exit → start →
// reconnect and auto-closes when the new node connects (onConnected). Phase is worker-updated;
// dialog_open_ gates rendering — both atomic since onConnected/the worker may run off the main thread.
// error_ is written only on the main thread (processWalletSwitchRevert) and read by the (main-thread) UI.
enum class WalletSwitchPhase : int { None = 0, Stopping, Starting, Reconnecting, Failed };
std::atomic<int> wallet_switch_phase_{0};
std::atomic<bool> wallet_switch_dialog_open_{false};
std::string wallet_switch_error_;
// Set by the deleteBlockchainData worker (item count); the main loop surfaces a completion toast // Set by the deleteBlockchainData worker (item count); the main loop surfaces a completion toast
// and resets it to -1. Atomic because the worker thread writes it and the UI thread reads/clears it. // and resets it to -1. Atomic because the worker thread writes it and the UI thread reads/clears it.

View File

@@ -504,7 +504,13 @@ void App::tryConnect()
void App::onConnected() void App::onConnected()
{ {
state_.connected = true; 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 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_wait_attempts_ = 0; // re-arm the port-busy / start-failure notifications
daemon_start_error_shown_ = false; 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 // 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. // wallet's seed once it connects. onDisconnected alone doesn't cover chat.
resetChatSession(); resetChatSession();
ui::Notifications::instance().info( // Open the switch progress modal — it stays up through stop → wait-for-exit → start → reconnect and
"Switching wallet — stopping the node and restarting on the new wallet. " // auto-closes when the new node connects (onConnected). The worker advances the phase from here.
"A graceful shutdown can take up to a minute.", 60.0f); 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&) { async_tasks_.submit("Switch wallet", [this, needRescan](const util::AsyncTaskManager::Token&) {
bool ok = false; bool ok = false;
@@ -1126,6 +1134,7 @@ void App::switchToWallet(const std::string& walletFile, bool stopDaemonConfirmed
} else { } else {
// The relaunch is ours — clear the adopted-external latch so the fresh process is treated // 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). // 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 (daemon_controller_) daemon_controller_->clearExternalDaemonDetected();
if (needRescan && daemon_controller_) daemon_controller_->setRescanOnNextStart(true); if (needRescan && daemon_controller_) daemon_controller_->setRescanOnNextStart(true);
ok = shutting_down_ ? true : startEmbeddedDaemon(); 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)); std::this_thread::sleep_for(std::chrono::milliseconds(1500));
if (!isEmbeddedDaemonRunning()) ok = false; 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 (...) { } catch (...) {
ok = false; // a throw during stop/start is a failed switch — revert rather than wedge 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(); if (daemon_controller_) daemon_controller_->resetCrashCount();
// Accurate reason: "port wouldn't free" (the running node kept the connection) vs. a genuinely bad // 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. // target wallet. The former is the adopted-external-daemon case; the latter a daemon that exited in init.
if (switch_stop_failed_.exchange(false)) { const std::string reason = switch_stop_failed_.exchange(false)
ui::Notifications::instance().error( ? std::string("Couldn't switch wallets — the running node didn't release its connection in time. "
"Couldn't switch wallets — the running node didn't release its connection in time. " "It's still on the previous wallet.")
"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 { } 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 daemon_restarting_ = false; // the connect loop now restarts the previous wallet's daemon
} }

View File

@@ -379,6 +379,11 @@ void App::buildSweepCatalog()
add("modal-switch-stopnode", ui::NavPage::Settings, add("modal-switch-stopnode", ui::NavPage::Settings,
[](App& a) { a.pending_switch_wallet_file_ = "wallet-savings.dat"; a.show_switch_stop_daemon_confirm_ = true; }, [](App& a) { a.pending_switch_wallet_file_ = "wallet-savings.dat"; a.show_switch_stop_daemon_confirm_ = true; },
[](App& a) { a.show_switch_stop_daemon_confirm_ = false; a.pending_switch_wallet_file_.clear(); }); [](App& a) { a.show_switch_stop_daemon_confirm_ = false; a.pending_switch_wallet_file_.clear(); });
add("modal-switch-progress", ui::NavPage::Settings,
[](App& a) { a.wallet_switch_phase_.store(static_cast<int>(App::WalletSwitchPhase::Stopping));
a.wallet_switch_dialog_open_.store(true); },
[](App& a) { a.wallet_switch_dialog_open_.store(false);
a.wallet_switch_phase_.store(static_cast<int>(App::WalletSwitchPhase::None)); });
// Wave-2 fund/secret dialogs (setup never fires the async RPC — no button is clicked). // Wave-2 fund/secret dialogs (setup never fires the async RPC — no button is clicked).
add("modal-shield", ui::NavPage::Send, add("modal-shield", ui::NavPage::Send,
[](App&) { ui::ShieldDialog::showShieldCoinbase(); }, [](App&) { ui::ShieldDialog::showShieldCoinbase(); },

View File

@@ -1966,6 +1966,7 @@ int main(int argc, char* argv[])
bool backdropNeedsFrames = (backdrop_active || app.getGradientTexture() != 0) bool backdropNeedsFrames = (backdrop_active || app.getGradientTexture() != 0)
&& !opaqueBackground; && !opaqueBackground;
bool animating = app.isShuttingDown() bool animating = app.isShuttingDown()
|| app.isWalletSwitchInProgress()
|| backdropNeedsFrames || backdropNeedsFrames
|| app.hasTransactionSendProgress() || app.hasTransactionSendProgress()
|| app.isTransactionRefreshInProgress() || app.isTransactionRefreshInProgress()

View File

@@ -1045,6 +1045,14 @@ void I18n::loadBuiltinEnglish()
strings_["switch_stopnode_warn"] = "A node is already running that this wallet didn't start."; strings_["switch_stopnode_warn"] = "A node is already running that this wallet didn't start.";
strings_["switch_stopnode_body"] = "Switching wallets restarts the node on the wallet you selected. The running node will be stopped and relaunched on the new wallet — if you kept it running on purpose, it comes back up automatically."; strings_["switch_stopnode_body"] = "Switching wallets restarts the node on the wallet you selected. The running node will be stopped and relaunched on the new wallet — if you kept it running on purpose, it comes back up automatically.";
strings_["switch_stopnode_confirm"] = "Stop node & switch"; strings_["switch_stopnode_confirm"] = "Stop node & switch";
// Wallet-switch live progress modal
strings_["switch_progress_title"] = "Switching wallet";
strings_["switch_progress_failed_title"] = "Wallet switch failed";
strings_["switch_progress_stopping"] = "Stopping the current node";
strings_["switch_progress_starting"] = "Starting the node on the new wallet";
strings_["switch_progress_reconnecting"] = "Reconnecting";
strings_["switch_progress_hint"] = "A graceful shutdown can take up to a minute.";
strings_["switch_progress_background"] = "Continue in background";
// Receive Tab // Receive Tab
strings_["receiving_addresses"] = "Your Receiving Addresses"; strings_["receiving_addresses"] = "Your Receiving Addresses";