feat(app): multi-wallet chooser, confirm-password, encrypt toggle

Multi-wallet (§8):
- AppPaths.WalletFiles(net) enumerates all *.wallet.json in the wallets
  dir; WizardStartOpen shows a chooser step when more than one is found
- New StepChooseWallet + ChooseWalletCommand; Back from StepOpen returns
  to chooser (or to StepStart when only one wallet)
- WalletFileEntry record for the chooser ItemsControl

Password step (Electrum style):
- ConfirmPasswordInput: password must be typed twice before creating
- EncryptWallet checkbox (default true); when unchecked, password is
  null (plaintext file) with an explicit warning
- Validation: empty password with encryption on → msg.password.required;
  mismatch → msg.password.mismatch
- ConfirmPasswordInput cleared on wallet open and wizard back

AppPaths:
- DefaultDataRoot() is now platform-aware: %APPDATA%\PalladiumWallet on
  Windows, ~/.palladium-wallet on Linux/macOS (matches §8 convention)
- Legacy root fallback removed (no prior releases to migrate from)

i18n: wiz.choose.title, wiz.password.confirm, wiz.password.encrypt,
wiz.password.encrypt.hint, msg.choose.wallet, msg.password.required,
msg.password.mismatch — all 6 languages
This commit is contained in:
2026-06-12 10:13:09 +02:00
parent a8a97f09b7
commit 7f2759b2fc
4 changed files with 140 additions and 23 deletions
+72 -6
View File
@@ -40,6 +40,9 @@ public sealed record AddressInfo(
/// <summary>Contatto in rubrica: nome + indirizzo blockchain.</summary>
public sealed record ContactEntry(string Name, string Address);
/// <summary>Voce della lista di scelta wallet: nome file + percorso completo.</summary>
public sealed record WalletFileEntry(string Name, string Path);
/// <summary>
/// ViewModel unico dell'applicazione (wizard §15 ridotto + dashboard):
/// pannello di setup (crea/ripristina/apri) e pannello wallet
@@ -167,6 +170,7 @@ public partial class MainWindowViewModel : ViewModelBase
public const string StepDataLocation = "data-location";
public const string StepStart = "start";
public const string StepChooseWallet = "choose-wallet";
public const string StepOpen = "open";
public const string StepShowSeed = "show-seed";
public const string StepConfirmSeed = "confirm-seed";
@@ -177,6 +181,7 @@ public partial class MainWindowViewModel : ViewModelBase
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsStepDataLocation))]
[NotifyPropertyChangedFor(nameof(IsStepStart))]
[NotifyPropertyChangedFor(nameof(IsStepChooseWallet))]
[NotifyPropertyChangedFor(nameof(IsStepOpen))]
[NotifyPropertyChangedFor(nameof(IsStepShowSeed))]
[NotifyPropertyChangedFor(nameof(IsStepConfirmSeed))]
@@ -187,6 +192,7 @@ public partial class MainWindowViewModel : ViewModelBase
public bool IsStepDataLocation => SetupStep == StepDataLocation;
public bool IsStepStart => SetupStep == StepStart;
public bool IsStepChooseWallet => SetupStep == StepChooseWallet;
public bool IsStepOpen => SetupStep == StepOpen;
public bool IsStepShowSeed => SetupStep == StepShowSeed;
public bool IsStepConfirmSeed => SetupStep == StepConfirmSeed;
@@ -212,6 +218,14 @@ public partial class MainWindowViewModel : ViewModelBase
[ObservableProperty]
private string passwordInput = "";
/// <summary>Conferma password alla creazione (stile Electrum: digitata due volte).</summary>
[ObservableProperty]
private string confirmPasswordInput = "";
/// <summary>Se true il file wallet viene cifrato con la password (default, stile Electrum).</summary>
[ObservableProperty]
private bool encryptWallet = true;
// ---- pannello wallet ----
[ObservableProperty]
@@ -280,6 +294,9 @@ public partial class MainWindowViewModel : ViewModelBase
public ObservableCollection<AddressRow> Addresses { get; } = [];
/// <summary>Wallet disponibili nella cartella della rete, per la schermata di scelta.</summary>
public ObservableCollection<WalletFileEntry> WalletList { get; } = [];
// ---- tab indirizzi ----
[ObservableProperty]
@@ -457,8 +474,8 @@ public partial class MainWindowViewModel : ViewModelBase
private void RefreshSetupState()
{
SetupStep = StepStart;
MnemonicInput = ConfirmMnemonicInput = PassphraseInput = PasswordInput = "";
WalletFileExists = WalletStore.Exists(AppPaths.DefaultWalletPath(Net));
MnemonicInput = ConfirmMnemonicInput = PassphraseInput = PasswordInput = ConfirmPasswordInput = "";
WalletFileExists = AppPaths.WalletFiles(Net).Count > 0;
StatusMessage = WalletFileExists
? Loc.Tr("msg.welcome.existing")
: Loc.Tr("msg.welcome.new");
@@ -535,6 +552,31 @@ public partial class MainWindowViewModel : ViewModelBase
[RelayCommand]
private void WizardStartOpen()
{
// Con più wallet nella cartella della rete si sceglie quale aprire;
// con uno solo si va dritti alla password (multi-wallet §8).
var files = AppPaths.WalletFiles(Net);
if (files.Count > 1)
{
WalletList.Clear();
foreach (var path in files)
WalletList.Add(new WalletFileEntry(Path.GetFileName(path), path));
SetupStep = StepChooseWallet;
StatusMessage = Loc.Tr("msg.choose.wallet");
return;
}
_pendingOpenPath = files.Count == 1 ? files[0] : AppPaths.DefaultWalletPath(Net);
PasswordInput = "";
SetupStep = StepOpen;
StatusMessage = Loc.Tr("msg.open.password");
}
/// <summary>Sceglie quale wallet aprire dalla lista e passa alla password.</summary>
[RelayCommand]
private void ChooseWallet(WalletFileEntry? entry)
{
if (entry is null)
return;
_pendingOpenPath = entry.Path;
PasswordInput = "";
SetupStep = StepOpen;
StatusMessage = Loc.Tr("msg.open.password");
@@ -600,7 +642,8 @@ public partial class MainWindowViewModel : ViewModelBase
[RelayCommand]
private void WizardNextFromPassphrase()
{
PasswordInput = "";
PasswordInput = ConfirmPasswordInput = "";
EncryptWallet = true;
SetupStep = StepPassword;
StatusMessage = Loc.Tr("msg.password.info");
}
@@ -610,7 +653,8 @@ public partial class MainWindowViewModel : ViewModelBase
{
SetupStep = SetupStep switch
{
StepOpen or StepShowSeed or StepWords => StepStart,
StepOpen => WalletList.Count > 1 ? StepChooseWallet : StepStart,
StepChooseWallet or StepShowSeed or StepWords => StepStart,
StepConfirmSeed => StepShowSeed,
StepPassphrase => _isRestoreFlow ? StepWords : StepConfirmSeed,
StepPassword => StepPassphrase,
@@ -623,6 +667,29 @@ public partial class MainWindowViewModel : ViewModelBase
[RelayCommand]
private void CreateOrRestore()
{
// Scelta cifratura stile Electrum: con cifratura attiva serve una
// password non vuota, digitata due volte e coincidente. Solo se la
// cifratura è disattivata il file resta in chiaro (avviso esplicito).
string? password;
if (EncryptWallet)
{
if (string.IsNullOrEmpty(PasswordInput))
{
StatusMessage = Loc.Tr("msg.password.required");
return;
}
if (PasswordInput != ConfirmPasswordInput)
{
StatusMessage = Loc.Tr("msg.password.mismatch");
return;
}
password = PasswordInput;
}
else
{
password = null;
}
try
{
var (doc, account) = WalletLoader.NewFromMnemonic(
@@ -634,7 +701,6 @@ public partial class MainWindowViewModel : ViewModelBase
var path = AppPaths.DefaultWalletPath(Net);
for (var n = 2; WalletStore.Exists(path); n++)
path = Path.Combine(AppPaths.WalletsDir(Net), $"wallet-{n}.wallet.json");
var password = string.IsNullOrEmpty(PasswordInput) ? null : PasswordInput;
WalletStore.Save(doc, path, password);
OpenLoaded(doc, account, path, password);
}
@@ -710,7 +776,7 @@ public partial class MainWindowViewModel : ViewModelBase
_account = account;
_walletPath = path;
_password = password;
MnemonicInput = ConfirmMnemonicInput = PassphraseInput = PasswordInput = "";
MnemonicInput = ConfirmMnemonicInput = PassphraseInput = PasswordInput = ConfirmPasswordInput = "";
SetupStep = StepStart;
NetworkInfo = $"{doc.Network} · {doc.ScriptKind} · m/{doc.AccountPath}"