feat(request,balance): Request-Payment create-address empty-state + tx empty states

- Request-Payment no longer dead-ends with no shielded address: it shows a hint
  and a "Create a shielded address" button (auto-selecting the new address), and
  labels address entries with their contact/wallet names.
- The balance "Recent transactions" empty area uses a proper empty-state and now
  distinguishes "still loading / syncing" from "no transactions yet" instead of a
  dim one-liner that also showed mid-sync.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-10-18 22:50:30 -05:00
co-authored by Claude Opus 4.8
parent f2d9590058
commit 3f127f9b62
2 changed files with 81 additions and 20 deletions
+21 -1
View File
@@ -1057,7 +1057,27 @@ void RenderSharedRecentTx(App* app, float recentH, float availW, float hs, float
int count = std::min({4, maxRows, (int)txs.size()}); // show only the most recent (state.transactions is newest-first)
if (count == 0) {
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("no_transactions_yet"));
const bool roomForIcon = ImGui::GetContentRegionAvail().y >= 100.0f * dp;
if (!app->isConnected()) {
// Disconnected: not "empty" and not "loading" — keep a plain caption.
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(),
TR(app->isLiteBuild() ? "lite_no_wallet" : "not_connected"));
} else {
const std::string txLoadingText = app->transactionRefreshProgressText();
// In lite, transactionRefreshProgressText() is empty and last_tx_update is stamped differently,
// so treat only an active sync as "loading" there.
const bool loadingOrSyncing = !txLoadingText.empty() ||
(state.sync.syncing && !state.sync.isSynced()) ||
(!app->isLiteBuild() && state.last_tx_update == 0);
if (loadingOrSyncing) {
char lbuf[192];
const char* base = !txLoadingText.empty() ? txLoadingText.c_str() : TR("loading_transactions");
snprintf(lbuf, sizeof(lbuf), "%s%s", base, material::LoadingDots());
material::DrawEmptyState(roomForIcon ? ICON_MD_HOURGLASS_EMPTY : nullptr, lbuf);
} else {
material::DrawEmptyState(roomForIcon ? ICON_MD_RECEIPT_LONG : nullptr, TR("no_transactions_yet"));
}
}
} else {
ImDrawList* dl = ImGui::GetWindowDrawList();
ImFont* capFont = Type().caption();
+60 -19
View File
@@ -32,6 +32,7 @@ static uintptr_t s_qr_texture = 0;
static int s_qr_width = 0;
static int s_qr_height = 0;
static bool s_uri_dirty = true;
static bool s_creating_address = false; // true while a Create-shielded-address RPC is in flight
// Helper to build payment URI
static std::string buildPaymentUri()
@@ -48,7 +49,8 @@ void RequestPaymentDialog::show(const std::string& address)
s_label[0] = '\0';
s_payment_uri.clear();
s_uri_dirty = true;
s_creating_address = false;
// Only a shielded address can be paid (DragonX is a private chain): never prefill a mining address.
if (!address.empty() && !util::isTransparentAddress(address)) {
strncpy(s_address, address.c_str(), sizeof(s_address) - 1);
@@ -67,6 +69,7 @@ void RequestPaymentDialog::show(const std::string& address)
void RequestPaymentDialog::hide()
{
s_open = false;
s_creating_address = false;
s_payment_uri.clear();
if (s_qr_texture != 0) {
FreeQRTexture(s_qr_texture);
@@ -100,17 +103,54 @@ void RequestPaymentDialog::render(App* app)
ImGui::Spacing();
// Address selection
ImGui::Text("%s", TR("request_receive_address"));
std::string addr_display = s_address[0] ? s_address : TR("request_select_address");
if (addr_display.length() > static_cast<size_t>(zAddrLbl.truncate)) {
addr_display = addr_display.substr(0, zAddrFrontLbl.truncate) + "..." + addr_display.substr(addr_display.length() - zAddrBackLbl.truncate);
}
ImGui::SetNextItemWidth(-1);
if (ImGui::BeginCombo("##Address", addr_display.c_str())) {
// Z-addresses
if (!state.z_addresses.empty()) {
const bool noShielded = state.z_addresses.empty();
if (noShielded) {
// Nothing to request to yet — offer to create a shielded address instead of dead-ending.
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(material::OnSurfaceMedium()));
ImGui::TextWrapped("%s", TR("request_no_shielded"));
ImGui::PopStyleColor();
ImGui::Spacing();
if (s_creating_address) {
ImGui::BeginDisabled();
material::TactileButton(TR("generating"), ImVec2(0, 0), S.resolveFont(actionBtn.font));
ImGui::EndDisabled();
} else {
ImGui::BeginDisabled(!app->isConnected());
if (material::TactileButton(TR("request_create_shielded"), ImVec2(0, 0), S.resolveFont(actionBtn.font))) {
if (app->isConnected()) {
s_creating_address = true;
app->createNewZAddress([](const std::string& addr) {
s_creating_address = false;
if (addr.empty()) {
Notifications::instance().error(TR("failed_create_shielded"));
} else {
Notifications::instance().success(TR("new_shielded_created"));
// Pre-select the new address if the dialog is still open and none chosen.
if (s_open && s_address[0] == '\0') {
strncpy(s_address, addr.c_str(), sizeof(s_address) - 1);
s_address[sizeof(s_address) - 1] = '\0';
s_uri_dirty = true;
}
}
});
}
}
ImGui::EndDisabled();
}
} else {
ImGui::Text("%s", TR("request_receive_address"));
std::string addr_display = s_address[0] ? s_address : std::string(TR("request_select_address"));
if (addr_display.length() > static_cast<size_t>(zAddrLbl.truncate)) {
addr_display = addr_display.substr(0, zAddrFrontLbl.truncate) + "..." + addr_display.substr(addr_display.length() - zAddrBackLbl.truncate);
}
if (s_address[0]) {
std::string l = app->getAddressLabel(s_address);
if (!l.empty()) addr_display = l + " " + addr_display;
}
ImGui::SetNextItemWidth(-1);
if (ImGui::BeginCombo("##Address", addr_display.c_str())) {
ImGui::TextDisabled("%s", TR("request_shielded_addrs"));
for (size_t i = 0; i < state.z_addresses.size(); i++) {
const auto& addr = state.z_addresses[i];
@@ -118,19 +158,20 @@ void RequestPaymentDialog::render(App* app)
if (label.length() > static_cast<size_t>(zAddrLbl.truncate)) {
label = label.substr(0, zAddrFrontLbl.truncate) + "..." + label.substr(label.length() - zAddrBackLbl.truncate);
}
if (ImGui::Selectable(label.c_str(), addr.address == s_address)) {
std::string clbl = app->getAddressLabel(addr.address);
std::string shown = (clbl.empty() ? label : (clbl + " " + label)) + "##" + addr.address;
if (ImGui::Selectable(shown.c_str(), addr.address == s_address)) {
strncpy(s_address, addr.address.c_str(), sizeof(s_address) - 1);
s_uri_dirty = true;
}
}
// No mining (transparent) addresses: they only receive mining rewards, so a request to one
// could never be paid.
ImGui::EndCombo();
}
// No mining (transparent) addresses: they only receive mining rewards, so a request to one
// could never be paid.
ImGui::EndCombo();
}
ImGui::Spacing();
// Amount (optional)