Files
ObsidianDragon/src/ui/windows/export_transactions_dialog.cpp
DanS ddca8b2e43 v1.2.0: UX audit — security fixes, accessibility, and polish
Security (P0):
- Fix sidebar remaining interactive behind lock screen
- Extend auto-lock idle detection to include active widget interactions
- Distinguish missing PIN vault from wrong PIN; auto-switch to passphrase

Blocking UX (P1):
- Add 15s timeout for encryption state check to prevent indefinite loading
- Show restart reason in loading overlay after wallet encryption
- Add Force Quit button on shutdown screen after 10s
- Warn user if embedded daemon fails to start during wizard completion

Polish (P2):
- Use configured explorer URL in Receive tab instead of hardcoded URL
- Increase request memo buffer from 256 to 512 bytes to match Send tab
- Extend notification duration to 5s for critical operations (tx sent,
  wallet encrypted, key import, backup, export)
- Add Reduce Motion accessibility setting (disables page fade + balance lerp)
- Show estimated remaining time during mining thread benchmark
- Add staleness indicator to market price data (warning after 5 min)

New i18n keys: incorrect_pin, incorrect_passphrase, pin_not_set,
restarting_after_encryption, force_quit, reduce_motion, tt_reduce_motion,
ago, wizard_daemon_start_failed
2026-04-04 19:10:58 -05:00

166 lines
5.4 KiB
C++

// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "export_transactions_dialog.h"
#include "../../app.h"
#include "../../util/i18n.h"
#include "../../util/platform.h"
#include "../notifications.h"
#include "../schema/ui_schema.h"
#include "../material/draw_helpers.h"
#include "../theme.h"
#include "imgui.h"
#include <string>
#include <fstream>
#include <sstream>
#include <ctime>
#include <iomanip>
namespace dragonx {
namespace ui {
// Static state
static bool s_open = false;
static char s_filename[256] = "";
static std::string s_status;
// Helper to escape CSV field
static std::string escapeCSV(const std::string& field)
{
if (field.find(',') != std::string::npos ||
field.find('"') != std::string::npos ||
field.find('\n') != std::string::npos) {
// Escape quotes and wrap in quotes
std::string escaped;
escaped.reserve(field.size() + 4);
escaped += '"';
for (char c : field) {
if (c == '"') escaped += "\"\"";
else escaped += c;
}
escaped += '"';
return escaped;
}
return field;
}
void ExportTransactionsDialog::show()
{
s_open = true;
s_status.clear();
// Generate default filename with timestamp
std::time_t now = std::time(nullptr);
char timebuf[32];
std::strftime(timebuf, sizeof(timebuf), "%Y%m%d_%H%M%S", std::localtime(&now));
snprintf(s_filename, sizeof(s_filename), "dragonx_transactions_%s.csv", timebuf);
}
bool ExportTransactionsDialog::isOpen()
{
return s_open;
}
void ExportTransactionsDialog::render(App* app)
{
if (!s_open) return;
auto& S = schema::UI();
auto win = S.window("dialogs.export-transactions");
auto exportBtn = S.button("dialogs.export-transactions", "export-button");
auto closeBtn = S.button("dialogs.export-transactions", "close-button");
if (material::BeginOverlayDialog(TR("export_tx_title"), &s_open, win.width, 0.94f)) {
const auto& state = app->getWalletState();
ImGui::Text(TR("export_tx_count"), state.transactions.size());
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Filename
ImGui::Text("%s", TR("output_filename"));
ImGui::SetNextItemWidth(-1);
ImGui::InputText("##Filename", s_filename, sizeof(s_filename));
ImGui::Spacing();
ImGui::TextDisabled("%s", TR("file_save_location"));
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Export button
if (material::StyledButton(TR("export"), ImVec2(exportBtn.width, 0), S.resolveFont(exportBtn.font))) {
if (state.transactions.empty()) {
Notifications::instance().warning(TR("export_tx_none"));
} else {
std::string configDir = util::Platform::getConfigDir();
std::string filepath = configDir + "/" + s_filename;
std::ofstream file(filepath);
if (!file.is_open()) {
s_status = "Failed to create file";
Notifications::instance().error(TR("export_tx_file_fail"));
} else {
// Write CSV header
file << "Date,Type,Amount,Address,TXID,Confirmations,Memo\n";
// Write transactions
for (const auto& tx : state.transactions) {
// Date
std::time_t t = static_cast<std::time_t>(tx.timestamp);
char datebuf[32];
std::strftime(datebuf, sizeof(datebuf), "%Y-%m-%d %H:%M:%S", std::localtime(&t));
file << datebuf << ",";
// Type
file << escapeCSV(tx.type) << ",";
// Amount
std::ostringstream amt;
amt << std::fixed << std::setprecision(8) << tx.amount;
file << amt.str() << ",";
// Address
file << escapeCSV(tx.address) << ",";
// TXID
file << escapeCSV(tx.txid) << ",";
// Confirmations
file << tx.confirmations << ",";
// Memo
file << escapeCSV(tx.memo) << "\n";
}
file.close();
s_status = "Exported " + std::to_string(state.transactions.size()) +
" transactions to: " + filepath;
Notifications::instance().success(TR("export_tx_success"), 5.0f);
}
}
}
ImGui::SameLine();
if (material::StyledButton("Close", ImVec2(closeBtn.width, 0), S.resolveFont(closeBtn.font))) {
s_open = false;
}
// Status
if (!s_status.empty()) {
ImGui::Spacing();
ImGui::TextWrapped("%s", s_status.c_str());
}
material::EndOverlayDialog();
}
}
} // namespace ui
} // namespace dragonx