installer: reuse existing accounts

This commit is contained in:
edouardparis 2025-06-16 16:54:08 +02:00
parent ad75d77e94
commit e5a68c359f
5 changed files with 159 additions and 29 deletions

View File

@ -102,6 +102,8 @@ pub enum SelectBackend {
// Commands messages
OTPRequested(Result<(AuthClient, String), Error>),
OTPResent(Result<(), Error>),
ExistingConnectAccounts(Vec<String>),
SelectConnectAccount(String),
Connected(Result<context::RemoteBackend, Error>),
}

View File

@ -137,7 +137,8 @@ impl Installer {
BackupDescriptor::default().into(),
RegisterDescriptor::new_create_wallet().into(),
ChooseBackend::new(network).into(),
RemoteBackendLogin::new(network).into(),
RemoteBackendLogin::new(network, destination_path.network_directory(network))
.into(),
SelectBitcoindTypeStep::new().into(),
InternalBitcoindStep::new(&context.liana_directory).into(),
DefineNode::default().into(),
@ -147,7 +148,8 @@ impl Installer {
UserFlow::ShareXpubs => vec![ShareXpubs::new(network, signer.clone()).into()],
UserFlow::AddWallet => vec![
ChooseBackend::new(network).into(),
RemoteBackendLogin::new(network).into(),
RemoteBackendLogin::new(network, destination_path.network_directory(network))
.into(),
ImportRemoteWallet::new(network).into(),
ImportDescriptor::new(network).into(),
RecoverMnemonic::default().into(),

View File

@ -7,6 +7,7 @@ use liana_ui::{component::form, widget::Element};
use crate::{
daemon::DaemonError,
dir::NetworkDirectory,
hw::HardwareWallets,
installer::{
context::{self, Context, RemoteBackend},
@ -18,6 +19,7 @@ use crate::{
self,
auth::{AuthClient, AuthError},
backend::{api, BackendClient},
cache,
},
};
@ -97,6 +99,8 @@ pub enum ConnectionStep {
pub struct RemoteBackendLogin {
network: Network,
network_dir: NetworkDirectory,
connect_accounts: Vec<String>,
processing: bool,
step: ConnectionStep,
connection_error: Option<Error>,
@ -104,9 +108,11 @@ pub struct RemoteBackendLogin {
}
impl RemoteBackendLogin {
pub fn new(network: Network) -> Self {
pub fn new(network: Network, network_dir: NetworkDirectory) -> Self {
Self {
network,
network_dir,
connect_accounts: Vec::new(),
step: ConnectionStep::EnterEmail {
email: form::Value::default(),
},
@ -140,6 +146,21 @@ impl Step for RemoteBackendLogin {
.is_ok();
email.value = value;
}
Message::SelectBackend(message::SelectBackend::ExistingConnectAccounts(
accounts,
)) => {
self.connect_accounts = accounts;
}
Message::SelectBackend(message::SelectBackend::SelectConnectAccount(email)) => {
return Task::perform(
connect_with_existing_account(
email,
self.network,
self.network_dir.clone(),
),
|msg| Message::SelectBackend(message::SelectBackend::Connected(msg)),
)
}
Message::SelectBackend(message::SelectBackend::RequestOTP) => {
if email.value.is_empty() {
email.valid = false;
@ -189,6 +210,32 @@ impl Step for RemoteBackendLogin {
}
}
}
Message::SelectBackend(message::SelectBackend::Connected(res)) => {
self.processing = false;
match res {
Ok(remote_backend) => {
self.step = ConnectionStep::Connected {
email: remote_backend
.user_email()
.expect("Gui connected to Liana backend")
.to_string(),
remote_backend,
};
return Task::perform(async move {}, |_| Message::Next);
}
Err(e) => {
if let Error::Auth(AuthError { http_status, .. }) = e {
if http_status == Some(403) {
self.auth_error = Some("Token has expired or is invalid")
} else {
self.connection_error = Some(e);
}
} else {
self.connection_error = Some(e);
}
}
}
}
_ => {}
},
ConnectionStep::EnterOtp {
@ -243,7 +290,6 @@ impl Step for RemoteBackendLogin {
.map(Message::SelectBackend);
}
}
Message::SelectBackend(message::SelectBackend::Connected(res)) => {
self.processing = false;
match res {
@ -281,6 +327,27 @@ impl Step for RemoteBackendLogin {
Task::none()
}
fn load(&self) -> Task<Message> {
if let Ok(cache) = cache::ConnectCache::from_file(&self.network_dir) {
Task::perform(
async move {
cache
.accounts
.into_iter()
.map(|a| a.email)
.collect::<Vec<String>>()
},
|accounts| {
Message::SelectBackend(message::SelectBackend::ExistingConnectAccounts(
accounts,
))
},
)
} else {
Task::none()
}
}
fn apply(&mut self, ctx: &mut Context) -> bool {
if let ConnectionStep::Connected { remote_backend, .. } = &self.step {
ctx.remote_backend = remote_backend.clone();
@ -310,6 +377,7 @@ impl Step for RemoteBackendLogin {
email,
self.processing,
self.connection_error.as_ref(),
&self.connect_accounts,
self.auth_error,
),
ConnectionStep::EnterOtp { email, otp, .. } => view::connection_step_enter_otp(
@ -330,6 +398,36 @@ impl Step for RemoteBackendLogin {
}
}
pub async fn connect_with_existing_account(
email: String,
network: Network,
network_dir: NetworkDirectory,
) -> Result<context::RemoteBackend, Error> {
let config = client::get_service_config(network).await.map_err(|e| {
if e.status() == Some(reqwest::StatusCode::NOT_FOUND) {
Error::Unexpected("Remote servers are unresponsive".to_string())
} else {
Error::Unexpected(e.to_string())
}
})?;
let client = AuthClient::new(config.auth_api_url, config.auth_api_public_key, email);
let mut tokens = cache::Account::from_cache(&network_dir, &client.email)
.map_err(|_| Error::Unexpected("Account must be in cache".to_string()))?
.ok_or(Error::Unexpected("Account must be in cache".to_string()))?
.tokens;
if tokens.expires_at < chrono::Utc::now().timestamp() {
tokens = cache::update_connect_cache(&network_dir, &tokens, &client, true)
.await
.map_err(|e| Error::Unexpected(format!("Failed to update cache: {}", e)))?;
}
let client = BackendClient::connect(client, config.backend_api_url, tokens, network).await?;
Ok(RemoteBackend::WithoutWallet(client))
}
pub async fn connect(
auth: AuthClient,
token: String,

View File

@ -2102,18 +2102,40 @@ pub fn login(progress: (usize, usize), connection_step: Element<Message>) -> Ele
}
pub fn connection_step_enter_email<'a>(
email: &form::Value<String>,
email: &'a form::Value<String>,
processing: bool,
connection_error: Option<&Error>,
auth_error: Option<&'static str>,
connection_error: Option<&'a Error>,
accounts: &'a [String],
auth_error: Option<&'a str>,
) -> Element<'a, Message> {
Column::new()
.spacing(20)
.push_maybe(if !accounts.is_empty() {
Some(text("Choose an account you are already using:"))
} else {
None
})
.push(
accounts
.iter()
.fold(Row::new().spacing(10), |row, a| {
row.push(
Button::new(Container::new(p1_regular(a)).padding(5))
.style(theme::button::secondary)
.on_press(Message::SelectBackend(
message::SelectBackend::SelectConnectAccount(a.clone()),
)),
)
})
.wrap(),
)
.push_maybe(connection_error.map(|e| text(e.to_string()).style(theme::text::warning)))
.push_maybe(auth_error.map(|e| text(e.to_string()).style(theme::text::warning)))
.push(text(
"Enter the email you want to associate with the wallet:",
))
.push(if accounts.is_empty() {
text("Enter an email you want to associate with the wallet:")
} else {
text("Or enter a new email you want to associate with the wallet:")
})
.push(
form::Form::new_trimmed("email", email, |msg| {
Message::SelectBackend(message::SelectBackend::EmailEdited(msg))
@ -2123,13 +2145,15 @@ pub fn connection_step_enter_email<'a>(
.warning("Email is not valid"),
)
.push(
button::secondary(None, "Next")
.on_press_maybe(if processing || !email.valid {
None
} else {
Some(Message::SelectBackend(message::SelectBackend::RequestOTP))
})
.width(Length::Fixed(200.0)),
Row::new().push(Space::with_width(Length::Fill)).push(
button::secondary(None, "Send token")
.on_press_maybe(if processing || !email.valid {
None
} else {
Some(Message::SelectBackend(message::SelectBackend::RequestOTP))
})
.width(Length::Fixed(200.0)),
),
)
.into()
}

View File

@ -27,19 +27,8 @@ impl ConnectCache {
})
}
}
}
#[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<Option<Self>, ConnectCacheError> {
pub fn from_file(network_dir: &NetworkDirectory) -> Result<Self, ConnectCacheError> {
let mut path = network_dir.path().to_path_buf();
path.push(CONNECT_CACHE_FILENAME);
@ -53,6 +42,21 @@ impl Account {
ConnectCacheError::ReadingFile(format!("Parsing settings file: {}", e))
})
})
}
}
#[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<Option<Self>, ConnectCacheError> {
ConnectCache::from_file(network_dir)
.map(|cache| cache.accounts.into_iter().find(|c| c.email == email))
}
}