From 1746314e7d8ef4e06cc05951dc620d88d280d26e Mon Sep 17 00:00:00 2001 From: edouardparis Date: Tue, 29 Apr 2025 15:14:53 +0200 Subject: [PATCH 1/2] Add liana-connect cache file --- Cargo.lock | 16 ++ liana-gui/Cargo.toml | 1 + liana-gui/src/app/settings.rs | 17 +- liana-gui/src/installer/mod.rs | 45 ++++- .../services/connect/client/backend/mod.rs | 53 ++---- .../src/services/connect/client/cache.rs | 159 ++++++++++++++++++ liana-gui/src/services/connect/client/mod.rs | 1 + liana-gui/src/services/connect/login.rs | 124 +++++--------- 8 files changed, 290 insertions(+), 126 deletions(-) create mode 100644 liana-gui/src/services/connect/client/cache.rs diff --git a/Cargo.lock b/Cargo.lock index 3d7adba1..bc90d070 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -254,6 +254,21 @@ dependencies = [ "slab", ] +[[package]] +name = "async-fd-lock" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7569377d7062165f6f7834d9cb3051974a2d141433cc201c2f94c149e993cccf" +dependencies = [ + "async-trait", + "cfg-if", + "pin-project", + "rustix", + "thiserror 1.0.69", + "tokio", + "windows-sys 0.52.0", +] + [[package]] name = "async-fs" version = "2.1.2" @@ -2993,6 +3008,7 @@ dependencies = [ name = "liana-gui" version = "10.0.0" dependencies = [ + "async-fd-lock", "async-hwi", "async-trait", "backtrace", diff --git a/liana-gui/Cargo.toml b/liana-gui/Cargo.toml index 2a049d20..bf96d225 100644 --- a/liana-gui/Cargo.toml +++ b/liana-gui/Cargo.toml @@ -29,6 +29,7 @@ iced_runtime = "0.13.1" email_address = "0.2.7" tokio = {version = "1.21.0", features = ["signal"]} +async-fd-lock = "0.2.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/liana-gui/src/app/settings.rs b/liana-gui/src/app/settings.rs index 8bd0c82a..fc741685 100644 --- a/liana-gui/src/app/settings.rs +++ b/liana-gui/src/app/settings.rs @@ -66,7 +66,21 @@ impl Settings { pub struct AuthConfig { pub email: String, pub wallet_id: String, - pub refresh_token: String, + // legacy field, refresh_token is now stored in the connect cache file + // Keep it in case, user want to open the wallet with a previous Liana-GUI version. + // Field cannot be ignored as the settings file is override during settings update. + // TODO: remove later after multiple versions. + pub refresh_token: Option, +} + +impl AuthConfig { + pub fn new(email: String, wallet_id: String) -> Self { + Self { + email, + wallet_id, + refresh_token: None, + } + } } #[derive(Debug, Clone, Deserialize, Serialize)] @@ -241,7 +255,6 @@ pub enum SettingsError { WritingFile(String), Unexpected(String), } - impl std::fmt::Display for SettingsError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { diff --git a/liana-gui/src/installer/mod.rs b/liana-gui/src/installer/mod.rs index d1502105..31239d13 100644 --- a/liana-gui/src/installer/mod.rs +++ b/liana-gui/src/installer/mod.rs @@ -13,6 +13,7 @@ use liana_ui::{ widget::{Column, Element}, }; use lianad::config::Config; +use std::ops::Deref; use tracing::{error, info, warn}; use std::io::Write; @@ -37,6 +38,7 @@ use crate::{ api::payload::{Provider, ProviderKey}, BackendClient, BackendWalletClient, }, + cache::update_connect_cache, }, }, signer::Signer, @@ -550,6 +552,22 @@ pub async fn create_remote_wallet( info!("Settings file created"); + let backend = remote_backend.inner_client(); + if let Err(e) = update_connect_cache( + &network_datadir, + backend.auth.read().await.deref(), + backend.auth_client(), + false, + ) + .await + { + // this error is not critical, the liana-connect backend stored the wallet + // and user can reauthenticate. + tracing::error!("Failed to update Liana-Connect cache: {}", e); + } else { + info!("Liana-Connect cache updated"); + }; + Ok(gui_config_path) } @@ -599,6 +617,22 @@ pub async fn import_remote_wallet( info!("Gui configuration file created"); + let backend = backend.inner_client(); + if let Err(e) = update_connect_cache( + &network_datadir, + backend.auth.read().await.deref(), + backend.auth_client(), + false, + ) + .await + { + // this error is not critical, the liana-connect backend stored the wallet + // and user can reauthenticate. + tracing::error!("Failed to update Liana-Connect cache: {}", e); + } else { + info!("Liana-Connect cache updated"); + }; + Ok(gui_config_path) } @@ -631,19 +665,16 @@ pub async fn extract_remote_gui_settings(ctx: &Context, backend: &BackendWalletC .expect("LianaDescriptor.to_string() always include the checksum") .to_string(); - let auth = backend.inner_client().auth.read().await; - Settings { wallets: vec![WalletSetting { name: wallet_name(descriptor), descriptor_checksum, keys: Vec::new(), hardware_wallets: Vec::new(), - remote_backend_auth: Some(AuthConfig { - email: backend.user_email().to_string(), - wallet_id: backend.wallet_id(), - refresh_token: auth.refresh_token.clone(), - }), + remote_backend_auth: Some(AuthConfig::new( + backend.user_email().to_string(), + backend.wallet_id(), + )), }], } } diff --git a/liana-gui/src/services/connect/client/backend/mod.rs b/liana-gui/src/services/connect/client/backend/mod.rs index f81daf3c..ba58f713 100644 --- a/liana-gui/src/services/connect/client/backend/mod.rs +++ b/liana-gui/src/services/connect/client/backend/mod.rs @@ -20,7 +20,6 @@ use reqwest::{Error, IntoUrl, Method, RequestBuilder, Response}; use tokio::sync::RwLock; use crate::{ - app::settings::{AuthConfig, Settings}, daemon::{model::*, Daemon, DaemonBackend, DaemonError}, dir::LianaDirectory, hw::HardwareWalletConfig, @@ -28,7 +27,10 @@ use crate::{ use self::api::{UTXOKind, DEFAULT_OUTPOINTS_LIMIT}; -use super::auth::{self, AccessTokenResponse, AuthError}; +use super::{ + auth::{self, AccessTokenResponse, AuthError}, + cache::update_connect_cache, +}; impl From for DaemonError { fn from(value: Error) -> Self { @@ -101,6 +103,10 @@ impl BackendClient { }) } + pub fn auth_client(&self) -> &auth::AuthClient { + &self.auth_client + } + pub fn user_email(&self) -> &str { &self.auth_client.email } @@ -532,45 +538,20 @@ impl Daemon for BackendWalletClient { return Ok(()); } Ok(mut old) => { - let new = self - .inner - .auth_client - .refresh_token(&auth.refresh_token) - .await?; - let network_dir = datadir.network_directory(network); - let mut settings = Settings::from_file(&network_dir).map_err(|e| { - DaemonError::Unexpected(format!( - "Cannot access to settings.json file: {}", - e - )) - })?; - if let Some(wallet_settings) = settings.wallets.iter_mut().find(|w| { - if let Some(auth) = &w.remote_backend_auth { - auth.wallet_id == self.wallet_uuid - } else { - false - } - }) { - wallet_settings.remote_backend_auth = Some(AuthConfig { - email: self.inner.auth_client.email.clone(), - wallet_id: self.wallet_id(), - refresh_token: new.refresh_token.clone(), - }); - } else { - tracing::info!("Wallet id was not found in the settings"); - } - - settings.to_file(&network_dir).map_err(|e| { - DaemonError::Unexpected(format!( - "Cannot access to settings.json file: {}", - e - )) + let new = update_connect_cache( + &network_dir, + &old, + &self.inner.auth_client, + true, // refresh the token + ) + .await + .map_err(|e| { + DaemonError::Unexpected(format!("Cannot update Liana-connect cache: {}", e)) })?; *old = new; - tracing::info!("Liana backend access was refreshed"); } } } diff --git a/liana-gui/src/services/connect/client/cache.rs b/liana-gui/src/services/connect/client/cache.rs new file mode 100644 index 00000000..1a5885a7 --- /dev/null +++ b/liana-gui/src/services/connect/client/cache.rs @@ -0,0 +1,159 @@ +use crate::dir::NetworkDirectory; +use async_fd_lock::LockWrite; +use serde::{Deserialize, Serialize}; +use std::io::SeekFrom; +use tokio::fs::OpenOptions; +use tokio::io::AsyncSeekExt; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; + +use super::auth::{AccessTokenResponse, AuthClient, AuthError}; + +pub const CONNECT_CACHE_FILENAME: &str = "connect.json"; + +#[derive(Debug, Default, Clone, Deserialize, Serialize)] +pub struct ConnectCache { + pub accounts: Vec, +} + +impl ConnectCache { + fn upsert_credential(&mut self, email: &str, tokens: AccessTokenResponse) { + if let Some(c) = self.accounts.iter_mut().find(|c| c.email == email) { + c.tokens = tokens; + } else { + self.accounts.push(Account { + email: email.to_string(), + tokens, + }) + } + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Account { + pub email: String, + pub tokens: AccessTokenResponse, +} + +impl Account { + pub fn from_cache( + network_dir: &NetworkDirectory, + email: &str, + ) -> Result, ConnectCacheError> { + let mut path = network_dir.path().to_path_buf(); + path.push(CONNECT_CACHE_FILENAME); + + std::fs::read(path) + .map_err(|e| match e.kind() { + std::io::ErrorKind::NotFound => ConnectCacheError::NotFound, + _ => ConnectCacheError::ReadingFile(format!("Reading settings file: {}", e)), + }) + .and_then(|file_content| { + serde_json::from_slice::(&file_content).map_err(|e| { + ConnectCacheError::ReadingFile(format!("Parsing settings file: {}", e)) + }) + }) + .map(|cache| cache.accounts.into_iter().find(|c| c.email == email)) + } +} + +pub async fn update_connect_cache( + network_dir: &NetworkDirectory, + current_tokens: &AccessTokenResponse, + client: &AuthClient, + refresh: bool, +) -> Result { + let email = &client.email; + let mut path = network_dir.path().to_path_buf(); + path.push(CONNECT_CACHE_FILENAME); + + let file_exists = tokio::fs::try_exists(&path).await.unwrap_or(false); + + let mut file = OpenOptions::new() + .read(true) + .write(true) + .create(true) + .truncate(false) + .open(&path) + .await + .map_err(|e| ConnectCacheError::ReadingFile(format!("Opening file: {}", e)))? + .lock_write() + .await + .map_err(|e| ConnectCacheError::ReadingFile(format!("Locking file: {:?}", e)))?; + + let mut cache = if file_exists { + let mut file_content = Vec::new(); + file.read_to_end(&mut file_content) + .await + .map_err(|e| ConnectCacheError::ReadingFile(format!("Reading file content: {}", e)))?; + + match serde_json::from_slice::(&file_content) { + Ok(cache) => cache, + Err(e) => { + tracing::warn!("Something wrong with Liana-Connect cache file: {:?}", e); + tracing::warn!("Liana-Connect cache file is reset"); + ConnectCache::default() + } + } + } else { + ConnectCache::default() + }; + + if let Some(c) = cache.accounts.iter().find(|cred| cred.email == *email) { + // An other process updated the tokens + if current_tokens.expires_at < c.tokens.expires_at { + tracing::debug!("Liana-Connect authentication tokens are up to date, nothing to do"); + return Ok(c.tokens.clone()); + } + } + + let tokens = if refresh { + client + .refresh_token(¤t_tokens.refresh_token) + .await + .map_err(ConnectCacheError::Updating)? + } else { + current_tokens.clone() + }; + + cache.upsert_credential(email, tokens.clone()); + + let content = serde_json::to_vec_pretty(&cache).map_err(|e| { + ConnectCacheError::WritingFile(format!("Failed to serialize settings: {}", e)) + })?; + + file.seek(SeekFrom::Start(0)).await.map_err(|e| { + ConnectCacheError::WritingFile(format!("Failed to seek to start of file: {}", e)) + })?; + + file.write_all(&content).await.map_err(|e| { + tracing::warn!("failed to write to file: {:?}", e); + ConnectCacheError::WritingFile(e.to_string()) + })?; + + file.inner_mut() + .set_len(content.len() as u64) + .await + .map_err(|e| ConnectCacheError::WritingFile(format!("Failed to truncate file: {}", e)))?; + + Ok(tokens) +} + +#[derive(Debug, Clone)] +pub enum ConnectCacheError { + NotFound, + ReadingFile(String), + WritingFile(String), + Unexpected(String), + Updating(AuthError), +} +impl std::fmt::Display for ConnectCacheError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::NotFound => write!(f, "ConnectCache file not found"), + Self::ReadingFile(e) => write!(f, "Error while reading file: {}", e), + Self::WritingFile(e) => write!(f, "Error while writing file: {}", e), + Self::Unexpected(e) => write!(f, "Unexpected error: {}", e), + Self::Updating(e) => write!(f, "Error while updating cache file: {}", e), + } + } +} diff --git a/liana-gui/src/services/connect/client/mod.rs b/liana-gui/src/services/connect/client/mod.rs index 1eb4add2..eda54292 100644 --- a/liana-gui/src/services/connect/client/mod.rs +++ b/liana-gui/src/services/connect/client/mod.rs @@ -1,5 +1,6 @@ pub mod auth; pub mod backend; +pub mod cache; use liana::miniscript::bitcoin; diff --git a/liana-gui/src/services/connect/login.rs b/liana-gui/src/services/connect/login.rs index e1dce45b..2c872637 100644 --- a/liana-gui/src/services/connect/login.rs +++ b/liana-gui/src/services/connect/login.rs @@ -13,7 +13,7 @@ use lianad::commands::ListCoinsResult; use crate::{ app::{ cache::coins_to_cache, - settings::{AuthConfig, Settings, SettingsError, WalletSetting}, + settings::{Settings, SettingsError}, }, daemon::DaemonError, dir::LianaDirectory, @@ -22,15 +22,18 @@ use crate::{ use super::client::{ auth::{AuthClient, AuthError}, backend::{api, BackendClient, BackendWalletClient}, + cache::{self, update_connect_cache, ConnectCacheError}, }; #[derive(Debug, Clone)] pub enum Error { Auth(AuthError), + CredentialsMissing, // DaemonError does not implement Clone. // TODO: maybe Arc is overkill Backend(Arc), Settings(SettingsError), + Cache(cache::ConnectCacheError), Unexpected(String), } @@ -38,8 +41,10 @@ impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { Self::Auth(e) => write!(f, "Authentication error: {}", e), + Self::CredentialsMissing => write!(f, "credentials missing"), Self::Backend(e) => write!(f, "Remote backend error: {}", e), Self::Settings(e) => write!(f, "Settings file error: {}", e), + Self::Cache(e) => write!(f, "Connect cache file error: {}", e), Self::Unexpected(e) => write!(f, "Unexpected error: {}", e), } } @@ -63,6 +68,12 @@ impl From for Error { } } +impl From for Error { + fn from(value: ConnectCacheError) -> Self { + Self::Cache(value) + } +} + #[derive(Debug, Clone)] pub enum Message { View(ViewMessage), @@ -156,13 +167,13 @@ impl LianaLiteLogin { }, Task::none(), ), - Ok(auth_config) => ( + Ok(setting) => ( Self { network, - datadir, + datadir: datadir.clone(), step: ConnectionStep::CheckingAuthFile, connection_error: None, - wallet_id: auth_config.wallet_id.clone(), + wallet_id: setting.wallet_id.clone(), auth_error: None, processing: true, }, @@ -174,14 +185,14 @@ impl LianaLiteLogin { let client = AuthClient::new( service_config.auth_api_url, service_config.auth_api_public_key, - auth_config.email, + setting.email, ); - connect_with_refresh_token( + connect_with_credentials( client, - auth_config.refresh_token, - auth_config.wallet_id, + setting.wallet_id, service_config.backend_api_url, network, + datadir, ) .await }, @@ -327,9 +338,11 @@ impl LianaLiteLogin { self.auth_error = None; let wallet_id = self.wallet_id.clone(); let network = self.network; + let datadir = self.datadir.clone(); return Task::perform( async move { - connect(client, otp, wallet_id, backend_api_url, network).await + connect(client, otp, wallet_id, backend_api_url, network, datadir) + .await }, Message::Connected, ); @@ -343,21 +356,8 @@ impl LianaLiteLogin { return Task::perform(async move { Some(client) }, Message::Install); } Ok(BackendState::WalletExists(client, wallet, coins)) => { - let datadir = self.datadir.clone(); - let network = self.network; return Task::perform( - async move { - update_wallet_auth_settings( - datadir, - network, - wallet.clone(), - client.user_email().to_string(), - client.auth().await.refresh_token, - ) - .await?; - - Ok((client, wallet, coins)) - }, + async move { Ok((client, wallet, coins)) }, Message::Run, ); } @@ -494,68 +494,20 @@ impl LianaLiteLogin { } } -async fn update_wallet_auth_settings( - datadir: LianaDirectory, - network: Network, - wallet: api::Wallet, - email: String, - refresh_token: String, -) -> Result<(), Error> { - let network_dir = datadir.network_directory(network); - let mut settings = Settings::from_file(&network_dir)?; - - let descriptor_checksum = wallet - .descriptor - .to_string() - .split_once('#') - .map(|(_, checksum)| checksum) - .expect("Failed to get checksum from a valid LianaDescriptor") - .to_string(); - - let remote_backend_auth = Some(AuthConfig { - email, - wallet_id: wallet.id.clone(), - refresh_token, - }); - - if let Some(wallet_settings) = settings.wallets.iter_mut().find(|w| { - if let Some(auth) = &w.remote_backend_auth { - auth.wallet_id == wallet.id - } else { - false - } - }) { - wallet_settings.remote_backend_auth = remote_backend_auth; - } else { - tracing::info!("Wallet id was not found in the settings, adding now the wallet settings to the settings.json file"); - settings.wallets.insert( - 0, - WalletSetting { - name: wallet.name, - descriptor_checksum, - keys: Vec::new(), - hardware_wallets: Vec::new(), - remote_backend_auth, - }, - ); - } - - settings.to_file(&network_dir).map_err(|e| { - DaemonError::Unexpected(format!("Cannot access to settings.json file: {}", e)) - })?; - - Ok(()) -} - pub async fn connect( auth: AuthClient, token: String, wallet_id: String, backend_api_url: String, network: Network, + liana_directory: LianaDirectory, ) -> Result { + let network_dir = liana_directory.network_directory(network); let access = auth.verify_otp(token.trim_end()).await?; - let client = BackendClient::connect(auth, backend_api_url, access.clone(), network).await?; + let client = + BackendClient::connect(auth.clone(), backend_api_url, access.clone(), network).await?; + + update_connect_cache(&network_dir, &access, &auth, false).await?; let wallets = client.list_wallets().await?; if wallets.is_empty() { @@ -566,25 +518,35 @@ pub async fn connect( let first = wallets.first().cloned().ok_or(DaemonError::NoAnswer)?; let (wallet_client, wallet) = client.connect_wallet(first); let coins = coins_to_cache(Arc::new(wallet_client.clone())).await?; + Ok(BackendState::WalletExists(wallet_client, wallet, coins)) } else if let Some(wallet) = wallets.into_iter().find(|w| w.id == wallet_id) { let (wallet_client, wallet) = client.connect_wallet(wallet); let coins = coins_to_cache(Arc::new(wallet_client.clone())).await?; + Ok(BackendState::WalletExists(wallet_client, wallet, coins)) } else { Ok(BackendState::NoWallet(client)) } } -pub async fn connect_with_refresh_token( +pub async fn connect_with_credentials( auth: AuthClient, - refresh_token: String, wallet_id: String, backend_api_url: String, network: Network, + liana_directory: LianaDirectory, ) -> Result { - let access = auth.refresh_token(&refresh_token).await?; - let client = BackendClient::connect(auth, backend_api_url, access.clone(), network).await?; + let network_dir = liana_directory.network_directory(network); + let mut tokens = cache::Credential::from_cache(&network_dir, &auth.email)? + .ok_or(Error::CredentialsMissing)? + .tokens; + + if tokens.expires_at < chrono::Utc::now().timestamp() { + tokens = cache::update_connect_cache(&network_dir, &tokens, &auth, true).await?; + } + + let client = BackendClient::connect(auth, backend_api_url, tokens, network).await?; if let Some(wallet) = client .list_wallets() From fe14c7807e60a080052ebc94c1e0d160c8db8ed6 Mon Sep 17 00:00:00 2001 From: edouardparis Date: Thu, 1 May 2025 15:20:31 +0200 Subject: [PATCH 2/2] pin email in liana connect login --- liana-gui/src/main.rs | 18 +- liana-gui/src/services/connect/login.rs | 323 ++++++++++-------------- 2 files changed, 144 insertions(+), 197 deletions(-) diff --git a/liana-gui/src/main.rs b/liana-gui/src/main.rs index 400d1347..a5440470 100644 --- a/liana-gui/src/main.rs +++ b/liana-gui/src/main.rs @@ -223,14 +223,13 @@ impl GUI { ); let network_dir = datadir_path.network_directory(network); if let Ok(settings) = app::settings::Settings::from_file(&network_dir) { - if settings + if let Some(setting) = settings .wallets - .first() - .map(|w| w.remote_backend_auth.is_some()) - == Some(true) + .into_iter() + .find_map(|w| w.remote_backend_auth) { let (login, command) = - login::LianaLiteLogin::new(datadir_path, network, settings); + login::LianaLiteLogin::new(datadir_path, network, setting); self.state = State::Login(Box::new(login)); command.map(|msg| Message::Login(Box::new(msg))) } else { @@ -296,14 +295,13 @@ impl GUI { let network_dir = i.datadir.network_directory(i.network); let settings = app::settings::Settings::from_file(&network_dir) .expect("A settings file was created"); - if settings + if let Some(setting) = settings .wallets - .first() - .map(|w| w.remote_backend_auth.is_some()) - == Some(true) + .into_iter() + .find_map(|w| w.remote_backend_auth) { let (login, command) = - login::LianaLiteLogin::new(i.datadir.clone(), i.network, settings); + login::LianaLiteLogin::new(i.datadir.clone(), i.network, setting); self.state = State::Login(Box::new(login)); command.map(|msg| Message::Login(Box::new(msg))) } else { diff --git a/liana-gui/src/services/connect/login.rs b/liana-gui/src/services/connect/login.rs index 2c872637..e063cb8f 100644 --- a/liana-gui/src/services/connect/login.rs +++ b/liana-gui/src/services/connect/login.rs @@ -13,7 +13,7 @@ use lianad::commands::ListCoinsResult; use crate::{ app::{ cache::coins_to_cache, - settings::{Settings, SettingsError}, + settings::{AuthConfig, SettingsError}, }, daemon::DaemonError, dir::LianaDirectory, @@ -99,8 +99,6 @@ pub enum Message { #[derive(Debug, Clone)] pub enum ViewMessage { RequestOTP, - EditEmail, - EmailEdited(String), OTPEdited(String), BackToLauncher(Network), } @@ -116,6 +114,7 @@ pub struct LianaLiteLogin { pub network: Network, wallet_id: String, + email: String, processing: bool, step: ConnectionStep, @@ -128,13 +127,10 @@ pub struct LianaLiteLogin { pub enum ConnectionStep { CheckingAuthFile, - EnterEmail { - email: form::Value, - }, + CheckEmail, EnterOtp { client: AuthClient, backend_api_url: String, - email: String, otp: form::Value, }, } @@ -143,63 +139,41 @@ impl LianaLiteLogin { pub fn new( datadir: LianaDirectory, network: Network, - settings: Settings, + setting: AuthConfig, ) -> (Self, Task) { - match settings - .wallets - .first() - .cloned() - .and_then(|w| w.remote_backend_auth) - .ok_or(Error::Unexpected( - "Missing auth configuration in settings.json".to_string(), - )) { - Err(e) => ( - Self { - network, - datadir, - step: ConnectionStep::EnterEmail { - email: form::Value::default(), - }, - wallet_id: String::new(), - connection_error: Some(e), - auth_error: None, - processing: true, - }, - Task::none(), - ), - Ok(setting) => ( - Self { - network, - datadir: datadir.clone(), - step: ConnectionStep::CheckingAuthFile, - connection_error: None, - wallet_id: setting.wallet_id.clone(), - auth_error: None, - processing: true, - }, - Task::perform( - async move { - let service_config = super::client::get_service_config(network) - .await - .map_err(|e| Error::Unexpected(e.to_string()))?; - let client = AuthClient::new( - service_config.auth_api_url, - service_config.auth_api_public_key, - setting.email, - ); - connect_with_credentials( - client, - setting.wallet_id, - service_config.backend_api_url, - network, - datadir, - ) + ( + Self { + network, + datadir: datadir.clone(), + step: ConnectionStep::CheckingAuthFile, + connection_error: None, + wallet_id: setting.wallet_id.clone(), + email: setting.email.clone(), + auth_error: None, + processing: true, + }, + Task::perform( + async move { + let service_config = super::client::get_service_config(network) .await - }, - Message::Connected, - ), + .map_err(|e| Error::Unexpected(e.to_string()))?; + let client = AuthClient::new( + service_config.auth_api_url, + service_config.auth_api_public_key, + setting.email, + ); + connect_with_credentials( + client, + setting.wallet_id, + service_config.backend_api_url, + network, + datadir, + ) + .await + }, + Message::Connected, ), - } + ) } pub fn update(&mut self, message: Message) -> Task { @@ -218,36 +192,27 @@ impl LianaLiteLogin { ); } Err(e) => { - self.connection_error = Some(e); - self.step = ConnectionStep::EnterEmail { - email: form::Value::default(), - }; + // Do not display error, if the Liana-Connect cache does not exist, + // simply ask user to do the authentication steps. + if !matches!(e, Error::CredentialsMissing) { + self.connection_error = Some(e); + } + self.step = ConnectionStep::CheckEmail; } } } } - ConnectionStep::EnterEmail { email } => match message { - Message::View(ViewMessage::EmailEdited(value)) => { - email.valid = value.is_empty() - || email_address::EmailAddress::parse_with_options( - &value, - email_address::Options::default().with_required_tld(), - ) - .is_ok(); - email.value = value; - } + ConnectionStep::CheckEmail => match message { Message::View(ViewMessage::RequestOTP) => { - if email.value.is_empty() { - email.valid = false; - } else if email.valid { - let email = email.value.clone(); - let network = self.network; - self.processing = true; - self.connection_error = None; - self.auth_error = None; - return Task::perform( - async move { - let config = super::client::get_service_config(network) + let email = self.email.clone(); + let network = self.network; + self.processing = true; + self.connection_error = None; + self.auth_error = None; + return Task::perform( + async move { + let config = + super::client::get_service_config(network) .await .map_err(|e| { if e.status() == Some(reqwest::StatusCode::NOT_FOUND) { @@ -258,24 +223,22 @@ impl LianaLiteLogin { Error::Unexpected(e.to_string()) } })?; - let client = AuthClient::new( - config.auth_api_url, - config.auth_api_public_key, - email, - ); - client.sign_in_otp().await?; - Ok((client, config.backend_api_url)) - }, - Message::OTPRequested, - ); - } + let client = AuthClient::new( + config.auth_api_url, + config.auth_api_public_key, + email, + ); + client.sign_in_otp().await?; + Ok((client, config.backend_api_url)) + }, + Message::OTPRequested, + ); } Message::OTPRequested(res) => { self.processing = false; match res { Ok((client, backend_api_url)) => { self.step = ConnectionStep::EnterOtp { - email: email.value.to_owned(), otp: form::Value::default(), client, backend_api_url, @@ -290,18 +253,9 @@ impl LianaLiteLogin { }, ConnectionStep::EnterOtp { client, - email, otp, backend_api_url, } => match message { - Message::View(ViewMessage::EditEmail) => { - self.step = ConnectionStep::EnterEmail { - email: form::Value { - value: email.clone(), - valid: true, - }, - }; - } Message::View(ViewMessage::RequestOTP) => { *otp = form::Value::default(); let client = client.clone(); @@ -390,88 +344,66 @@ impl LianaLiteLogin { pub fn view(&self) -> Element { let content = Into::>::into( Container::new( - Column::new() - .spacing(100) - .align_x(Alignment::Center) - .push( - Column::new() - .align_x(Alignment::Center) - .spacing(20) - .width(Length::Fill) - .push(h2("Liana Connect")) - .push( - Column::new() - .max_width(500) - .spacing(20) - .push(match &self.step { - ConnectionStep::CheckingAuthFile => Column::new(), - ConnectionStep::EnterEmail { email } => Column::new() - .spacing(20) - .push_maybe( - self.auth_error - .map(|e| text(e).style(theme::text::warning)), - ) - .push( - form::Form::new_trimmed("email", email, |msg| { - ViewMessage::EmailEdited(msg) - }) - .size(P1_SIZE) - .padding(10) - .warning("Email is not valid"), - ) - .push(button::secondary(None, "Next").on_press_maybe( - if self.processing { + Column::new().spacing(100).align_x(Alignment::Center).push( + Column::new() + .align_x(Alignment::Center) + .spacing(20) + .width(Length::Fill) + .push(h2("Liana Connect")) + .push( + Column::new() + .max_width(500) + .spacing(20) + .push(match &self.step { + ConnectionStep::CheckingAuthFile => Column::new(), + ConnectionStep::CheckEmail => Column::new() + .spacing(20) + .align_x(Alignment::Center) + .push_maybe( + self.auth_error + .map(|e| text(e).style(theme::text::warning)), + ) + .push(text(&self.email)) + .push( + button::secondary(None, "Login") + .width(Length::Fixed(200.0)) + .on_press_maybe(if self.processing { None } else { Some(ViewMessage::RequestOTP) - }, - )), - ConnectionStep::EnterOtp { otp, .. } => Column::new() - .push(text("An authentication was send to your email")) - .push_maybe( - self.auth_error - .map(|e| text(e).style(theme::text::warning)), - ) - .spacing(20) - .push( - form::Form::new_trimmed("Token", otp, |msg| { - ViewMessage::OTPEdited(msg) - }) - .size(P1_SIZE) - .padding(10) - .warning("Token is not valid"), - ) - .push( - Row::new() - .spacing(10) - .push( - button::secondary( - Some(icon::previous_icon()), - "Change email", - ) - .on_press(ViewMessage::EditEmail), - ) - .push( - button::secondary(None, "Resend token") - .on_press_maybe(if self.processing { - None - } else { - Some(ViewMessage::RequestOTP) - }), - ), + }), + ), + ConnectionStep::EnterOtp { otp, .. } => Column::new() + .spacing(20) + .align_x(Alignment::Center) + .push(text("An authentication was sent to your email:")) + .push(text(&self.email)) + .push_maybe( + self.auth_error + .map(|e| text(e).style(theme::text::warning)), + ) + .push( + form::Form::new_trimmed("Token", otp, |msg| { + ViewMessage::OTPEdited(msg) + }) + .size(P1_SIZE) + .padding(10) + .warning("Token is not valid"), + ) + .push( + Row::new().spacing(10).push( + button::secondary(None, "Resend token") + .width(Length::Fixed(200.0)) + .on_press_maybe(if self.processing { + None + } else { + Some(ViewMessage::RequestOTP) + }), ), - }), - ), - ) - .push_maybe(if !matches!(self.step, ConnectionStep::CheckingAuthFile) { - Some( - button::secondary(Some(icon::previous_icon()), "Change network") - .width(Length::Fixed(200.0)) - .on_press(ViewMessage::BackToLauncher(self.network)), - ) - } else { - None - }), + ), + }), + ), + ), ) .padding(50) .center_x(Length::Fill) @@ -490,7 +422,20 @@ impl LianaLiteLogin { ); } - col.push(content).into() + col.push_maybe(if !matches!(self.step, ConnectionStep::CheckingAuthFile) { + Some( + Container::new( + button::secondary(Some(icon::previous_icon()), "Go back") + .width(Length::Fixed(200.0)) + .on_press(Message::View(ViewMessage::BackToLauncher(self.network))), + ) + .padding(20), + ) + } else { + None + }) + .push(content) + .into() } } @@ -538,7 +483,11 @@ pub async fn connect_with_credentials( liana_directory: LianaDirectory, ) -> Result { let network_dir = liana_directory.network_directory(network); - let mut tokens = cache::Credential::from_cache(&network_dir, &auth.email)? + let mut tokens = cache::Account::from_cache(&network_dir, &auth.email) + .map_err(|e| match e { + ConnectCacheError::NotFound => Error::CredentialsMissing, + _ => e.into(), + })? .ok_or(Error::CredentialsMissing)? .tokens;