feat(settings): redesign the remove-encryption (decrypt) modal onto reference design
Migrate App::renderDecryptWalletDialog ("Remove Wallet Encryption") onto the
settings-modal reference design — presentation-only for a fund-critical dialog,
plus one deliberate secret-hygiene addition:
- struct-form BeginOverlayDialog(OverlayDialogSpec{BlurFloat, cardWidth 480,
idSuffix "decrypt"}) replacing the legacy positional overload
- Phase 0 amber TextWrapped warning -> material::DialogWarningHeader(decrypt_warning)
- drop the redundant Phase 0 Separator
- every rendered-chrome string across all 4 phases (PassphraseEntry / Working /
Success / Error) -> TR() (14 new decrypt_*/try_again keys added to the English
source + all 8 res/lang/*.json additively; the button reuses
settings_remove_encryption, the label reuses change_pass_current, cancel/close reused)
- HARDENING: wipe decrypt_pass_buf_ on BlurFloat dismiss (X/Esc/outside-click),
mirroring the change-passphrase fix; canClose() gates pOpen so this cannot fire
during the Working phase
The fund-moving async worker pyramid (unlock -> export -> stop -> backup ->
restart -> re-import, vault/PIN cleanup) is byte-for-byte unchanged; the three
in-worker toast notifications and the "Incorrect passphrase" status are left in
English to avoid touching the fund path (a separate service-layer i18n pass).
Verified: build + ctest + hygiene clean; i18n complete in 8 langs with no format
specifiers and the wallet.dat.encrypted.bak literal preserved; a 3-lens adversarial
review (secret-hygiene / fund-path-untouched / ui-i18n) returned zero findings, with
the fund-path lens proving the async pyramid diffs IDENTICAL against HEAD.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1434,26 +1434,21 @@ void App::renderDecryptWalletDialog() {
|
||||
bool canClose = wallet_security_workflow_.canClose();
|
||||
bool* pOpen = canClose ? &show_decrypt_dialog_ : nullptr;
|
||||
|
||||
if (BeginOverlayDialog("Remove Wallet Encryption", pOpen, 480.0f, 0.94f)) {
|
||||
OverlayDialogSpec ov;
|
||||
ov.title = TR("decrypt_title"); ov.p_open = pOpen;
|
||||
ov.style = OverlayStyle::BlurFloat;
|
||||
ov.cardWidth = 480.0f; ov.idSuffix = "decrypt";
|
||||
if (BeginOverlayDialog(ov)) {
|
||||
|
||||
// ---- Phase 0: Passphrase entry ----
|
||||
if (decryptState.phase == DecryptPhase::PassphraseEntry) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1, 0.7f, 0.3f, 1));
|
||||
ImGui::TextWrapped(ICON_MD_WARNING
|
||||
" This will remove encryption from your wallet. "
|
||||
"Your private keys will be stored unprotected on disk.");
|
||||
ImGui::PopStyleColor();
|
||||
DialogWarningHeader(TR("decrypt_warning"));
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::TextWrapped(
|
||||
"The wallet will be exported, the daemon restarted with a fresh "
|
||||
"unencrypted wallet, and all keys re-imported. This may take "
|
||||
"several minutes depending on wallet size.");
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::TextWrapped("%s", TR("decrypt_desc"));
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::Text("Current Passphrase:");
|
||||
ImGui::TextUnformatted(TR("change_pass_current"));
|
||||
ImGui::PushItemWidth(-1);
|
||||
bool enterPressed = ImGui::InputText("##decrypt_pass", decrypt_pass_buf_,
|
||||
sizeof(decrypt_pass_buf_), ImGuiInputTextFlags_Password |
|
||||
@@ -1469,7 +1464,7 @@ void App::renderDecryptWalletDialog() {
|
||||
|
||||
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
||||
ImGui::BeginDisabled(!valid || decryptState.inProgress);
|
||||
if (ui::material::TactileButton("Remove Encryption", ImVec2(btnW, 40)) || (enterPressed && valid)) {
|
||||
if (ui::material::TactileButton(TR("settings_remove_encryption"), ImVec2(btnW, 40)) || (enterPressed && valid)) {
|
||||
std::string passphrase(decrypt_pass_buf_);
|
||||
memset(decrypt_pass_buf_, 0, sizeof(decrypt_pass_buf_));
|
||||
wallet_security_workflow_.start(std::chrono::steady_clock::now());
|
||||
@@ -1660,7 +1655,7 @@ void App::renderDecryptWalletDialog() {
|
||||
ImGui::EndDisabled();
|
||||
|
||||
ImGui::SameLine();
|
||||
if (ui::material::TactileButton("Cancel", ImVec2(btnW, 40))) {
|
||||
if (ui::material::TactileButton(TR("cancel"), ImVec2(btnW, 40))) {
|
||||
memset(decrypt_pass_buf_, 0, sizeof(decrypt_pass_buf_));
|
||||
show_decrypt_dialog_ = false;
|
||||
}
|
||||
@@ -1669,11 +1664,11 @@ void App::renderDecryptWalletDialog() {
|
||||
} else if (decryptState.phase == DecryptPhase::Working) {
|
||||
// Step checklist
|
||||
const char* stepLabels[] = {
|
||||
"Unlocking wallet",
|
||||
"Exporting wallet keys",
|
||||
"Stopping daemon",
|
||||
"Backing up encrypted wallet",
|
||||
"Restarting daemon"
|
||||
TR("decrypt_step_unlock"),
|
||||
TR("decrypt_step_export"),
|
||||
TR("decrypt_step_stop"),
|
||||
TR("decrypt_step_backup"),
|
||||
TR("decrypt_step_restart")
|
||||
};
|
||||
const int numSteps = 5;
|
||||
|
||||
@@ -1750,10 +1745,9 @@ void App::renderDecryptWalletDialog() {
|
||||
|
||||
// Step-specific hints
|
||||
if (decryptState.step == DecryptStep::RestartDaemon) {
|
||||
ImGui::TextWrapped("Waiting for the daemon to finish starting up...");
|
||||
ImGui::TextWrapped("%s", TR("decrypt_wait_restart"));
|
||||
} else {
|
||||
ImGui::TextWrapped("Please wait. The daemon is exporting keys, restarting, "
|
||||
"and re-importing. This may take several minutes.");
|
||||
ImGui::TextWrapped("%s", TR("decrypt_wait_general"));
|
||||
}
|
||||
|
||||
// Total elapsed
|
||||
@@ -1770,15 +1764,13 @@ void App::renderDecryptWalletDialog() {
|
||||
ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.5f, 1.0f), ICON_MD_CHECK_CIRCLE);
|
||||
ImGui::PopFont();
|
||||
ImGui::SameLine();
|
||||
ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.5f, 1.0f), "Wallet decrypted successfully!");
|
||||
ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.5f, 1.0f), "%s", TR("decrypt_success_title"));
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::TextWrapped(
|
||||
"Your wallet is now unencrypted. A backup of the encrypted wallet "
|
||||
"was saved as wallet.dat.encrypted.bak in your data directory.");
|
||||
ImGui::TextWrapped("%s", TR("decrypt_success_desc"));
|
||||
|
||||
ImGui::Spacing();
|
||||
if (ui::material::TactileButton("Close", ImVec2(-1, 40))) {
|
||||
if (ui::material::TactileButton(TR("close"), ImVec2(-1, 40))) {
|
||||
show_decrypt_dialog_ = false;
|
||||
}
|
||||
|
||||
@@ -1788,23 +1780,31 @@ void App::renderDecryptWalletDialog() {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), ICON_MD_ERROR);
|
||||
ImGui::PopFont();
|
||||
ImGui::SameLine();
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "Decryption failed");
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "%s", TR("decrypt_error_title"));
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::TextWrapped("%s", decryptState.status.c_str());
|
||||
|
||||
ImGui::Spacing();
|
||||
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
||||
if (ui::material::TactileButton("Try Again", ImVec2(btnW, 40))) {
|
||||
if (ui::material::TactileButton(TR("try_again"), ImVec2(btnW, 40))) {
|
||||
wallet_security_workflow_.reset();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ui::material::TactileButton("Close", ImVec2(btnW, 40))) {
|
||||
if (ui::material::TactileButton(TR("close"), ImVec2(btnW, 40))) {
|
||||
show_decrypt_dialog_ = false;
|
||||
}
|
||||
}
|
||||
EndOverlayDialog();
|
||||
}
|
||||
|
||||
// Wipe the passphrase buffer if the dialog was dismissed (X / Esc / outside-
|
||||
// click) without submitting. The submit and Cancel paths already memset it;
|
||||
// this covers the BlurFloat dismiss paths. (canClose is false during Working,
|
||||
// so an in-flight decrypt cannot be dismissed here.)
|
||||
if (!show_decrypt_dialog_) {
|
||||
memset(decrypt_pass_buf_, 0, sizeof(decrypt_pass_buf_));
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
|
||||
@@ -522,6 +522,21 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["change_pass_current"] = "Current Passphrase:";
|
||||
strings_["change_pass_new"] = "New Passphrase:";
|
||||
strings_["change_pass_confirm"] = "Confirm New:";
|
||||
// --- Remove-encryption (decrypt) dialog (settings) ---
|
||||
strings_["decrypt_title"] = "Remove Wallet Encryption";
|
||||
strings_["decrypt_warning"] = "This will remove encryption from your wallet. Your private keys will be stored unprotected on disk.";
|
||||
strings_["decrypt_desc"] = "The wallet will be exported, the daemon restarted with a fresh unencrypted wallet, and all keys re-imported. This may take several minutes depending on wallet size.";
|
||||
strings_["decrypt_step_unlock"] = "Unlocking wallet";
|
||||
strings_["decrypt_step_export"] = "Exporting wallet keys";
|
||||
strings_["decrypt_step_stop"] = "Stopping daemon";
|
||||
strings_["decrypt_step_backup"] = "Backing up encrypted wallet";
|
||||
strings_["decrypt_step_restart"] = "Restarting daemon";
|
||||
strings_["decrypt_wait_restart"] = "Waiting for the daemon to finish starting up...";
|
||||
strings_["decrypt_wait_general"] = "Please wait. The daemon is exporting keys, restarting, and re-importing. This may take several minutes.";
|
||||
strings_["decrypt_success_title"] = "Wallet decrypted successfully!";
|
||||
strings_["decrypt_success_desc"] = "Your wallet is now unencrypted. A backup of the encrypted wallet was saved as wallet.dat.encrypted.bak in your data directory.";
|
||||
strings_["decrypt_error_title"] = "Decryption failed";
|
||||
strings_["try_again"] = "Try Again";
|
||||
strings_["wiz_confirm"] = "Confirm:";
|
||||
strings_["wiz_strength_weak"] = "Weak";
|
||||
strings_["wiz_strength_fair"] = "Fair";
|
||||
|
||||
Reference in New Issue
Block a user