feat(transfer): add Send-tab guards, a correct TxID label + copy, inline reasons

The address-transfer dialog (own-address moves) enabled Confirm on amount alone,
so a disconnected / syncing / locked wallet failed only at submit. It now
mirrors the Send tab's guards (strictly more restrictive — the transfer logic is
unchanged) and re-checks live state before broadcast, shows an inline reason
when blocked (enter amount / not connected / syncing / locked), and labels the
result as a "Transaction ID" (not "Operation ID") with a copy button.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-10-18 11:44:19 -05:00
co-authored by Claude Opus 4.8
parent fb281714f9
commit adf61e43d6
2 changed files with 37 additions and 7 deletions
+35 -7
View File
@@ -141,6 +141,14 @@ public:
double newFromBal = s_info.fromBalance - totalDeduct; double newFromBal = s_info.fromBalance - totalDeduct;
double newToBal = s_info.toBalance + amount; double newToBal = s_info.toBalance + amount;
bool amountValid = amount > 0 && totalDeduct <= s_info.fromBalance; bool amountValid = amount > 0 && totalDeduct <= s_info.fromBalance;
// Mirror the Send tab's guards (this dialog previously enabled Confirm on amount alone, so a
// disconnected / syncing / locked wallet would fail only at submit). These are own-address
// moves, but gating is strictly more restrictive — the transfer logic below is unchanged.
const auto& ws = s_app->getWalletState();
const bool connected = s_app->isConnected();
const bool syncing = ws.sync.syncing;
const bool liteLocked = s_app->liteWallet() && ws.isLocked();
const bool canTransfer = amountValid && !s_sending && connected && !syncing && !liteLocked;
ImGui::Separator(); ImGui::Separator();
ImGui::Spacing(); ImGui::Spacing();
@@ -167,6 +175,19 @@ public:
ImGui::TextWrapped("%s", TR("insufficient_funds")); ImGui::TextWrapped("%s", TR("insufficient_funds"));
ImGui::PopStyleColor(); ImGui::PopStyleColor();
} }
if (amount <= 0.0) {
ImGui::Spacing();
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Error()));
ImGui::TextWrapped("%s", TR("send_tooltip_enter_amount"));
ImGui::PopStyleColor();
} else if (amountValid && (!connected || syncing || liteLocked)) {
ImGui::Spacing();
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Warning()));
ImGui::TextWrapped("%s", !connected ? TR("send_tooltip_not_connected")
: syncing ? TR("send_tooltip_syncing")
: TR("transfer_tooltip_locked"));
ImGui::PopStyleColor();
}
if (amountValid && newFromBal < 1e-9) { if (amountValid && newFromBal < 1e-9) {
ImGui::Spacing(); ImGui::Spacing();
// Full-balance send: same warning-icon treatment as the de-shielding header above, // Full-balance send: same warning-icon treatment as the de-shielding header above,
@@ -204,13 +225,13 @@ public:
bool outConfirm = false; bool outConfirm = false;
bool outClose = false; bool outClose = false;
DialogActionFooter(s_sending ? sendingLabel : confirmLabel, DialogActionFooter(s_sending ? sendingLabel : confirmLabel,
amountValid && !s_sending, canTransfer,
cancelLabel, outConfirm, outClose, btnW); cancelLabel, outConfirm, outClose, btnW);
if (outClose) { if (outClose) {
s_open = false; s_open = false;
} }
if (outConfirm) { if (outConfirm && canTransfer) { // re-check live state (the dialog persists across frames)
s_sending = true; s_sending = true;
s_app->sendTransaction(s_info.fromAddr, s_info.toAddr, s_app->sendTransaction(s_info.fromAddr, s_info.toAddr,
amount, s_fee, "", amount, s_fee, "",
@@ -296,12 +317,19 @@ private:
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("transfer_sent_desc")); Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("transfer_sent_desc"));
ImGui::Spacing(); ImGui::Spacing();
{ {
char buf[128]; // The result is a TRANSACTION id, not an "Operation ID" — label it correctly and make
snprintf(buf, sizeof(buf), TR("shield_operation_id"), // it copyable.
s_resultMsg.size() > 40 std::string disp = s_resultMsg.size() > 40
? (s_resultMsg.substr(0, 20) + "..." + s_resultMsg.substr(s_resultMsg.size() - 10)).c_str() ? (s_resultMsg.substr(0, 20) + "..." + s_resultMsg.substr(s_resultMsg.size() - 10))
: s_resultMsg.c_str()); : s_resultMsg;
char buf[160];
snprintf(buf, sizeof(buf), TR("transfer_txid_label"), disp.c_str());
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), buf); Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), buf);
ImGui::SameLine();
if (ImGui::SmallButton(TR("grpb_copy"))) {
ImGui::SetClipboardText(s_resultMsg.c_str());
Notifications::instance().info(TR("copied_to_clipboard"));
}
} }
} else { } else {
ImGui::Spacing(); ImGui::Spacing();
+2
View File
@@ -1118,6 +1118,8 @@ void I18n::loadBuiltinEnglish()
strings_["confirm_transfer"] = "Confirm Transfer"; strings_["confirm_transfer"] = "Confirm Transfer";
strings_["transfer_sent"] = "Transfer Sent"; strings_["transfer_sent"] = "Transfer Sent";
strings_["transfer_sent_desc"] = "Your transfer has been submitted to the network."; strings_["transfer_sent_desc"] = "Your transfer has been submitted to the network.";
strings_["transfer_txid_label"] = "Transaction ID: %s";
strings_["transfer_tooltip_locked"] = "Wallet is locked — unlock it to transfer";
strings_["transfer_failed"] = "Transfer Failed"; strings_["transfer_failed"] = "Transfer Failed";
// Misc dialog strings // Misc dialog strings