feat(ui): donate tab in Help overlay

Help button now shows a two-tab overlay:
- Info: existing about/version content
- Donate: donate flow to the dev address (plm1qdq3…) with free amount
  input, prepare preview, and confirm-and-broadcast button

New MainWindowViewModel.Donate.cs partial: DonateAmount / DonatePreview
/ HasPendingDonate observable properties, PrepareDonateCommand (builds
the tx with hardcoded dev address via the existing TransactionFactory),
ConfirmDonateCommand (broadcasts and syncs). Donate state is reset on
overlay close. Six-language Loc keys added (help.tab.info/donate,
donate.desc/dev.address/amount/prepare/confirm).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-17 08:40:02 +02:00
parent 9744619eb4
commit b13b66160c
4 changed files with 171 additions and 6 deletions
+13
View File
@@ -60,6 +60,19 @@ public sealed class Loc
"Portefeuille SPV pour la cryptomonnaie Palladium (PLM).", "Portefeuille SPV pour la cryptomonnaie Palladium (PLM).",
"Carteira SPV para a criptomoeda Palladium (PLM).", "Carteira SPV para a criptomoeda Palladium (PLM).",
"SPV-Wallet für die Kryptowährung Palladium (PLM)."], "SPV-Wallet für die Kryptowährung Palladium (PLM)."],
["help.tab.info"] = ["Info", "Info", "Info", "Info", "Info", "Info"],
["help.tab.donate"] = ["Dona", "Donate", "Donar", "Faire un don", "Doar", "Spenden"],
["donate.desc"] = [
"Se questo wallet ti è utile, considera una piccola donazione allo sviluppatore.",
"If you find this wallet useful, consider a small donation to the developer.",
"Si esta wallet te resulta útil, considera una pequeña donación al desarrollador.",
"Si ce portefeuille vous est utile, envisagez un petit don au développeur.",
"Se esta carteira é útil para você, considere uma pequena doação ao desenvolvedor.",
"Wenn Ihnen dieses Wallet nützlich ist, erwägen Sie eine kleine Spende an den Entwickler."],
["donate.dev.address"] = ["Indirizzo sviluppatore", "Developer address", "Dirección del desarrollador", "Adresse du développeur", "Endereço do desenvolvedor", "Entwickleradresse"],
["donate.amount"] = ["Importo donazione", "Donation amount", "Monto de donación", "Montant du don", "Valor da doação", "Spendenbetrag"],
["donate.prepare"] = ["Prepara donazione", "Prepare donation", "Preparar donación", "Préparer le don", "Preparar doação", "Spende vorbereiten"],
["donate.confirm"] = ["Conferma e invia", "Confirm and send", "Confirmar y enviar", "Confirmer et envoyer", "Confirmar e enviar", "Bestätigen und senden"],
["settings.unit.short"] = ["Unità", "Unit", "Unidad", "Unité", "Unidade", "Einheit"], ["settings.unit.short"] = ["Unità", "Unit", "Unidad", "Unité", "Unidade", "Einheit"],
// Wizard // Wizard
@@ -0,0 +1,96 @@
using System;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using NBitcoin;
using PalladiumWallet.Core.Chain;
using PalladiumWallet.Core.Net;
using PalladiumWallet.Core.Wallet;
namespace PalladiumWallet.App.ViewModels;
public partial class MainWindowViewModel
{
public const string DevAddress = "plm1qdq3gu2zvg9lyr8gxd6yln4wavc5tlp8prmvfay";
[ObservableProperty]
private string donateAmount = "";
[ObservableProperty]
private string donatePreview = "";
[ObservableProperty]
private bool hasPendingDonate;
private BuiltTransaction? _pendingDonate;
[RelayCommand]
private async Task PrepareDonate()
{
_pendingDonate = null;
HasPendingDonate = false;
if (_account is null || _doc?.Cache is null || _lastTransactions is null)
{
DonatePreview = "Sincronizza prima di inviare.";
return;
}
try
{
var destination = BitcoinAddress.Create(DevAddress, PalladiumNetworks.For(Net));
if (!CoinAmount.TryParseIn(DonateAmount.Trim(), _config.Unit, out var amount) || amount <= 0)
{
DonatePreview = "Importo non valido.";
return;
}
if (!decimal.TryParse(SendFeeRate, System.Globalization.NumberStyles.Any,
System.Globalization.CultureInfo.InvariantCulture, out var feeRate) || feeRate <= 0)
feeRate = 1m;
_pendingDonate = new TransactionFactory(_account).Build(
_doc.Cache.Utxos, _lastTransactions, destination, amount, feeRate,
_doc.Cache.NextChangeIndex, sendAll: false);
DonatePreview = $"txid {_pendingDonate.Txid[..16]}… · " +
$"fee {Fmt(_pendingDonate.Fee.Satoshi)} " +
$"({_pendingDonate.Transaction.GetVirtualSize()} vB)" +
(_pendingDonate.Signed ? "" : " · watch-only");
HasPendingDonate = _pendingDonate.Signed;
}
catch (Exception ex)
{
_pendingDonate = null;
HasPendingDonate = false;
DonatePreview = $"Errore: {ex.Message}";
}
await Task.CompletedTask;
}
[RelayCommand]
private async Task ConfirmDonate()
{
if (_pendingDonate is null || _client is null)
return;
try
{
var txid = await _client.BroadcastAsync(_pendingDonate.ToHex());
DonatePreview = $"Grazie! txid: {txid}";
DonateAmount = "";
_pendingDonate = null;
HasPendingDonate = false;
await ConnectAndSync();
}
catch (Exception ex)
{
DonatePreview = $"Errore broadcast: {ex.Message}";
}
}
private void ResetDonate()
{
DonateAmount = "";
DonatePreview = "";
HasPendingDonate = false;
_pendingDonate = null;
}
}
@@ -70,5 +70,5 @@ public partial class MainWindowViewModel
private void OpenHelp() => IsHelpOpen = true; private void OpenHelp() => IsHelpOpen = true;
[RelayCommand] [RelayCommand]
private void CloseHelp() => IsHelpOpen = false; private void CloseHelp() { IsHelpOpen = false; ResetDonate(); }
} }
+61 -5
View File
@@ -1378,12 +1378,68 @@
<TextBlock Text="{Binding Loc[help.title]}" <TextBlock Text="{Binding Loc[help.title]}"
FontSize="18" FontWeight="Bold"/> FontSize="18" FontWeight="Bold"/>
<StackPanel Spacing="4"> <TabControl>
<TextBlock Text="Palladium Wallet" FontSize="16" FontWeight="Bold"/> <!-- Tab: Info -->
<TextBlock Text="{Binding WindowTitle}" FontSize="12" Foreground="{DynamicResource TextSecondaryBrush}"/> <TabItem Header="{Binding Loc[help.tab.info]}">
</StackPanel> <StackPanel Spacing="12" Margin="0,12,0,0">
<StackPanel Spacing="4">
<TextBlock Text="Palladium Wallet" FontSize="16" FontWeight="Bold"/>
<TextBlock Text="{Binding WindowTitle}" FontSize="12"
Foreground="{DynamicResource TextSecondaryBrush}"/>
</StackPanel>
<TextBlock Text="{Binding Loc[help.info]}" TextWrapping="Wrap"/>
</StackPanel>
</TabItem>
<TextBlock Text="{Binding Loc[help.info]}" TextWrapping="Wrap"/> <!-- Tab: Donate -->
<TabItem Header="{Binding Loc[help.tab.donate]}">
<StackPanel Spacing="12" Margin="0,12,0,0">
<TextBlock Text="{Binding Loc[donate.desc]}" TextWrapping="Wrap"
Foreground="{DynamicResource TextSecondaryBrush}"/>
<!-- Dev address (read-only) -->
<StackPanel Spacing="4">
<TextBlock Text="{Binding Loc[donate.dev.address]}" FontSize="11"
Foreground="{DynamicResource TextSecondaryBrush}"/>
<SelectableTextBlock Text="{x:Static vm:MainWindowViewModel.DevAddress}"
FontFamily="monospace" FontSize="11"
TextWrapping="Wrap"/>
</StackPanel>
<!-- Amount input -->
<StackPanel Spacing="4">
<TextBlock Text="{Binding Loc[donate.amount]}" FontSize="11"
Foreground="{DynamicResource TextSecondaryBrush}"/>
<Grid ColumnDefinitions="*,Auto">
<TextBox Grid.Column="0"
Text="{Binding DonateAmount}"
PlaceholderText="{Binding UnitLabel}"/>
<TextBlock Grid.Column="1" Text="{Binding UnitLabel}"
VerticalAlignment="Center" Margin="8,0,0,0"
Foreground="{DynamicResource TextSecondaryBrush}"/>
</Grid>
</StackPanel>
<!-- Preview -->
<SelectableTextBlock Text="{Binding DonatePreview}"
IsVisible="{Binding DonatePreview,
Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
FontFamily="monospace" FontSize="11"
TextWrapping="Wrap"
Foreground="{DynamicResource TextSecondaryBrush}"/>
<!-- Buttons -->
<StackPanel Orientation="Horizontal" Spacing="8" HorizontalAlignment="Right">
<Button Content="{Binding Loc[donate.confirm]}"
Classes="accent"
IsEnabled="{Binding HasPendingDonate}"
Command="{Binding ConfirmDonateCommand}"/>
<Button Content="{Binding Loc[donate.prepare]}"
Command="{Binding PrepareDonateCommand}"/>
</StackPanel>
</StackPanel>
</TabItem>
</TabControl>
<Button Content="{Binding Loc[addr.close]}" <Button Content="{Binding Loc[addr.close]}"
HorizontalAlignment="Right" HorizontalAlignment="Right"