diff --git a/src/Angor/Avalonia/AngorApp/UI/Sections/Settings/ISettingsSectionViewModel.cs b/src/Angor/Avalonia/AngorApp/UI/Sections/Settings/ISettingsSectionViewModel.cs index 57e17a5ef..4f95412a8 100644 --- a/src/Angor/Avalonia/AngorApp/UI/Sections/Settings/ISettingsSectionViewModel.cs +++ b/src/Angor/Avalonia/AngorApp/UI/Sections/Settings/ISettingsSectionViewModel.cs @@ -19,11 +19,15 @@ internal interface ISettingsSectionViewModel : IDisposable ReactiveCommand AddIndexer { get; } ReactiveCommand AddRelay { get; } ReactiveCommand RefreshIndexers { get; } - ReactiveCommand DeleteWallet { get; } + ReactiveCommand RefreshRelays { get; } + ReactiveCommand ChangeNetwork { get; } + ReactiveCommand WipeData { get; } + ReactiveCommand BackupWallet { get; } IEnhancedCommand ImportWallet { get; } bool IsBitcoinPreferred { get; set; } bool IsDebugMode { get; set; } bool IsTestnet { get; } + bool HasWallet { get; } } internal class SettingsSectionViewModelSample : ISettingsSectionViewModel @@ -32,11 +36,14 @@ public SettingsSectionViewModelSample() { Indexers = new ObservableCollection { - new("https://indexer.angor.io", true, UrlStatus.Online, DateTime.UtcNow, _ => { }, _ => { }) + new("https://test.indexer.angor.io", false, UrlStatus.Offline, DateTime.UtcNow, _ => { }, _ => { }), + new("https://signet.angor.online", true, UrlStatus.Online, DateTime.UtcNow, _ => { }, _ => { }), + new("https://signet2.angor.online", false, UrlStatus.Offline, DateTime.UtcNow, _ => { }, _ => { }) }; Relays = new ObservableCollection { - new("wss://relay.angor.io", false, UrlStatus.Offline, DateTime.UtcNow, _ => { }) + new("wss://relay.angor.io", false, UrlStatus.Online, DateTime.UtcNow, _ => { }, name: "strfry default"), + new("wss://relay2.angor.io", false, UrlStatus.Online, DateTime.UtcNow, _ => { }, name: "strfry2 default") }; } @@ -49,10 +56,14 @@ public SettingsSectionViewModelSample() public ReactiveCommand AddIndexer { get; } = ReactiveCommand.Create(() => { }); public ReactiveCommand AddRelay { get; } = ReactiveCommand.Create(() => { }); public ReactiveCommand RefreshIndexers { get; } = ReactiveCommand.Create(() => { }); - public ReactiveCommand DeleteWallet { get; } = ReactiveCommand.Create(() => { }); + public ReactiveCommand RefreshRelays { get; } = ReactiveCommand.Create(() => { }); + public ReactiveCommand ChangeNetwork { get; } = ReactiveCommand.Create(() => { }); + public ReactiveCommand WipeData { get; } = ReactiveCommand.Create(() => { }); + public ReactiveCommand BackupWallet { get; } = ReactiveCommand.Create(() => { }); public IEnhancedCommand ImportWallet { get; } = ReactiveCommand.Create(() => { }).Enhance(); public bool IsBitcoinPreferred { get; set; } = true; public bool IsDebugMode { get; set; } = false; public bool IsTestnet { get; } = true; + public bool HasWallet { get; } = true; public void Dispose() { } } diff --git a/src/Angor/Avalonia/AngorApp/UI/Sections/Settings/SettingsSectionView.axaml b/src/Angor/Avalonia/AngorApp/UI/Sections/Settings/SettingsSectionView.axaml index fc9ab6e78..6a7150632 100644 --- a/src/Angor/Avalonia/AngorApp/UI/Sections/Settings/SettingsSectionView.axaml +++ b/src/Angor/Avalonia/AngorApp/UI/Sections/Settings/SettingsSectionView.axaml @@ -4,6 +4,8 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:controls="clr-namespace:AngorApp.UI.Shared.Controls" xmlns:settings="clr-namespace:AngorApp.UI.Sections.Settings" + xmlns:fa="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" + xmlns:i="https://github.com/projektanker/icons.avalonia" mc:Ignorable="d" d:DesignWidth="1200" x:Class="AngorApp.UI.Sections.Settings.SettingsSectionView" x:DataType="settings:ISettingsSectionViewModel"> @@ -32,72 +34,91 @@ + + + + + + + + + + + - - - WARNING NOTICE: Changing networks will wipe your wallet data. - - - - - - - - - Debug mode is only available on testnet networks. When enabled, additional debugging features will be available. - + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + - + - + - - - - - - - - - + + + + + + + + - + @@ -105,19 +126,45 @@ + + + + + + + + + + + + + + + - - - - + - + + + + + + + + + + + + + + + - + @@ -125,15 +172,50 @@ - + + + - Import an existing wallet using your recovery words. + + + + + + + + + + + + Debug mode is only available on testnet networks. When enabled, additional debugging features will be available. + + + + + + + + Download a backup of your account (seed) so you never lose access. + + + + + + + + Import an existing wallet using your recovery words. + + - + + + + + diff --git a/src/Angor/Avalonia/AngorApp/UI/Sections/Settings/SettingsSectionViewModel.cs b/src/Angor/Avalonia/AngorApp/UI/Sections/Settings/SettingsSectionViewModel.cs index 4c6a5d1ec..e3734c8ef 100644 --- a/src/Angor/Avalonia/AngorApp/UI/Sections/Settings/SettingsSectionViewModel.cs +++ b/src/Angor/Avalonia/AngorApp/UI/Sections/Settings/SettingsSectionViewModel.cs @@ -8,6 +8,7 @@ using Angor.Shared.Services; using System.Linq; using System.Reactive.Disposables; +using Angor.Sdk.Common; using AngorApp.UI.Shared.Controls; using AngorApp.UI.Shared.Services; using AngorApp.UI.Sections.Wallet.CreateAndImport; @@ -36,6 +37,8 @@ public partial class SettingsSectionViewModel : ReactiveObject, ISettingsSection private readonly INetworkService networkService; + private readonly ISensitiveWalletDataProvider sensitiveWalletDataProvider; + private string network; private string newIndexer; @@ -50,9 +53,11 @@ public partial class SettingsSectionViewModel : ReactiveObject, ISettingsSection private bool isTestnet; + private bool hasWallet; + private readonly CompositeDisposable disposable = new(); - public SettingsSectionViewModel(INetworkStorage networkStorage, IWalletStore walletStore, UIServices uiServices, INetworkService networkService, INetworkConfiguration networkConfiguration, IWalletContext walletContext, WalletImportWizard walletImportWizard) + public SettingsSectionViewModel(INetworkStorage networkStorage, IWalletStore walletStore, UIServices uiServices, INetworkService networkService, INetworkConfiguration networkConfiguration, IWalletContext walletContext, WalletImportWizard walletImportWizard, ISensitiveWalletDataProvider sensitiveWalletDataProvider) { this.networkStorage = networkStorage; this.walletStore = walletStore; @@ -60,6 +65,7 @@ public SettingsSectionViewModel(INetworkStorage networkStorage, IWalletStore wal this.walletContext = walletContext; this.networkConfiguration = networkConfiguration; this.networkService = networkService; + this.sensitiveWalletDataProvider = sensitiveWalletDataProvider; this.networkService.AddSettingsIfNotExist(); @@ -79,50 +85,23 @@ public SettingsSectionViewModel(INetworkStorage networkStorage, IWalletStore wal AddIndexer = ReactiveCommand.Create(DoAddIndexer, this.WhenAnyValue(x => x.NewIndexer, url => !string.IsNullOrWhiteSpace(url))).DisposeWith(disposable); AddRelay = ReactiveCommand.Create(DoAddRelay, this.WhenAnyValue(x => x.NewRelay, url => !string.IsNullOrWhiteSpace(url))).DisposeWith(disposable); RefreshIndexers = ReactiveCommand.CreateFromTask(RefreshIndexersAsync).DisposeWith(disposable); + RefreshRelays = ReactiveCommand.CreateFromTask(RefreshRelaysAsync).DisposeWith(disposable); + ChangeNetwork = ReactiveCommand.CreateFromTask(ChangeNetworkAsync).DisposeWith(disposable); ImportWallet = ReactiveCommand.CreateFromTask(walletImportWizard.Start).Enhance().DisposeWith(disposable); - var canDeleteWallet = walletContext.CurrentWalletChanges + var canBackupWallet = walletContext.CurrentWalletChanges .Select(maybe => maybe.HasValue) .StartWith(walletContext.CurrentWallet.HasValue) .ObserveOn(RxApp.MainThreadScheduler); - DeleteWallet = ReactiveCommand.CreateFromTask(DeleteWalletAsync, canDeleteWallet).DisposeWith(disposable); + WipeData = ReactiveCommand.CreateFromTask(WipeDataAsync).DisposeWith(disposable); + BackupWallet = ReactiveCommand.CreateFromTask(BackupWalletAsync, canBackupWallet).DisposeWith(disposable); - this.WhenAnyValue(x => x.Network) - .Skip(1) - .Where(_ => !restoringNetwork) - .SelectMany(async n => (n, await this.uiServices.Dialog.ShowConfirmation("Change network?", "Changing network will delete the current wallet"))) + // Track wallet state + walletContext.CurrentWalletChanges + .Select(maybe => maybe.HasValue) + .StartWith(walletContext.CurrentWallet.HasValue) .ObserveOn(RxApp.MainThreadScheduler) - .Subscribe(t => t.Item2.Match( - confirmed => - { - if (confirmed) - { - networkStorage.SetNetwork(t.n); - networkStorage.SetSettings(new SettingsInfo()); - networkConfiguration.SetNetwork(t.n switch - { - "Mainnet" => new BitcoinMain(), - "Liquid" => new LiquidMain(), - _ => new Angornet() - }); - networkService.AddSettingsIfNotExist(); - var s = networkStorage.GetSettings(); - Reset(Indexers, s.Indexers.Select(CreateIndexer)); - Reset(Relays, s.Relays.Select(CreateRelay)); - this.walletStore.SaveAll([]); - currentNetwork = t.n; - } - else - { - RestoreNetwork(); - } - return Unit.Default; - }, - () => - { - RestoreNetwork(); - return Unit.Default; - })) + .Subscribe(hasValue => HasWallet = hasValue) .DisposeWith(disposable); IsBitcoinPreferred = uiServices.IsBitcoinPreferred; @@ -145,7 +124,10 @@ public SettingsSectionViewModel(INetworkStorage networkStorage, IWalletStore wal public ReactiveCommand AddIndexer { get; } public ReactiveCommand AddRelay { get; } public ReactiveCommand RefreshIndexers { get; } - public ReactiveCommand DeleteWallet { get; } + public ReactiveCommand RefreshRelays { get; } + public ReactiveCommand ChangeNetwork { get; } + public ReactiveCommand WipeData { get; } + public ReactiveCommand BackupWallet { get; } public IEnhancedCommand ImportWallet { get; } public string Network @@ -182,6 +164,12 @@ public bool IsTestnet private set => this.RaiseAndSetIfChanged(ref isTestnet, value); } + public bool HasWallet + { + get => hasWallet; + private set => this.RaiseAndSetIfChanged(ref hasWallet, value); + } + private void DoAddIndexer() { Indexers.Add(CreateIndexer(new SettingsUrl @@ -242,8 +230,52 @@ private void DoRemoveRelay(SettingsUrlViewModel url) SaveSettings(); } + private async Task ChangeNetworkAsync() + { + var confirmation = await uiServices.Dialog.ShowConfirmation("Change network?", "Changing network will delete the current wallet and all local data. This action cannot be undone."); + var shouldChange = confirmation.GetValueOrDefault(() => false); + + if (!shouldChange) + { + return; + } + + // Cycle through networks + var currentIndex = Array.IndexOf(Networks.ToArray(), Network); + var nextIndex = (currentIndex + 1) % Networks.Count; + var newNetwork = Networks[nextIndex]; + + networkStorage.SetNetwork(newNetwork); + networkStorage.SetSettings(new SettingsInfo()); + networkConfiguration.SetNetwork(newNetwork switch + { + "Mainnet" => new BitcoinMain(), + "Liquid" => new LiquidMain(), + _ => new Angornet() + }); + networkService.AddSettingsIfNotExist(); + var s = networkStorage.GetSettings(); + Reset(Indexers, s.Indexers.Select(CreateIndexer)); + Reset(Relays, s.Relays.Select(CreateRelay)); + this.walletStore.SaveAll([]); + currentNetwork = newNetwork; + Network = newNetwork; + } + private async Task RefreshIndexersAsync() { + // If no indexers exist, add the default indexers back + if (Indexers.Count == 0) + { + var defaultIndexers = networkConfiguration.GetDefaultIndexerUrls(); + foreach (var indexer in defaultIndexers) + { + Indexers.Add(CreateIndexer(indexer)); + } + SaveSettings(); + } + + // Refresh indexer status try { await networkService.CheckServices(true); @@ -257,30 +289,86 @@ private async Task RefreshIndexersAsync() Reset(Indexers, settings.Indexers.Select(CreateIndexer)); } - private async Task DeleteWalletAsync() + private async Task RefreshRelaysAsync() { - var confirmation = await uiServices.Dialog.ShowConfirmation("Delete wallet?", "Deleting the current wallet will remove all local wallet data. This action cannot be undone."); - var shouldDelete = confirmation.GetValueOrDefault(() => false); + // If no relays exist, add the default relays back + if (Relays.Count == 0) + { + var defaultRelays = networkConfiguration.GetDefaultRelayUrls(); + foreach (var relay in defaultRelays) + { + Relays.Add(CreateRelay(relay)); + } + SaveSettings(); + } - if (!shouldDelete) + // Refresh relay status + try + { + await networkService.CheckServices(true); + } + catch (Exception ex) + { + await uiServices.Dialog.ShowMessage("Relay refresh failed", ex.Message); + } + + var settings = networkStorage.GetSettings(); + Reset(Relays, settings.Relays.Select(CreateRelay)); + } + + private async Task WipeDataAsync() + { + var confirmation = await uiServices.Dialog.ShowConfirmation("Wipe all data?", "This will delete your wallet and all local settings. This action cannot be undone. Make sure you have backed up your seed words."); + var shouldWipe = confirmation.GetValueOrDefault(() => false); + + if (!shouldWipe) { return; } + // Delete wallet if exists + var wallet = walletContext.CurrentWallet.GetValueOrDefault(); + if (wallet is not null) + { + await walletContext.DeleteWallet(wallet.Id); + } + + // Reset settings + networkStorage.SetSettings(new SettingsInfo()); + networkService.AddSettingsIfNotExist(); + var s = networkStorage.GetSettings(); + Reset(Indexers, s.Indexers.Select(CreateIndexer)); + Reset(Relays, s.Relays.Select(CreateRelay)); + walletStore.SaveAll([]); + + await uiServices.Dialog.ShowMessage("Data wiped", "All local data has been removed."); + } + + private async Task BackupWalletAsync() + { var wallet = walletContext.CurrentWallet.GetValueOrDefault(); if (wallet is null) { + await uiServices.Dialog.ShowMessage("No wallet", "No wallet found to backup."); return; } - var deleteResult = await walletContext.DeleteWallet(wallet.Id); - if (deleteResult.IsFailure) + var sensitiveDataResult = await sensitiveWalletDataProvider.RequestSensitiveData(wallet.Id); + if (sensitiveDataResult.IsFailure) { - await uiServices.Dialog.ShowMessage("Delete wallet failed", deleteResult.Error); + await uiServices.Dialog.ShowMessage("Backup failed", sensitiveDataResult.Error); return; } - await uiServices.Dialog.ShowMessage("Wallet deleted", "The current wallet has been removed."); + var (seedWords, passphrase) = sensitiveDataResult.Value; + var message = $"Your seed words:\n\n{seedWords}"; + if (passphrase.HasValue && !string.IsNullOrEmpty(passphrase.Value)) + { + message += $"\n\nPassphrase: {passphrase.Value}"; + } + message += "\n\nPlease write these down and store them securely. Never share them with anyone."; + + await uiServices.Dialog.ShowMessage("Backup - Seed Words", message); } private static void Refresh(ObservableCollection collection) diff --git a/src/Angor/Avalonia/AngorApp/UI/Themes/V2/Resources/Colors.axaml b/src/Angor/Avalonia/AngorApp/UI/Themes/V2/Resources/Colors.axaml index 34bde4769..ba66e4e22 100644 --- a/src/Angor/Avalonia/AngorApp/UI/Themes/V2/Resources/Colors.axaml +++ b/src/Angor/Avalonia/AngorApp/UI/Themes/V2/Resources/Colors.axaml @@ -217,7 +217,7 @@ - + 1