feat(overview): migrate the Export-Key modal to the reference design

Presentation-only migration of KeyExportDialog::render (reveals a private or
viewing key) — a secret-handling modal:
- struct-form BeginOverlayDialog(OverlayDialogSpec{BlurFloat, cardWidth cardW,
  idSuffix keyexport}) replacing the legacy positional overload
- replace the bespoke red-bordered "WARNING" box with material::DialogWarningHeader
  (user-approved) so the key-export warning matches the import-key reference; passes
  the correct per-key-type body key (private vs viewing)
- remove the two dividers (after the warning, above the Close footer)
- 4 StyledButton -> TactileButton (retry / reveal / show-hide / close)

The secret path is byte-for-byte unchanged: the z_exportkey/dumpprivkey/
z_exportviewingkey (and lite exportPrivateKeys) fetch + their secureWipeLiteSecret
calls, the "*"-masking when hidden, the Show/Hide toggle, copySecretToClipboard
(auto-clearing), the QR generate/cache/releaseQr, and the secret-clear on BOTH exit
paths (Close button + the post-EndOverlayDialog dismiss teardown).

Verified: build + ctest + hygiene clean; a 2-lens adversarial review
(secret-hygiene / presentation-fund) returned zero findings, proving every
secret-handling path diffs byte-for-byte identical to HEAD.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-14 18:23:14 -05:00
parent d29283a9b7
commit 88e10f3e06

View File

@@ -66,7 +66,6 @@ void KeyExportDialog::render(App* app)
TR("export_private_key") : TR("export_viewing_key"); TR("export_private_key") : TR("export_viewing_key");
auto& S = schema::UI(); auto& S = schema::UI();
auto warningBox = S.drawElement("dialogs.key-export", "warning-box");
auto addrInput = S.input("dialogs.key-export", "address-input"); auto addrInput = S.input("dialogs.key-export", "address-input");
auto revealBtn = S.button("dialogs.key-export", "reveal-button"); auto revealBtn = S.button("dialogs.key-export", "reveal-button");
auto keyDisplay = S.drawElement("dialogs.key-export", "key-display"); auto keyDisplay = S.drawElement("dialogs.key-export", "key-display");
@@ -79,54 +78,14 @@ void KeyExportDialog::render(App* app)
// Modal scales to 85% of the window width. BeginOverlayDialog multiplies the width by // Modal scales to 85% of the window width. BeginOverlayDialog multiplies the width by
// Layout::dpiScale(); divide it out here so the final card is exactly 85% at any font scale. // Layout::dpiScale(); divide it out here so the final card is exactly 85% at any font scale.
const float cardW = (0.85f * ImGui::GetMainViewport()->Size.x) / Layout::dpiScale(); const float cardW = (0.85f * ImGui::GetMainViewport()->Size.x) / Layout::dpiScale();
if (material::BeginOverlayDialog(title, &s_open, cardW, 0.94f)) { material::OverlayDialogSpec ov;
ImGui::Spacing(); ov.title = title; ov.p_open = &s_open;
ov.style = material::OverlayStyle::BlurFloat;
ov.cardWidth = cardW; ov.idSuffix = "keyexport";
if (material::BeginOverlayDialog(ov)) {
material::DialogWarningHeader((s_key_type == KeyType::Private)
? TR("key_export_private_warning") : TR("key_export_viewing_warning"));
// Warning section with colored background
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.6f, 0.2f, 0.2f, 0.3f));
// Auto-size the warning box to its (wrapping, font-scaled) text so it never clips it.
(void)warningBox;
ImGui::BeginChild("WarningBox", ImVec2(-1, 0),
ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_Borders);
// Heading — warning icon (drawn with the icon font, not the text font, so it isn't tofu) +
// larger h6 text, centered, for emphasis.
{
ImFont* iconF = material::Type().iconMed();
ImFont* textF = material::Type().h6();
const char* icon = ICON_MD_WARNING;
const char* txt = TR("warning_upper");
ImGui::PushFont(iconF); const float iw = ImGui::CalcTextSize(icon).x; ImGui::PopFont();
ImGui::PushFont(textF); const float tw = ImGui::CalcTextSize(txt).x; ImGui::PopFont();
const float sp = ImGui::GetStyle().ItemSpacing.x;
const float wa = ImGui::GetContentRegionAvail().x;
const float total = iw + sp + tw;
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (wa > total ? (wa - total) * 0.5f : 0.0f));
const ImVec4 red(1.0f, 0.42f, 0.42f, 1.0f);
ImGui::PushFont(iconF); ImGui::TextColored(red, "%s", icon); ImGui::PopFont();
ImGui::SameLine(0, sp);
ImGui::PushFont(textF); ImGui::TextColored(red, "%s", txt); ImGui::PopFont();
}
ImGui::Spacing();
// Body — centered wrapped text.
{
const char* body = (s_key_type == KeyType::Private)
? TR("key_export_private_warning") : TR("key_export_viewing_warning");
ImDrawList* wdl = ImGui::GetWindowDrawList();
ImFont* wf = ImGui::GetFont();
const float wfs = ImGui::GetFontSize();
const float wa = ImGui::GetContentRegionAvail().x;
const ImVec2 wp = ImGui::GetCursorScreenPos();
const float bh = widgets::DrawCenteredWrapped(wdl, wf, wfs, wp, wa, material::OnSurface(), body);
ImGui::Dummy(ImVec2(wa, bh));
}
ImGui::EndChild();
ImGui::PopStyleColor();
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing(); ImGui::Spacing();
// Address — click-to-copy field (chunked, width-clamped, centered). // Address — click-to-copy field (chunked, width-clamped, centered).
@@ -145,7 +104,7 @@ void KeyExportDialog::render(App* app)
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), TR("error_format"), s_error.c_str()); ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), TR("error_format"), s_error.c_str());
// Not a dead-end: clearing the error falls back through to the Reveal button next frame. // Not a dead-end: clearing the error falls back through to the Reveal button next frame.
ImGui::Spacing(); ImGui::Spacing();
if (material::StyledButton(TR("retry"), ImVec2(revealBtn.width, 0), S.resolveFont(revealBtn.font))) { if (material::TactileButton(TR("retry"), ImVec2(revealBtn.width, 0), S.resolveFont(revealBtn.font))) {
s_error.clear(); s_error.clear();
} }
} else if (s_key.empty() && s_address.length() < 26) { } else if (s_key.empty() && s_address.length() < 26) {
@@ -154,7 +113,7 @@ void KeyExportDialog::render(App* app)
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "%s", TR("invalid_address")); ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "%s", TR("invalid_address"));
} else if (s_key.empty()) { } else if (s_key.empty()) {
// Show button to fetch key // Show button to fetch key
if (material::StyledButton(TR("key_export_reveal"), ImVec2(revealBtn.width, 0), S.resolveFont(revealBtn.font))) { if (material::TactileButton(TR("key_export_reveal"), ImVec2(revealBtn.width, 0), S.resolveFont(revealBtn.font))) {
s_fetching = true; s_fetching = true;
// Check if z-address or t-address // Check if z-address or t-address
@@ -292,7 +251,7 @@ void KeyExportDialog::render(App* app)
app->copySecretToClipboard(s_key); // auto-clearing clipboard app->copySecretToClipboard(s_key); // auto-clearing clipboard
} }
ImGui::Spacing(); ImGui::Spacing();
if (material::StyledButton(s_show_key ? TR("hide") : TR("show"), ImVec2(0, 0), if (material::TactileButton(s_show_key ? TR("hide") : TR("show"), ImVec2(0, 0),
material::Type().subtitle1())) { material::Type().subtitle1())) {
s_show_key = !s_show_key; s_show_key = !s_show_key;
} }
@@ -328,8 +287,6 @@ void KeyExportDialog::render(App* app)
} }
} }
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing(); ImGui::Spacing();
// Close button // Close button
@@ -337,7 +294,7 @@ void KeyExportDialog::render(App* app)
float avail_width = ImGui::GetContentRegionAvail().x; float avail_width = ImGui::GetContentRegionAvail().x;
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (avail_width - button_width) / 2.0f); ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (avail_width - button_width) / 2.0f);
if (material::StyledButton(TR("close"), ImVec2(button_width, 0), S.resolveFont(closeBtn.font))) { if (material::TactileButton(TR("close"), ImVec2(button_width, 0), S.resolveFont(closeBtn.font))) {
s_open = false; s_open = false;
// Clear sensitive data // Clear sensitive data
s_key.clear(); s_key.clear();