From 279c1c75a1dfe6617798b59cbe3932c803aeb90f Mon Sep 17 00:00:00 2001 From: jp1ac4 <121959000+jp1ac4@users.noreply.github.com> Date: Fri, 8 Dec 2023 18:26:22 +0000 Subject: [PATCH] gui: add option to delete wallet --- gui/src/launcher.rs | 232 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 202 insertions(+), 30 deletions(-) diff --git a/gui/src/launcher.rs b/gui/src/launcher.rs index 14f6624c..f6302e87 100644 --- a/gui/src/launcher.rs +++ b/gui/src/launcher.rs @@ -1,10 +1,14 @@ use std::path::PathBuf; -use iced::{widget::Space, Alignment, Command, Length, Subscription}; +use iced::{ + widget::{tooltip, Space}, + Alignment, Command, Length, Subscription, +}; use liana::{config::ConfigError, miniscript::bitcoin::Network}; use liana_ui::{ - component::{badge, card, text::*}, + color, + component::{badge, button, card, modal::Modal, notification, text::*}, icon, image, theme, util::*, widget::*, @@ -12,10 +16,22 @@ use liana_ui::{ use crate::app; +fn wallet_name(network: &Network) -> String { + match network { + Network::Bitcoin => "Bitcoin Mainnet", + Network::Testnet => "Bitcoin Testnet", + Network::Signet => "Bitcoin Signet", + Network::Regtest => "Bitcoin Regtest", + _ => "Bitcoin unknown", + } + .to_string() +} + pub struct Launcher { choices: Vec, datadir_path: PathBuf, error: Option, + delete_wallet_modal: Option, } impl Launcher { @@ -35,6 +51,7 @@ impl Launcher { datadir_path, choices, error: None, + delete_wallet_modal: None, } } @@ -54,6 +71,35 @@ impl Launcher { check_network_datadir(self.datadir_path.clone(), network), Message::Checked, ), + Message::View(ViewMessage::DeleteWallet(DeleteWalletMessage::ShowModal(network))) => { + let wallet_datadir = self.datadir_path.join(network.to_string()); + let config_path = wallet_datadir.join(app::config::DEFAULT_FILE_NAME); + let internal_bitcoind = if let Ok(cfg) = app::Config::from_file(&config_path) { + Some(cfg.start_internal_bitcoind) + } else { + None + }; + self.delete_wallet_modal = Some(DeleteWalletModal::new( + network, + wallet_datadir, + internal_bitcoind, + )); + Command::none() + } + Message::View(ViewMessage::DeleteWallet(DeleteWalletMessage::Deleted)) => { + if let Some(modal) = &self.delete_wallet_modal { + let choices = self.choices.clone(); + self.choices = choices + .into_iter() + .filter(|c| c != &modal.network) + .collect(); + } + Command::none() + } + Message::View(ViewMessage::DeleteWallet(DeleteWalletMessage::CloseModal)) => { + self.delete_wallet_modal = None; + Command::none() + } Message::Checked(res) => match res { Err(e) => { self.error = Some(e); @@ -70,12 +116,17 @@ impl Launcher { }) } }, - _ => Command::none(), + _ => { + if let Some(modal) = &mut self.delete_wallet_modal { + return modal.update(message); + } + Command::none() + } } } pub fn view(&self) -> Element { - Into::>::into( + let content = Into::>::into( Column::new() .push( Container::new(image::liana_brand_grey().width(Length::Fixed(200.0))) @@ -96,31 +147,43 @@ impl Launcher { .spacing(10), |col, choice| { col.push( - Button::new( - Row::new() - .spacing(20) - .align_items(Alignment::Center) - .push( - badge::Badge::new(icon::bitcoin_icon()) - .style(match choice { - Network::Bitcoin => { - theme::Badge::Bitcoin - } - _ => theme::Badge::Standard, - }), + Row::new() + .spacing(10) + .push( + Button::new( + Row::new() + .spacing(20) + .align_items(Alignment::Center) + .push( + badge::Badge::new( + icon::bitcoin_icon(), + ) + .style(match choice { + Network::Bitcoin => { + theme::Badge::Bitcoin + } + _ => theme::Badge::Standard, + }), + ) + .push(text(wallet_name(choice))), ) - .push(text(match choice { - Network::Bitcoin => "Bitcoin Mainnet", - Network::Testnet => "Bitcoin Testnet", - Network::Signet => "Bitcoin Signet", - Network::Regtest => "Bitcoin Regtest", - _ => "Bitcoin unknown", - })), - ) - .on_press(ViewMessage::Check(*choice)) - .padding(10) - .width(Length::Fill) - .style(theme::Button::Border), + .on_press(ViewMessage::Check(*choice)) + .padding(10) + .width(Length::Fill) + .style(theme::Button::Border), + ) + .push(tooltip::Tooltip::new( + Button::new(icon::trash_icon()) + .on_press(ViewMessage::DeleteWallet( + DeleteWalletMessage::ShowModal( + *choice, + ), + )) + .style(theme::Button::Destructive), + "Delete wallet", + tooltip::Position::Right, + )) + .align_items(Alignment::Center), ) }, ) @@ -148,11 +211,20 @@ impl Launcher { ) .push(Space::with_height(Length::Fixed(100.0))), ) - .map(Message::View) + .map(Message::View); + if let Some(modal) = &self.delete_wallet_modal { + Modal::new(content, modal.view()) + .on_blur(Some(Message::View(ViewMessage::DeleteWallet( + DeleteWalletMessage::CloseModal, + )))) + .into() + } else { + content + } } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum Message { View(ViewMessage), Install(PathBuf), @@ -164,6 +236,106 @@ pub enum Message { pub enum ViewMessage { StartInstall, Check(Network), + DeleteWallet(DeleteWalletMessage), +} + +#[derive(Debug, Clone)] +pub enum DeleteWalletMessage { + ShowModal(Network), + CloseModal, + Confirm, + Deleted, +} + +struct DeleteWalletModal { + network: Network, + wallet_datadir: PathBuf, + warning: Option, + deleted: bool, + // `None` means we were not able to determine whether wallet uses internal bitcoind. + internal_bitcoind: Option, +} + +impl DeleteWalletModal { + fn new(network: Network, wallet_datadir: PathBuf, internal_bitcoind: Option) -> Self { + Self { + network, + wallet_datadir, + warning: None, + deleted: false, + internal_bitcoind, + } + } + + fn update(&mut self, message: Message) -> Command { + if let Message::View(ViewMessage::DeleteWallet(DeleteWalletMessage::Confirm)) = message { + self.warning = None; + if let Err(e) = std::fs::remove_dir_all(&self.wallet_datadir) { + self.warning = Some(e); + } else { + self.deleted = true; + return Command::perform(async {}, |_| { + Message::View(ViewMessage::DeleteWallet(DeleteWalletMessage::Deleted)) + }); + }; + } + Command::none() + } + fn view(&self) -> Element { + let mut confirm_button = button::primary(None, "Delete wallet") + .width(Length::Fixed(200.0)) + .style(theme::Button::Destructive); + if self.warning.is_none() { + confirm_button = + confirm_button.on_press(ViewMessage::DeleteWallet(DeleteWalletMessage::Confirm)); + } + // Use separate `Row`s for help text in order to have better spacing. + let help_text_1 = format!( + "Are you sure you want to delete the wallet and all associated data for {}?", + wallet_name(&self.network) + ); + let help_text_2 = match self.internal_bitcoind { + Some(true) => Some("(The Liana-managed Bitcoin node for this network will not be affected by this action.)"), + Some(false) => None, + None => Some("(If you are using a Liana-managed Bitcoin node, it will not be affected by this action.)"), + }; + let help_text_3 = "WARNING: This cannot be undone."; + + Into::>::into( + card::simple( + Column::new() + .spacing(10) + .push(Container::new( + h4_bold(format!("Delete wallet for {}", wallet_name(&self.network))) + .style(color::RED) + .width(Length::Fill), + )) + .push(Row::new().push(text(help_text_1))) + .push_maybe( + help_text_2.map(|t| Row::new().push(p1_regular(t).style(color::GREY_3))), + ) + .push(Row::new()) + .push(Row::new().push(text(help_text_3))) + .push_maybe(self.warning.as_ref().map(|w| { + notification::warning(w.to_string(), w.to_string()).width(Length::Fill) + })) + .push( + Container::new(if !self.deleted { + Row::new().push(confirm_button) + } else { + Row::new() + .spacing(10) + .push(icon::circle_check_icon().style(color::GREEN)) + .push(text("Wallet successfully deleted").style(color::GREEN)) + }) + .align_x(iced_native::alignment::Horizontal::Center) + .width(Length::Fill), + ), + ) + .width(Length::Fixed(700.0)), + ) + .map(Message::View) + } } async fn check_network_datadir(mut path: PathBuf, network: Network) -> Result {