feat(settings): migrate Seed phrase backup modal to the reference design

Phase 2 (second security modal), presentation-only + the review's one nit.

Struct-form BlurFloat overlay (was the legacy positional overload); the phrase
state's plain red warning -> material::DialogWarningHeader; StyledButtons ->
TactileButtons with the Copy/Save/Close row and shared Close centered via
PlaceOverlayDialogActions (the existing helper) and their separators dropped;
success status now wraps. Already fully i18n'd.

UNCHANGED (verified by the adversarial review): the closeAndWipe sodium_memzero of
the 24-word phrase on every close path (Begin-false early return, outside-click/Esc
teardown, phrase Close, shared Close), the async exportSeedPhrase fetch + its
show_seed_backup_ callback guard, the atomic restricted-perms file-write +
sodium_memzero of its content buffer, and copySecretToClipboard / RenderSeedWordGrid.
No seed leak; Begin/EndOverlayDialog balanced on every return path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-14 00:28:25 -05:00
parent 8a30578872
commit 5dd68dc9bd

View File

@@ -3430,7 +3430,13 @@ void App::renderSeedBackupDialog()
}); });
} }
if (!ui::material::BeginOverlayDialog(TR("seed_backup_title"), &show_seed_backup_, 540.0f, 0.94f)) { ui::material::OverlayDialogSpec ov;
ov.title = TR("seed_backup_title");
ov.p_open = &show_seed_backup_;
ov.style = ui::material::OverlayStyle::BlurFloat;
ov.cardWidth = 560.0f;
ov.idSuffix = "seedbackup";
if (!ui::material::BeginOverlayDialog(ov)) {
if (seed_backup_fetch_started_) closeAndWipe(); if (seed_backup_fetch_started_) closeAndWipe();
return; return;
} }
@@ -3453,24 +3459,26 @@ void App::renderSeedBackupDialog()
} else if (seed_backup_no_mnemonic_) { } else if (seed_backup_no_mnemonic_) {
ImGui::TextWrapped("%s", TR("seed_backup_none")); ImGui::TextWrapped("%s", TR("seed_backup_none"));
} else if (!seed_backup_phrase_.empty()) { } else if (!seed_backup_phrase_.empty()) {
ImGui::TextWrapped("%s", TR("seed_backup_intro")); ui::material::DialogWarningHeader(TR("seed_backup_warning"));
ImGui::Spacing(); ImGui::Spacing();
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::ReadableError()); ImGui::TextWrapped("%s", TR("seed_backup_intro"));
ImGui::TextWrapped("%s", TR("seed_backup_warning"));
ImGui::PopStyleColor();
ImGui::Spacing(); ImGui::Spacing();
ui::RenderSeedWordGrid(ui::SplitSeedWords(seed_backup_phrase_)); ui::RenderSeedWordGrid(ui::SplitSeedWords(seed_backup_phrase_));
ImGui::Spacing(); ImGui::Spacing();
if (!seed_backup_status_.empty()) { if (!seed_backup_status_.empty()) {
ImGui::PushTextWrapPos(0.0f);
ImGui::TextColored(ui::material::SuccessVec4(), "%s", seed_backup_status_.c_str()); ImGui::TextColored(ui::material::SuccessVec4(), "%s", seed_backup_status_.c_str());
ImGui::PopTextWrapPos();
ImGui::Spacing(); ImGui::Spacing();
} }
ImGui::Separator(); // Footer: Copy | Save | Close — centered as a group (no divider).
if (ui::material::StyledButton(TR("seed_backup_copy"), ImVec2(120 * dp, 0))) { const float cW = 120 * dp, sW = 150 * dp;
ui::material::PlaceOverlayDialogActions(cW + sW + cW + ImGui::GetStyle().ItemSpacing.x * 2.0f);
if (ui::material::TactileButton(TR("seed_backup_copy"), ImVec2(cW, 0))) {
copySecretToClipboard(seed_backup_phrase_); copySecretToClipboard(seed_backup_phrase_);
} }
ImGui::SameLine(); ImGui::SameLine();
if (ui::material::StyledButton(TR("seed_backup_save"), ImVec2(150 * dp, 0))) { if (ui::material::TactileButton(TR("seed_backup_save"), ImVec2(sW, 0))) {
std::string path = util::Platform::getConfigDir() + "/dragonx-seed-backup.txt"; std::string path = util::Platform::getConfigDir() + "/dragonx-seed-backup.txt";
std::string content = seed_backup_phrase_ + "\n"; std::string content = seed_backup_phrase_ + "\n";
const bool ok = util::Platform::writeFileAtomically(path, content, const bool ok = util::Platform::writeFileAtomically(path, content,
@@ -3480,7 +3488,7 @@ void App::renderSeedBackupDialog()
: std::string(TR("seed_backup_save_failed"))) + path; : std::string(TR("seed_backup_save_failed"))) + path;
} }
ImGui::SameLine(); ImGui::SameLine();
if (ui::material::StyledButton(TR("seed_backup_close"), ImVec2(120 * dp, 0))) { if (ui::material::TactileButton(TR("seed_backup_close"), ImVec2(cW, 0))) {
closeAndWipe(); closeAndWipe();
ui::material::EndOverlayDialog(); ui::material::EndOverlayDialog();
return; return;
@@ -3492,8 +3500,10 @@ void App::renderSeedBackupDialog()
} }
ImGui::Spacing(); ImGui::Spacing();
ImGui::Separator(); // Centered Close (no divider).
if (ui::material::StyledButton(TR("seed_backup_close"), ImVec2(120 * dp, 0))) { const float cbW = 120 * dp;
ui::material::PlaceOverlayDialogActions(cbW);
if (ui::material::TactileButton(TR("seed_backup_close"), ImVec2(cbW, 0))) {
closeAndWipe(); closeAndWipe();
} }
ui::material::EndOverlayDialog(); ui::material::EndOverlayDialog();