// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 #include "request_payment_dialog.h" #include "../../app.h" #include "../../util/i18n.h" #include "../../util/payment_uri.h" #include "../../util/address_validation.h" #include "../notifications.h" #include "../schema/ui_schema.h" #include "../widgets/qr_code.h" #include "../widgets/copy_field.h" #include "../material/draw_helpers.h" #include "imgui.h" #include #include #include namespace dragonx { namespace ui { // Static state static bool s_open = false; static char s_address[512] = ""; static double s_amount = 0.0; static char s_memo[512] = ""; static char s_label[128] = ""; static std::string s_payment_uri; 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() { // Route through the shared builder so this dialog and the receive tab can't diverge on scheme/encoding. return dragonx::util::buildPaymentUri(s_address, s_amount, s_label, s_memo); } void RequestPaymentDialog::show(const std::string& address) { s_open = true; s_amount = 0.0; s_memo[0] = '\0'; 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); s_address[sizeof(s_address) - 1] = '\0'; } else { s_address[0] = '\0'; } // Free old QR texture if (s_qr_texture != 0) { FreeQRTexture(s_qr_texture); s_qr_texture = 0; } } void RequestPaymentDialog::hide() { s_open = false; s_creating_address = false; s_payment_uri.clear(); if (s_qr_texture != 0) { FreeQRTexture(s_qr_texture); s_qr_texture = 0; } } void RequestPaymentDialog::render(App* app) { if (!s_open) return; auto& S = schema::UI(); auto win = S.window("dialogs.request-payment"); auto zAddrLbl = S.label("dialogs.request-payment", "z-addr-label"); auto zAddrFrontLbl = S.label("dialogs.request-payment", "z-addr-front-label"); auto zAddrBackLbl = S.label("dialogs.request-payment", "z-addr-back-label"); auto amountInput = S.input("dialogs.request-payment", "amount-input"); auto memoInput = S.input("dialogs.request-payment", "memo-input"); auto qr = S.drawElement("dialogs.request-payment", "qr-code"); auto actionBtn = S.button("dialogs.request-payment", "action-button"); material::OverlayDialogSpec ov; ov.title = TR("request_title"); ov.p_open = &s_open; ov.style = material::OverlayStyle::BlurFloat; ov.cardWidth = win.width; ov.idSuffix = "requestpay"; if (material::BeginOverlayDialog(ov)) { const auto& state = app->getWalletState(); ImGui::TextWrapped("%s", TR("request_description")); ImGui::Spacing(); // Address selection 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([app](const std::string& addr) { s_creating_address = false; if (addr.empty()) { Notifications::instance().error(app->lastAddressError().empty() ? std::string(TR("failed_create_shielded")) : app->lastAddressError()); } 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(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]; std::string label = addr.address; if (label.length() > static_cast(zAddrLbl.truncate)) { label = label.substr(0, zAddrFrontLbl.truncate) + "..." + label.substr(label.length() - zAddrBackLbl.truncate); } 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(); } } ImGui::Spacing(); // Amount (optional) ImGui::Text("%s", TR("request_amount")); ImGui::SetNextItemWidth(amountInput.width * Layout::dpiScale()); if (ImGui::InputDouble("##Amount", &s_amount, 0.1, 1.0, "%.8f")) { s_uri_dirty = true; } ImGui::SameLine(); ImGui::TextDisabled("DRGX"); ImGui::Spacing(); // Label (optional) if (material::LabeledInput(TR("request_label"), "##Label", s_label, sizeof(s_label))) { s_uri_dirty = true; } ImGui::Spacing(); // Memo (optional, only for z-addr) bool is_zaddr = (s_address[0] == 'z'); if (is_zaddr) { if (material::LabeledInputMultiline(TR("request_memo"), "##Memo", s_memo, sizeof(s_memo), ImVec2(-1, memoInput.height > 0 ? memoInput.height : 60))) { s_uri_dirty = true; } } else { s_memo[0] = '\0'; // Clear memo for t-addr } ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); // Build and display payment URI if (s_uri_dirty && s_address[0] != '\0') { s_payment_uri = buildPaymentUri(); // Generate new QR code if (s_qr_texture != 0) { FreeQRTexture(s_qr_texture); } s_qr_texture = GenerateQRTexture(s_payment_uri.c_str(), &s_qr_width, &s_qr_height); s_uri_dirty = false; } // QR Code display if (s_qr_texture != 0) { RenderQRCode(s_qr_texture, qr.size > 0 ? qr.size : 200); } ImGui::Spacing(); // Payment URI display if (!s_payment_uri.empty() && s_address[0] != '\0') { ImGui::Text("%s", TR("request_payment_uri")); // Bordered read-only field: bounds the raw drgx: URI within a box (it horizontal-scrolls if // long) rather than 4-char-chunking it like an address or letting a plain field overflow. // The "Copy URI" button below copies the full value. { char uriBuf[2048]; snprintf(uriBuf, sizeof(uriBuf), "%s", s_payment_uri.c_str()); ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); ImGui::InputText("##URI", uriBuf, sizeof(uriBuf), ImGuiInputTextFlags_ReadOnly); } ImGui::Spacing(); // Footer buttons: size each to ITS OWN label (action-button.width is a floor, not the size) // so "Copy Full Address" never clips its final letter — mirrors qr_popup_dialog's per-label // sizing. Font metrics are already DPI-scaled, so don't multiply by dpiScale() here. ImFont* btnFont = S.resolveFont(actionBtn.font); ImGui::PushFont(btnFont); const float btnPad = ImGui::GetStyle().FramePadding.x * 2.0f + 12.0f; // small margin float w_uri = ImGui::CalcTextSize(TR("request_copy_uri")).x + btnPad; float w_addr = ImGui::CalcTextSize(TR("copy_address")).x + btnPad; ImGui::PopFont(); if (w_uri < actionBtn.width) w_uri = actionBtn.width; if (w_addr < actionBtn.width) w_addr = actionBtn.width; // Center the two-button copy row via the shared helper (no extra divider; the Separator // above already frames this region). Total = both button widths + the ItemSpacing between. material::BeginOverlayDialogFooter(w_uri + w_addr + ImGui::GetStyle().ItemSpacing.x, false); // Copy URI button if (material::TactileButton(TR("request_copy_uri"), ImVec2(w_uri, 0), btnFont)) { ImGui::SetClipboardText(s_payment_uri.c_str()); Notifications::instance().success(TR("request_uri_copied")); } ImGui::SameLine(); if (material::TactileButton(TR("copy_address"), ImVec2(w_addr, 0), btnFont)) { ImGui::SetClipboardText(s_address); Notifications::instance().success(TR("address_copied")); } } ImGui::Spacing(); // Close button — sized to its own label with the same floor. { ImFont* btnFont = S.resolveFont(actionBtn.font); ImGui::PushFont(btnFont); const float btnPad = ImGui::GetStyle().FramePadding.x * 2.0f + 12.0f; float w_close = ImGui::CalcTextSize(TR("close")).x + btnPad; ImGui::PopFont(); if (w_close < actionBtn.width) w_close = actionBtn.width; // Center the lone Close button via the shared helper (no extra divider — matches the // existing tight spacing above it). material::BeginOverlayDialogFooter(w_close, false); if (material::TactileButton(TR("close"), ImVec2(w_close, 0), btnFont)) { s_open = false; } } material::EndOverlayDialog(); } // Cleanup on close if (!s_open) { s_payment_uri.clear(); } if (!s_open && s_qr_texture != 0) { FreeQRTexture(s_qr_texture); s_qr_texture = 0; } } } // namespace ui } // namespace dragonx