feat(wallet): confirm before stopping an adopted node to switch wallets

When switching wallets, the node the app would restart may be one this session
ADOPTED — a dragonxd already running at launch (left up by "keep node running",
or started by the user). Rather than stopping it silently (or the old dead-code
"stop any external dragonxd first" refusal), show a "Stop the running node?"
confirmation first; switching then stops it (RPC stop + wait for the port to
fully free) and relaunches on the selected wallet.

- switchToWallet(walletFile, stopDaemonConfirmed=false): when the node is adopted
  (externalDaemonDetected) and not yet confirmed, defer to the dialog and return.
- renderSwitchStopDaemonDialog(): BlurFloat overlay (house style) with a warning
  header; confirm re-enters switchToWallet(w, true); cancel/X aborts, node keeps
  running. Owned nodes (started this session) still restart silently.
- i18n (EN + 8 languages, additive) + CJK subset rebuild; modal-switch-stopnode
  sweep surface for visual review.

Gate/re-entrancy adversarially verified (no state leak; owned switches ungated;
prompt reappears on a reverted switch; dead-daemon-before-confirm handled).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-15 19:17:08 -05:00
parent 30bc80b2f2
commit 42d59709f9
14 changed files with 97 additions and 3 deletions

View File

@@ -2034,6 +2034,7 @@ void App::render()
renderEncryptWalletDialog();
renderDecryptWalletDialog();
renderPinDialogs();
renderSwitchStopDaemonDialog();
// Render notifications (toast messages)
ui::Notifications::instance().render();
@@ -3860,6 +3861,42 @@ void App::renderAntivirusHelpDialog()
#endif
}
void App::renderSwitchStopDaemonDialog()
{
if (!show_switch_stop_daemon_confirm_) return;
ui::material::OverlayDialogSpec ov;
ov.title = TR("switch_stopnode_title");
ov.p_open = &show_switch_stop_daemon_confirm_; // the X / backdrop click acts as Cancel
ov.style = ui::material::OverlayStyle::BlurFloat;
ov.cardWidth = 520.0f; ov.idSuffix = "switchstopnode";
if (!ui::material::BeginOverlayDialog(ov)) {
// Closed via X/backdrop — treat as cancel: drop the pending target.
if (!show_switch_stop_daemon_confirm_) pending_switch_wallet_file_.clear();
return;
}
const float dp = ui::Layout::dpiScale();
ui::material::DialogWarningHeader(TR("switch_stopnode_warn"));
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
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))) {
const std::string w = pending_switch_wallet_file_;
pending_switch_wallet_file_.clear();
show_switch_stop_daemon_confirm_ = false;
switchToWallet(w, /*stopDaemonConfirmed=*/true); // now proceeds to stop + wait + restart
}
ImGui::SameLine();
if (ui::material::TactileButton(TR("cancel"), ImVec2(110.0f * dp, 0))) {
pending_switch_wallet_file_.clear();
show_switch_stop_daemon_confirm_ = false;
}
ui::material::EndOverlayDialog();
}
void App::refreshNow()
{
// Trigger immediate refresh on all categories