Begin to optimize adding hardware wallets for the 99% case.
ADD
WalletWasabi.Fluent/Converters/WalletTypeImageConverter.cs
+using Avalonia;
+using Avalonia.Data.Converters;
+using Avalonia.Media.Imaging;
+using Avalonia.Platform;
+using System;
+using System.Globalization;
+using WalletWasabi.Fluent.ViewModels.AddWallet;
+
+namespace WalletWasabi.Fluent.Converters
+{
+ public class WalletTypeImageConverter : IValueConverter
+ {
+ public static readonly WalletTypeImageConverter Instance = new WalletTypeImageConverter();
+
+ private WalletTypeImageConverter()
+ {
+ }
+
+ object? IValueConverter.Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is WalletType type)
+ {
+ Uri uri = new("avares://WalletWasabi.Fluent/Assets/HardwareIcons/generic.png");
+
+ switch (type)
+ {
+ case WalletType.Coldcard:
+ uri = new("avares://WalletWasabi.Fluent/Assets/HardwareIcons/coldcard.png");
+ break;
+
+ case WalletType.Trezor:
+ uri = new("avares://WalletWasabi.Fluent/Assets/HardwareIcons/trezor.png");
+ break;
+
+ case WalletType.Ledger:
+ uri = new("avares://WalletWasabi.Fluent/Assets/HardwareIcons/ledger.png");
+ break;
+
+ case WalletType.Normal:
+ // todo normal wallet image.
+ break;
+ }
+
+ var assets = AvaloniaLocator.Current.GetService<IAssetLoader>();
+
+ using var image = assets.Open(uri);
+ return new Bitmap(image);
+ }
+
+ return null;
+ }
+
+ object IValueConverter.ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}

ADD
WalletWasabi.Fluent/ViewModels/AddWallet/AddedWalletPageViewModel.cs
+using ReactiveUI;
+using WalletWasabi.Fluent.ViewModels.Navigation;
+
+namespace WalletWasabi.Fluent.ViewModels.AddWallet
+{
+ public enum WalletType
+ {
+ Normal,
+ Hardware,
+ Coldcard,
+ Trezor,
+ Ledger
+ }
+
+ public class AddedWalletPageViewModel : RoutableViewModel
+ {
+ public AddedWalletPageViewModel(NavigationStateViewModel navigationState, string walletName, WalletType type) : base(navigationState, NavigationTarget.DialogScreen)
+ {
+ WalletName = walletName;
+ Type = type;
+
+ NextCommand = ReactiveCommand.Create(() =>
+ {
+ ClearNavigation();
+ });
+ }
+
+ public WalletType Type { get; }
+
+ public string WalletName { get; } = "";
+ }
+}

MODIFY
WalletWasabi.Fluent/ViewModels/AddWallet/ConnectHardwareWalletViewModel.cs
WalletWasabi.Fluent/ViewModels/AddWallet/ConnectHardwareWalletViewModel.cs
+using NBitcoin;
+using ReactiveUI;
using System;
-using System.Collections.ObjectModel;
-using System.Diagnostics;
-using System.IO;
+using System.Collections.Generic;
using System.Linq;
using System.Reactive;
+using System.Reactive.Concurrency;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Threading;
using System.Threading.Tasks;
-using DynamicData;
-using NBitcoin;
-using ReactiveUI;
using WalletWasabi.Blockchain.Keys;
using WalletWasabi.Fluent.ViewModels.Navigation;
using WalletWasabi.Hwi;
-using WalletWasabi.Hwi.Models;
-using WalletWasabi.Logging;
using WalletWasabi.Wallets;
-using WalletWasabi.Fluent.ViewModels.Navigation;
-using HardwareWalletViewModel = WalletWasabi.Gui.Tabs.WalletManager.HardwareWallets.HardwareWalletViewModel;
namespace WalletWasabi.Fluent.ViewModels.AddWallet
{
- public class ConnectHardwareWalletViewModel : RoutableViewModel
+ public class HardwareDetectionState
{
- private HardwareWalletViewModel? _selectedHardwareWallet;
+ private HwiClient _client;
- public ConnectHardwareWalletViewModel(NavigationStateViewModel navigationState, string walletName, Network network, WalletManager walletManager)
- : base(navigationState, NavigationTarget.DialogScreen)
+ public HardwareDetectionState(string walletName, WalletManager walletManager, Network network)
{
WalletName = walletName;
WalletManager = walletManager;
- HwiClient = new HwiClient(network);
- HardwareWallets = new ObservableCollection<HardwareWalletViewModel>();
-
- OpenBrowserCommand = ReactiveCommand.CreateFromTask<string>(IoHelpers.OpenBrowserAsync);
-
- var nextCommandIsExecute =
- this.WhenAnyValue(x => x.SelectedHardwareWallet)
- .ObserveOn(RxApp.MainThreadScheduler)
- .Select(x => x?.HardwareWalletInfo.Fingerprint is { } && x.HardwareWalletInfo.IsInitialized());
- NextCommand = ReactiveCommand.CreateFromTask(ConnectSelectedHardwareWallet,nextCommandIsExecute);
-
- this.WhenAnyValue(x => x.SelectedHardwareWallet)
- .ObserveOn(RxApp.MainThreadScheduler)
- .Where(x => x is { } && !x.HardwareWalletInfo.IsInitialized() && x.HardwareWalletInfo.Model != HardwareWalletModels.Coldcard)
- .Subscribe(async x =>
- {
- if (DisposeCts is null)
- {
- return;
- }
-
- using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(21));
- using var initCts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, DisposeCts.Token);
-
- // Trezor T doesn't require interactive mode.
- var interactiveMode = !(x!.HardwareWalletInfo.Model == HardwareWalletModels.Trezor_T || x.HardwareWalletInfo.Model == HardwareWalletModels.Trezor_T_Simulator);
-
- try
- {
- // TODO: Notify the user to check the device
- await HwiClient.SetupAsync(x.HardwareWalletInfo.Model, x.HardwareWalletInfo.Path, interactiveMode, initCts.Token);
- }
- catch (Exception ex)
- {
- Logger.LogError(ex);
- }
- });
+ Network = network;
+ _client = new HwiClient(network);
- this.WhenNavigatedTo(() =>
- {
- DisposeCts = new CancellationTokenSource();
-
- StartDetection();
-
- return Disposable.Create(() =>
- {
- DisposeCts.Cancel();
- DisposeCts.Dispose();
- });
- });
+ Devices = Enumerable.Empty<Hwi.Models.HwiEnumerateEntry>();
}
- private string WalletName { get; }
-
- private WalletManager WalletManager { get; }
+ public IEnumerable<Hwi.Models.HwiEnumerateEntry> Devices { get; private set; }
- private HwiClient HwiClient { get; }
+ public Hwi.Models.HwiEnumerateEntry? SelectedDevice { get; set; }
- private Task? DetectionTask { get; set; }
+ public WalletManager WalletManager { get; }
- private CancellationTokenSource? DetectionCts { get; set; }
+ public Network Network { get; }
- private CancellationTokenSource? DisposeCts { get; set; }
+ public string WalletName { get; }
- public HardwareWalletViewModel? SelectedHardwareWallet
+ public async Task<KeyManager> GenerateWalletAsync()
{
- get => _selectedHardwareWallet;
- set => this.RaiseAndSetIfChanged(ref _selectedHardwareWallet, value);
- }
+ var selectedDevice = SelectedDevice;
- public ObservableCollection<HardwareWalletViewModel> HardwareWallets { get; }
-
- public ReactiveCommand<string, Unit> OpenBrowserCommand { get; }
-
- private async Task ConnectSelectedHardwareWallet()
- {
- if (SelectedHardwareWallet?.HardwareWalletInfo.Fingerprint is null)
+ if(selectedDevice is null)
{
- return;
+ throw new Exception("Cannot be null.");
}
- try
- {
- IsBusy = true;
- await StopDetectionAsync();
-
- var newWallet = await Task.Run(async () =>
- {
- var fingerPrint = (HDFingerprint)SelectedHardwareWallet.HardwareWalletInfo.Fingerprint;
+ var fingerPrint = (HDFingerprint)selectedDevice.Fingerprint;
using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(3));
- var extPubKey = await HwiClient.GetXpubAsync(SelectedHardwareWallet.HardwareWalletInfo.Model, SelectedHardwareWallet.HardwareWalletInfo.Path, KeyManager.DefaultAccountKeyPath, cts.Token);
+ var extPubKey = await _client.GetXpubAsync(selectedDevice.Model, selectedDevice.Path, KeyManager.DefaultAccountKeyPath, cts.Token).ConfigureAwait(false);
var path = WalletManager.WalletDirectories.GetWalletFilePaths(WalletName).walletFilePath;
return KeyManager.CreateNewHardwareWalletWatchOnly(fingerPrint, extPubKey, path);
- });
-
- WalletManager.AddWallet(newWallet);
-
- // Close dialog
- ClearNavigation();
}
- catch (Exception ex)
- {
- // TODO: Notify the user about the error
- Logger.LogError(ex);
- // Restart detection
- StartDetection();
- }
- finally
+ public async Task EnumerateHardwareWalletsAsync(CancellationToken token)
{
- IsBusy = false;
+ Devices = (await _client.EnumerateAsync(token).ConfigureAwait(false))
+ .Where(wallet => WalletManager.GetWallets()
+ .Any(x=>x.KeyManager.MasterFingerprint == wallet.Fingerprint) == false);
}
}
- private async Task StopDetectionAsync()
- {
- if (DetectionTask is { } task && DetectionCts is { } cts)
+ public class ConnectHardwareWalletViewModel : RoutableViewModel
{
- cts.Cancel();
- await task;
- }
- }
-
- private void StartDetection()
+ public ConnectHardwareWalletViewModel(NavigationStateViewModel navigationState, string walletName, Network network, WalletManager walletManager, bool trySkipPage = true)
+ : base(navigationState, NavigationTarget.DialogScreen)
{
- if (DisposeCts is { } cts)
+ if (trySkipPage)
{
- DetectionCts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token);
- DetectionTask = HardwareWalletDetectionAsync(DetectionCts);
- }
- }
+ IsBusy = true;
- private async Task HardwareWalletDetectionAsync(CancellationTokenSource detectionCts)
- {
- while (!detectionCts.IsCancellationRequested)
+ this.WhenNavigatedTo(() =>
{
- var sw = Stopwatch.StartNew();
-
- try
+ RxApp.MainThreadScheduler.Schedule(async () =>
{
- using var timeoutCts = new CancellationTokenSource(TimeSpan.FromMinutes(3));
-
- var detectedHardwareWallets = (await HwiClient.EnumerateAsync(timeoutCts.Token).ConfigureAwait(false)).Select(x => new HardwareWalletViewModel(x)).ToArray();
- detectionCts.Token.ThrowIfCancellationRequested();
+ var detectionState = new HardwareDetectionState(walletName, walletManager, network);
- // The wallets that already exist in the software.
- var alreadyExistingWalletsToRemove = detectedHardwareWallets.Where(wallet => WalletManager.GetWallets().Any(x => x.KeyManager.MasterFingerprint == wallet.HardwareWalletInfo.Fingerprint));
- // The wallets that are not detectable since the last enumeration.
- var disconnectedWalletsToRemove = HardwareWallets.Except(detectedHardwareWallets);
+ await detectionState.EnumerateHardwareWalletsAsync(CancellationToken.None);
- var toRemove = alreadyExistingWalletsToRemove.Union(disconnectedWalletsToRemove).ToArray();
- var toAdd = detectedHardwareWallets.Except(toRemove).Except(HardwareWallets);
-
- await Observable.Start(() =>
- {
- // Remove disconnected hardware wallets from the list
- HardwareWallets.RemoveMany(toRemove);
- // Add newly detected hardware wallets
- HardwareWallets.AddRange(toAdd);
+ var deviceCount = detectionState.Devices.Count();
- if(SelectedHardwareWallet is null)
+ if (deviceCount == 0)
{
- SelectedHardwareWallet = HardwareWallets.FirstOrDefault();
+ // navigate to detecting page.
}
- }, RxApp.MainThreadScheduler);
+ else if (deviceCount == 1)
+ {
+ // navigate to detected hw wallet page.
+ detectionState.SelectedDevice = detectionState.Devices.First();
+ NavigateTo(new DetectedHardwareWalletViewModel(navigationState, detectionState), NavigationTarget.DialogScreen);
}
- catch (Exception ex)
- {
- if (ex is not OperationCanceledException)
+ else
{
- Logger.LogError(ex);
+ // Do nothing... stay on this page.
}
+
+ IsBusy = false;
+ });
+
+ return Disposable.Empty;
+ });
}
- // Too fast enumeration causes the detected hardware wallets to be unable to provide the fingerprint.
- // Wait at least 5 seconds between two enumerations.
- sw.Stop();
- if (sw.Elapsed.Milliseconds < 5000)
+ NextCommand = ReactiveCommand.Create(() =>
{
- await Task.Delay(5000 - sw.Elapsed.Milliseconds).ConfigureAwait(false);
- }
- }
+ NavigateTo(new DetectHardwareWalletViewModel(navigationState, walletName, network, walletManager), NavigationTarget.DialogScreen);
+ });
}
+
+ public ReactiveCommand<string, Unit> OpenBrowserCommand { get; }
}
}

ADD
WalletWasabi.Fluent/ViewModels/AddWallet/DetectHardwareWalletViewModel.cs
+using System;
+using System.Collections.ObjectModel;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Reactive;
+using System.Reactive.Disposables;
+using System.Reactive.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using DynamicData;
+using NBitcoin;
+using ReactiveUI;
+using WalletWasabi.Blockchain.Keys;
+using WalletWasabi.Fluent.ViewModels.Navigation;
+using WalletWasabi.Hwi;
+using WalletWasabi.Hwi.Models;
+using WalletWasabi.Logging;
+using WalletWasabi.Wallets;
+using HardwareWalletViewModel = WalletWasabi.Gui.Tabs.WalletManager.HardwareWallets.HardwareWalletViewModel;
+
+namespace WalletWasabi.Fluent.ViewModels.AddWallet
+{
+ public class DetectHardwareWalletViewModel : RoutableViewModel
+ {
+ private HardwareWalletViewModel? _selectedHardwareWallet;
+
+ public DetectHardwareWalletViewModel(NavigationStateViewModel navigationState, string walletName, Network network, WalletManager walletManager)
+ : base(navigationState, NavigationTarget.DialogScreen)
+ {
+ WalletName = walletName;
+ WalletManager = walletManager;
+ HwiClient = new HwiClient(network);
+ HardwareWallets = new ObservableCollection<HardwareWalletViewModel>();
+
+ OpenBrowserCommand = ReactiveCommand.CreateFromTask<string>(IoHelpers.OpenBrowserAsync);
+
+ var nextCommandIsExecute =
+ this.WhenAnyValue(x => x.SelectedHardwareWallet)
+ .ObserveOn(RxApp.MainThreadScheduler)
+ .Select(x => x?.HardwareWalletInfo.Fingerprint is { } && x.HardwareWalletInfo.IsInitialized());
+ NextCommand = ReactiveCommand.CreateFromTask(ConnectSelectedHardwareWallet,nextCommandIsExecute);
+
+ this.WhenAnyValue(x => x.SelectedHardwareWallet)
+ .ObserveOn(RxApp.MainThreadScheduler)
+ .Where(x => x is { } && !x.HardwareWalletInfo.IsInitialized() && x.HardwareWalletInfo.Model != HardwareWalletModels.Coldcard)
+ .Subscribe(async x =>
+ {
+ if (DisposeCts is null)
+ {
+ return;
+ }
+
+ using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(21));
+ using var initCts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, DisposeCts.Token);
+
+ // Trezor T doesn't require interactive mode.
+ var interactiveMode = !(x!.HardwareWalletInfo.Model == HardwareWalletModels.Trezor_T || x.HardwareWalletInfo.Model == HardwareWalletModels.Trezor_T_Simulator);
+
+ try
+ {
+ // TODO: Notify the user to check the device
+ await HwiClient.SetupAsync(x.HardwareWalletInfo.Model, x.HardwareWalletInfo.Path, interactiveMode, initCts.Token);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError(ex);
+ }
+ });
+
+ this.WhenNavigatedTo(() =>
+ {
+ DisposeCts = new CancellationTokenSource();
+
+ StartDetection();
+
+ return Disposable.Create(() =>
+ {
+ DisposeCts.Cancel();
+ DisposeCts.Dispose();
+ });
+ });
+ }
+
+ private string WalletName { get; }
+
+ private WalletManager WalletManager { get; }
+
+ private HwiClient HwiClient { get; }
+
+ private Task? DetectionTask { get; set; }
+
+ private CancellationTokenSource? DetectionCts { get; set; }
+
+ private CancellationTokenSource? DisposeCts { get; set; }
+
+ public HardwareWalletViewModel? SelectedHardwareWallet
+ {
+ get => _selectedHardwareWallet;
+ set => this.RaiseAndSetIfChanged(ref _selectedHardwareWallet, value);
+ }
+
+ public ObservableCollection<HardwareWalletViewModel> HardwareWallets { get; }
+
+ public ReactiveCommand<string, Unit> OpenBrowserCommand { get; }
+
+ private async Task ConnectSelectedHardwareWallet()
+ {
+ if (SelectedHardwareWallet?.HardwareWalletInfo.Fingerprint is null)
+ {
+ return;
+ }
+
+ try
+ {
+ IsBusy = true;
+ await StopDetectionAsync();
+
+ var newWallet = await Task.Run(async () =>
+ {
+ var fingerPrint = (HDFingerprint)SelectedHardwareWallet.HardwareWalletInfo.Fingerprint;
+ using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(3));
+ var extPubKey = await HwiClient.GetXpubAsync(SelectedHardwareWallet.HardwareWalletInfo.Model, SelectedHardwareWallet.HardwareWalletInfo.Path, KeyManager.DefaultAccountKeyPath, cts.Token);
+ var path = WalletManager.WalletDirectories.GetWalletFilePaths(WalletName).walletFilePath;
+
+ return KeyManager.CreateNewHardwareWalletWatchOnly(fingerPrint, extPubKey, path);
+ });
+
+ WalletManager.AddWallet(newWallet);
+
+ // Close dialog
+ ClearNavigation();
+ }
+ catch (Exception ex)
+ {
+ // TODO: Notify the user about the error
+ Logger.LogError(ex);
+
+ // Restart detection
+ StartDetection();
+ }
+ finally
+ {
+ IsBusy = false;
+ }
+ }
+
+ private async Task StopDetectionAsync()
+ {
+ if (DetectionTask is { } task && DetectionCts is { } cts)
+ {
+ cts.Cancel();
+ await task;
+ }
+ }
+
+ private void StartDetection()
+ {
+ if (DisposeCts is { } cts)
+ {
+ DetectionCts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token);
+ DetectionTask = HardwareWalletDetectionAsync(DetectionCts);
+ }
+ }
+
+ protected async Task HardwareWalletDetectionAsync(CancellationTokenSource detectionCts)
+ {
+ while (!detectionCts.IsCancellationRequested)
+ {
+ var sw = Stopwatch.StartNew();
+
+ try
+ {
+ using var timeoutCts = new CancellationTokenSource(TimeSpan.FromMinutes(3));
+
+ var detectedHardwareWallets = (await HwiClient.EnumerateAsync(timeoutCts.Token).ConfigureAwait(false)).Select(x => new HardwareWalletViewModel(x)).ToArray();
+ detectionCts.Token.ThrowIfCancellationRequested();
+
+ // The wallets that already exist in the software.
+ var alreadyExistingWalletsToRemove = detectedHardwareWallets.Where(wallet => WalletManager.GetWallets().Any(x => x.KeyManager.MasterFingerprint == wallet.HardwareWalletInfo.Fingerprint));
+ // The wallets that are not detectable since the last enumeration.
+ var disconnectedWalletsToRemove = HardwareWallets.Except(detectedHardwareWallets);
+
+ var toRemove = alreadyExistingWalletsToRemove.Union(disconnectedWalletsToRemove).ToArray();
+ var toAdd = detectedHardwareWallets.Except(toRemove).Except(HardwareWallets);
+
+ await Observable.Start(() =>
+ {
+ // Remove disconnected hardware wallets from the list
+ HardwareWallets.RemoveMany(toRemove);
+ // Add newly detected hardware wallets
+ HardwareWallets.AddRange(toAdd);
+
+ if(SelectedHardwareWallet is null)
+ {
+ SelectedHardwareWallet = HardwareWallets.FirstOrDefault();
+ }
+ }, RxApp.MainThreadScheduler);
+
+ }
+ catch (Exception ex)
+ {
+ if (ex is not OperationCanceledException)
+ {
+ Logger.LogError(ex);
+ }
+ }
+
+ // Too fast enumeration causes the detected hardware wallets to be unable to provide the fingerprint.
+ // Wait at least 5 seconds between two enumerations.
+ sw.Stop();
+ if (sw.Elapsed.Milliseconds < 5000)
+ {
+ await Task.Delay(5000 - sw.Elapsed.Milliseconds).ConfigureAwait(false);
+ }
+ }
+ }
+ }
+}

ADD
WalletWasabi.Fluent/ViewModels/AddWallet/DetectedHardwareWalletViewModel.cs
+using ReactiveUI;
+using System.Reactive.Linq;
+using System.Threading.Tasks;
+using System.Windows.Input;
+using WalletWasabi.Extensions;
+using WalletWasabi.Fluent.ViewModels.Navigation;
+
+namespace WalletWasabi.Fluent.ViewModels.AddWallet
+{
+
+ public class DetectedHardwareWalletViewModel : RoutableViewModel
+ {
+ public DetectedHardwareWalletViewModel(NavigationStateViewModel navigationState, HardwareDetectionState detectionState)
+ : base(navigationState, NavigationTarget.DialogScreen)
+ {
+ var type = WalletType.Hardware;
+
+ switch (detectionState.SelectedDevice!.Model)
+ {
+ case Hwi.Models.HardwareWalletModels.Coldcard:
+ case Hwi.Models.HardwareWalletModels.Coldcard_Simulator:
+ type = WalletType.Coldcard;
+ break;
+
+ case Hwi.Models.HardwareWalletModels.Ledger_Nano_S:
+ type = WalletType.Ledger;
+ break;
+
+ case Hwi.Models.HardwareWalletModels.Trezor_1:
+ case Hwi.Models.HardwareWalletModels.Trezor_1_Simulator:
+ case Hwi.Models.HardwareWalletModels.Trezor_T:
+ case Hwi.Models.HardwareWalletModels.Trezor_T_Simulator:
+ type = WalletType.Trezor;
+ break;
+ }
+
+ Type = type;
+ TypeName = detectionState.SelectedDevice.Model.FriendlyName();
+
+ NextCommand = ReactiveCommand.CreateFromTask(async () =>
+ {
+ IsBusy = true;
+
+ var newWallet = await Task.Run(async () =>
+ {
+ return await detectionState.GenerateWalletAsync();
+ });
+
+ detectionState.WalletManager.AddWallet(newWallet);
+
+ NavigateTo(new AddedWalletPageViewModel(navigationState, detectionState.WalletName, Type), NavigationTarget.DialogScreen);
+
+ IsBusy = false;
+ });
+
+ NoCommand = ReactiveCommand.Create(() =>
+ {
+ NavigateTo(new ConnectHardwareWalletViewModel(navigationState, detectionState.WalletName, detectionState.Network, detectionState.WalletManager, false), NavigationTarget.DialogScreen);
+ });
+ }
+
+ public WalletType Type { get; }
+
+ public string TypeName { get; }
+
+ public ICommand NoCommand { get; }
+ }
+}

ADD
WalletWasabi.Fluent/Views/AddWallet/AddedWalletPageView.axaml
+<UserControl xmlns="https://github.com/avaloniaui"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:vm="using:WalletWasabi.Fluent.ViewModels.AddWallet"
+ xmlns:c="using:WalletWasabi.Fluent.Controls"
+ xmlns:conv="using:WalletWasabi.Fluent.Converters"
+ xmlns:i="using:Avalonia.Xaml.Interactivity"
+ xmlns:behaviors="using:WalletWasabi.Fluent.Behaviors"
+ mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+ x:DataType="vm:AddedWalletPageViewModel"
+ x:CompileBindings="True"
+ x:Class="WalletWasabi.Fluent.Views.AddWallet.AddedWalletPageView">
+ <c:ContentArea Title="Success" NextContent="Done" EnableNext="True" EnableCancel="False" EnableBack="False">
+ <StackPanel VerticalAlignment="Center" Spacing="40">
+ <Image Height="180" Source="{Binding Path='Type' Converter={x:Static conv:WalletTypeImageConverter.Instance}}" />
+ <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
+ <TextBlock Text="Your wallet '" />
+ <TextBlock Text="{Binding WalletName}" HorizontalAlignment="Center" />
+ <TextBlock Text="' was added to Wasabi." />
+ </StackPanel>
+ </StackPanel>
+ </c:ContentArea>
+</UserControl>

ADD
WalletWasabi.Fluent/Views/AddWallet/AddedWalletPageView.axaml.cs
+using Avalonia.Markup.Xaml;
+using Avalonia.ReactiveUI;
+using WalletWasabi.Fluent.ViewModels.AddWallet;
+
+namespace WalletWasabi.Fluent.Views.AddWallet
+{
+ public class AddedWalletPageView : ReactiveUserControl<AddedWalletPageViewModel>
+ {
+ public AddedWalletPageView()
+ {
+ InitializeComponent();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+ }
+}

MODIFY
WalletWasabi.Fluent/Views/AddWallet/ConnectHardwareWalletView.axaml
WalletWasabi.Fluent/Views/AddWallet/ConnectHardwareWalletView.axaml
d:DesignWidth="600" d:DesignHeight="500"
x:Class="WalletWasabi.Fluent.Views.AddWallet.ConnectHardwareWalletView">
- <UserControl.Styles>
- <Style Selector="StackPanel.Delay">
- <Setter Property="Opacity" Value="0"/>
- <Style.Animations>
- <Animation Duration="0:0:30">
- <KeyFrame Cue="0%">
- <Setter Property="IsVisible" Value="False"/>
- </KeyFrame>
- <KeyFrame Cue="100%">
- <Setter Property="IsVisible" Value="True"/>
- </KeyFrame>
- </Animation>
- </Style.Animations>
- <Setter Property="Transitions">
- <Transitions>
- <DoubleTransition Property="Opacity" Duration="0:0:0.5" />
- </Transitions>
- </Setter>
- </Style>
- <Style Selector="StackPanel.Delay[IsVisible=True]">
- <Setter Property="Opacity" Value="1"/>
- </Style>
- </UserControl.Styles>
-
<c:ContentArea Title="Hardware Wallet"
+ Caption="Ensure your device is not connected. Press continue when ready."
IsBusy="{Binding IsBusy}"
EnableCancel="True" CancelContent="Cancel"
EnableBack="True"
- EnableNext="True" NextContent="Connect" >
+ EnableNext="True" NextContent="Continue" >
<DockPanel>
<Button DockPanel.Dock="Bottom" HorizontalAlignment="Center" Classes="h8 plain activeHyperLink" Foreground="{DynamicResource SystemAccentColor}" Content="More information and help with Hardware Wallets" />
- <Panel>
- <ListBox IsVisible="{Binding WalletListVisible}" Background="Transparent" Items="{Binding HardwareWallets}" SelectedItem="{Binding SelectedHardwareWallet}">
- <ListBox.ItemsPanel>
- <ItemsPanelTemplate>
- <WrapPanel Orientation="Horizontal" HorizontalAlignment="Center" />
- </ItemsPanelTemplate>
- </ListBox.ItemsPanel>
- <ItemsControl.ItemTemplate>
- <DataTemplate>
- <Button Classes="plain" Command="{Binding OpenCommand}" Height="200" Width="150" Margin="8">
- <StackPanel>
- <Image Source="{Binding Path='.' Converter={x:Static conv:HardwareWalletIconConverter.Instance}}" />
- <TextBlock Text="{Binding WalletName}" HorizontalAlignment="Center" />
- </StackPanel>
- </Button>
- </DataTemplate>
- </ItemsControl.ItemTemplate>
- </ListBox>
- <DockPanel IsVisible="{Binding !WalletListVisible}">
- <StackPanel VerticalAlignment="Top" Spacing="40" DockPanel.Dock="Top" Margin="0 20 0 0">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="40">
<Image Height="100" Source="/Assets/HardwareIcons/ledger.png" />
<Image Height="100" Source="/Assets/HardwareIcons/coldcard.png" />
<Image Height="100" Source="/Assets/HardwareIcons/trezor.png" />
<Image Height="100" Source="/Assets/HardwareIcons/generic.png" />
</StackPanel>
- <TextBlock Text="Connect your Wallet to the USB Port on your PC and Login / Enter the PIN on the Wallet" HorizontalAlignment="Center" />
- </StackPanel>
- <c:ProgressRing IsIndeterminate="True" VerticalAlignment="Center" Height="100" Width="100" />
- </DockPanel>
- </Panel>
</DockPanel>
</c:ContentArea>
</UserControl>

ADD
WalletWasabi.Fluent/Views/AddWallet/DetectHardwareWalletView.axaml
+<UserControl xmlns="https://github.com/avaloniaui"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:c="clr-namespace:WalletWasabi.Fluent.Controls"
+ xmlns:h="using:WalletWasabi.Fluent.Helpers"
+ xmlns:conv="using:WalletWasabi.Fluent.Converters"
+ xmlns:svg="clr-namespace:Svg.Skia.Avalonia;assembly=Svg.Skia.Avalonia"
+ mc:Ignorable="d"
+ d:DesignWidth="600" d:DesignHeight="500"
+ x:Class="WalletWasabi.Fluent.Views.AddWallet.DetectHardwareWalletView">
+
+ <UserControl.Styles>
+ <Style Selector="StackPanel.Delay">
+ <Setter Property="Opacity" Value="0"/>
+ <Style.Animations>
+ <Animation Duration="0:0:30">
+ <KeyFrame Cue="0%">
+ <Setter Property="IsVisible" Value="False"/>
+ </KeyFrame>
+ <KeyFrame Cue="100%">
+ <Setter Property="IsVisible" Value="True"/>
+ </KeyFrame>
+ </Animation>
+ </Style.Animations>
+ <Setter Property="Transitions">
+ <Transitions>
+ <DoubleTransition Property="Opacity" Duration="0:0:0.5" />
+ </Transitions>
+ </Setter>
+ </Style>
+ <Style Selector="StackPanel.Delay[IsVisible=True]">
+ <Setter Property="Opacity" Value="1"/>
+ </Style>
+ </UserControl.Styles>
+
+ <c:ContentArea Title="Hardware Wallet"
+ IsBusy="{Binding IsBusy}"
+ EnableCancel="True" CancelContent="Cancel"
+ EnableBack="True"
+ EnableNext="True" NextContent="Connect" >
+
+ <DockPanel>
+ <Button DockPanel.Dock="Bottom" HorizontalAlignment="Center" Classes="h8 plain activeHyperLink" Foreground="{DynamicResource SystemAccentColor}" Content="More information and help with Hardware Wallets" />
+ <Panel>
+ <ListBox IsVisible="{Binding WalletListVisible}" Background="Transparent" Items="{Binding HardwareWallets}" SelectedItem="{Binding SelectedHardwareWallet}">
+ <ListBox.ItemsPanel>
+ <ItemsPanelTemplate>
+ <WrapPanel Orientation="Horizontal" HorizontalAlignment="Center" />
+ </ItemsPanelTemplate>
+ </ListBox.ItemsPanel>
+ <ItemsControl.ItemTemplate>
+ <DataTemplate>
+ <Button Classes="plain" Command="{Binding OpenCommand}" Height="200" Width="150" Margin="8">
+ <StackPanel>
+ <Image Source="{Binding Path='.' Converter={x:Static conv:HardwareWalletIconConverter.Instance}}" />
+ <TextBlock Text="{Binding WalletName}" HorizontalAlignment="Center" />
+ </StackPanel>
+ </Button>
+ </DataTemplate>
+ </ItemsControl.ItemTemplate>
+ </ListBox>
+ <DockPanel IsVisible="{Binding !WalletListVisible}">
+ <StackPanel VerticalAlignment="Top" Spacing="40" DockPanel.Dock="Top" Margin="0 20 0 0">
+ <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="40">
+ <Image Height="100" Source="/Assets/HardwareIcons/ledger.png" />
+ <Image Height="100" Source="/Assets/HardwareIcons/coldcard.png" />
+ <Image Height="100" Source="/Assets/HardwareIcons/trezor.png" />
+ <Image Height="100" Source="/Assets/HardwareIcons/generic.png" />
+ </StackPanel>
+ <TextBlock Text="Connect your Wallet to the USB Port on your PC and Login / Enter the PIN on the Wallet" HorizontalAlignment="Center" />
+ </StackPanel>
+ <c:ProgressRing IsIndeterminate="True" VerticalAlignment="Center" Height="100" Width="100" />
+ </DockPanel>
+ </Panel>
+ </DockPanel>
+ </c:ContentArea>
+</UserControl>

ADD
WalletWasabi.Fluent/Views/AddWallet/DetectHardwareWalletView.axaml.cs
+using Avalonia.Markup.Xaml;
+using Avalonia.ReactiveUI;
+using WalletWasabi.Fluent.ViewModels.AddWallet;
+
+namespace WalletWasabi.Fluent.Views.AddWallet
+{
+ public class DetectHardwareWalletView : ReactiveUserControl<DetectHardwareWalletViewModel>
+ {
+ public DetectHardwareWalletView()
+ {
+ InitializeComponent();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+ }
+}
\ No newline at end of file

ADD
WalletWasabi.Fluent/Views/AddWallet/DetectedHardwareWalletView.axaml
+<UserControl xmlns="https://github.com/avaloniaui"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:c="clr-namespace:WalletWasabi.Fluent.Controls"
+ xmlns:h="using:WalletWasabi.Fluent.Helpers"
+ xmlns:conv="using:WalletWasabi.Fluent.Converters"
+ xmlns:svg="clr-namespace:Svg.Skia.Avalonia;assembly=Svg.Skia.Avalonia"
+ mc:Ignorable="d"
+ d:DesignWidth="600" d:DesignHeight="500"
+ x:Class="WalletWasabi.Fluent.Views.AddWallet.DetectedHardwareWalletView">
+
+
+
+ <c:ContentArea Title="Hardware Wallet"
+ IsBusy="{Binding IsBusy}"
+ EnableCancel="False"
+ EnableBack="False"
+ EnableNext="False" >
+ <DockPanel>
+ <Panel DockPanel.Dock="Bottom">
+ <Button Content="No" Classes="plain" HorizontalAlignment="Left" Command="{Binding NoCommand}" />
+ <Button Content="Yes" Classes="action" HorizontalAlignment="Right" IsDefault="True" Command="{Binding NextCommand}" />
+ </Panel>
+ <StackPanel VerticalAlignment="Center" Spacing="40">
+ <Image Height="180" Source="{Binding Path='Type' Converter={x:Static conv:WalletTypeImageConverter.Instance}}" />
+ <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
+ <TextBlock Text="{Binding TypeName}" HorizontalAlignment="Center" />
+ <TextBlock Text=" was detected. Is this correct?" />
+ </StackPanel>
+ </StackPanel>
+ </DockPanel>
+ </c:ContentArea>
+</UserControl>

ADD
WalletWasabi.Fluent/Views/AddWallet/DetectedHardwareWalletView.axaml.cs
+using Avalonia.Markup.Xaml;
+using Avalonia.ReactiveUI;
+using WalletWasabi.Fluent.ViewModels.AddWallet;
+
+namespace WalletWasabi.Fluent.Views.AddWallet
+{
+ public class DetectedHardwareWalletView : ReactiveUserControl<DetectedHardwareWalletViewModel>
+ {
+ public DetectedHardwareWalletView()
+ {
+ InitializeComponent();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+ }
+}
\ No newline at end of file

Begin to optimize adding hardware wallets for the 99% case.