Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions liana-business/business-settings/src/message.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
//! Message types for business settings UI.

use liana_gui::{app::view::Close, export::ImportExportMessage};

/// Settings section for navigation.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Section {
Expand All @@ -19,4 +21,25 @@ pub enum Msg {

/// Register wallet on selected device.
RegisterWallet,

/// Copy descriptor to clipboard.
CopyDescriptor,

/// Export encrypted descriptor.
ExportEncryptedDescriptor,

/// Handle export progress/result.
Export(ImportExportMessage),
}

impl From<ImportExportMessage> for Msg {
fn from(msg: ImportExportMessage) -> Self {
Msg::Export(msg)
}
}

impl Close for Msg {
fn close() -> Self {
Msg::Export(ImportExportMessage::Close)
}
}
89 changes: 83 additions & 6 deletions liana-business/business-settings/src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,21 @@

use std::sync::Arc;

use iced::{Subscription, Task};
use iced::{clipboard, Subscription, Task};
use liana_gui::{
app::{
cache::Cache,
menu::Menu,
message::Message,
settings::SettingsUI,
state::{settings::wallet::RegisterWalletModal, State},
state::{export::ExportModal, settings::wallet::RegisterWalletModal, State},
view,
wallet::Wallet,
Config,
},
daemon::{Daemon, DaemonBackend},
dir::LianaDirectory,
export::{ImportExportMessage, ImportExportType},
};
use liana_ui::widget::{modal, Element};

Expand All @@ -30,6 +31,7 @@ pub struct BusinessSettingsUI {
#[allow(dead_code)]
processing: bool,
register_modal: Option<RegisterWalletModal>,
export_modal: Option<ExportModal>,
}

impl SettingsUI<Msg> for BusinessSettingsUI {
Expand All @@ -47,6 +49,7 @@ impl SettingsUI<Msg> for BusinessSettingsUI {
current_section: None,
processing: false,
register_modal: None,
export_modal: None,
};
// TODO: Fetch fiat setting from backend on load
(ui, Task::none())
Expand All @@ -65,24 +68,65 @@ impl SettingsUI<Msg> for BusinessSettingsUI {
}
Msg::SelectSection(section) => self.on_select_section(section),
Msg::RegisterWallet => Task::none(), // Handled in State::update()
Msg::CopyDescriptor => {
let descriptor = self.wallet.main_descriptor.to_string();
clipboard::write(descriptor)
}
Msg::ExportEncryptedDescriptor => {
self.export_modal = Some(ExportModal::new(
None,
ImportExportType::ExportEncryptedDescriptor(Box::new(
self.wallet.main_descriptor.clone(),
)),
));
self.export_modal
.as_ref()
.map(|m| m.launch(true))
.unwrap_or(Task::none())
}
Msg::Export(msg) => {
if matches!(msg, ImportExportMessage::Close) {
self.export_modal = None;
return Task::none();
}
if let Some(modal) = &mut self.export_modal {
modal.update(msg)
} else {
Task::none()
}
}
}
}

fn view<'a>(&'a self, _cache: &'a Cache) -> Element<'a, Msg> {
match self.current_section {
let content = match self.current_section {
None => views::list_view(),
Some(Section::Wallet) => views::wallet_view(self),
Some(Section::About) => views::about_view(),
};

if let Some(export_modal) = &self.export_modal {
export_modal.view(content)
} else {
content
}
}

fn subscription(&self) -> Subscription<Msg> {
Subscription::none()
if let Some(modal) = &self.export_modal {
modal
.subscription()
.map(|s| s.map(|m| Msg::Export(ImportExportMessage::Progress(m))))
.unwrap_or(Subscription::none())
} else {
Subscription::none()
}
}

fn stop(&mut self) {
self.current_section = None;
self.register_modal = None;
self.export_modal = None;
}

fn reload(&mut self, _daemon: Arc<dyn Daemon + Sync + Send>, wallet: Arc<Wallet>) -> Task<Msg> {
Expand Down Expand Up @@ -112,6 +156,13 @@ impl State for BusinessSettingsUI {
view::Message::Settings(view::SettingsMessage::AboutSection)
}
Msg::RegisterWallet => view::Message::Settings(view::SettingsMessage::RegisterWallet),
Msg::CopyDescriptor => {
view::Message::Clipboard(self.wallet.main_descriptor.to_string())
}
Msg::ExportEncryptedDescriptor => {
view::Message::Settings(view::SettingsMessage::ExportEncryptedDescriptor)
}
Msg::Export(msg) => view::Message::ImportExport(msg),
});
let dashboard = view::dashboard(&Menu::Settings, cache, None, content);

Expand Down Expand Up @@ -162,6 +213,27 @@ impl State for BusinessSettingsUI {
Task::none()
}
}
Message::View(view::Message::Settings(
view::SettingsMessage::ExportEncryptedDescriptor,
)) => {
SettingsUI::update(self, daemon, cache, Msg::ExportEncryptedDescriptor).map(|m| {
if let Msg::Export(export_msg) = m {
Message::View(view::Message::ImportExport(export_msg))
} else {
Message::View(view::Message::Close)
}
})
}
Message::View(view::Message::ImportExport(msg)) => {
SettingsUI::update(self, daemon, cache, Msg::Export(msg)).map(|m| {
if let Msg::Export(export_msg) = m {
Message::View(view::Message::ImportExport(export_msg))
} else {
Message::View(view::Message::Close)
}
})
}
Message::View(view::Message::Clipboard(text)) => clipboard::write(text),
Message::View(view::Message::Settings(ref settings_msg)) => {
let msg = match settings_msg {
view::SettingsMessage::EditWalletSettings => {
Expand All @@ -180,11 +252,16 @@ impl State for BusinessSettingsUI {
}

fn subscription(&self) -> Subscription<Message> {
if let Some(modal) = &self.register_modal {
let register_sub = if let Some(modal) = &self.register_modal {
modal.subscription()
} else {
Subscription::none()
}
};
let export_sub = SettingsUI::subscription(self).map(|msg| match msg {
Msg::Export(m) => Message::View(view::Message::ImportExport(m)),
_ => Message::View(view::Message::Close),
});
Subscription::batch([register_sub, export_sub])
}

fn reload(
Expand Down
20 changes: 20 additions & 0 deletions liana-business/business-settings/src/views/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

use iced::widget::{scrollable, Column, Container, Row, Space};
use iced::{Alignment, Length};
use liana_gui::app::view::settings::display_policy;
use liana_ui::{
component::{badge, button, card, separation, text::*},
icon, theme,
Expand Down Expand Up @@ -40,6 +41,16 @@ pub fn list_view() -> Element<'static, Msg> {
pub fn wallet_view(state: &BusinessSettingsUI) -> Element<'_, Msg> {
let header = section_header("Wallet");

// Policy display
let policy = state.wallet.main_descriptor.policy();
let policy_card = card::simple(display_policy(
policy,
state.wallet.keys_aliases.clone(),
&state.wallet.provider_keys,
))
.width(Length::Fill);

// Descriptor display
let descriptor = state.wallet.main_descriptor.to_string();
let descriptor_card = card::simple(
Column::new()
Expand All @@ -58,6 +69,14 @@ pub fn wallet_view(state: &BusinessSettingsUI) -> Element<'_, Msg> {
Row::new()
.spacing(10)
.push(Space::with_width(Length::Fill))
.push(
button::secondary(Some(icon::backup_icon()), "Encrypted descriptor")
.on_press(Msg::ExportEncryptedDescriptor),
)
.push(
button::secondary(Some(icon::clipboard_icon()), "Copy")
.on_press(Msg::CopyDescriptor),
)
.push(
button::secondary(Some(icon::chip_icon()), "Register on device")
.on_press(Msg::RegisterWallet),
Expand All @@ -71,6 +90,7 @@ pub fn wallet_view(state: &BusinessSettingsUI) -> Element<'_, Msg> {
.spacing(20)
.push(header)
.push(descriptor_card)
.push(policy_card)
.width(Length::Fill)
.into()
}
Expand Down
82 changes: 43 additions & 39 deletions liana-gui/src/app/view/settings/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1128,6 +1128,13 @@ pub fn wallet_settings<'a>(
)
.width(Length::Fill);

// Convert form values to HashMap for display_policy
let aliases_map: HashMap<Fingerprint, String> = keys_aliases
.iter()
.filter(|(_, v)| !v.value.is_empty())
.map(|(fg, v)| (*fg, v.value.clone()))
.collect();

dashboard(
&Menu::Settings,
cache,
Expand All @@ -1139,7 +1146,7 @@ pub fn wallet_settings<'a>(
.push(
card::simple(display_policy(
descriptor.policy(),
keys_aliases,
aliases_map,
provider_keys,
))
.width(Length::Fill),
Expand All @@ -1148,15 +1155,18 @@ pub fn wallet_settings<'a>(
)
}

fn display_policy<'a>(
/// Display wallet policy in human-readable format.
///
/// Shows primary path and recovery paths with thresholds, key names, and timelocks.
pub fn display_policy<M: 'static>(
policy: LianaPolicy,
keys_aliases: &'a [(Fingerprint, form::Value<String>)],
provider_keys: &'a HashMap<Fingerprint, ProviderKey>,
) -> Element<'a, Message> {
keys_aliases: HashMap<Fingerprint, String>,
provider_keys: &HashMap<Fingerprint, ProviderKey>,
) -> Element<'_, M> {
let (primary_threshold, primary_keys) = policy.primary_path().thresh_origins();
let recovery_paths = policy.recovery_paths();

// The iteration over an HashMap keys can have a different order at each refresh
// The iteration over a HashMap keys can have a different order at each refresh
let mut primary_keys: Vec<Fingerprint> = primary_keys.into_keys().collect();
primary_keys.sort();

Expand All @@ -1181,22 +1191,19 @@ fn display_policy<'a>(
.iter()
.enumerate()
.fold(Row::new().spacing(5), |row, (i, k)| {
let content = if let Some(alias) = keys_aliases
.iter()
.find(|(fg, a)| fg == k && !a.value.is_empty())
.map(|(_, f)| &f.value)
{
Container::new(
iced_tooltip::Tooltip::new(
text(alias).bold(),
text(k.to_string()),
iced_tooltip::Position::Bottom,
let content =
if let Some(alias) = keys_aliases.get(k).filter(|a| !a.is_empty()) {
Container::new(
iced_tooltip::Tooltip::new(
text(alias).bold(),
text(k.to_string()),
iced_tooltip::Position::Bottom,
)
.style(theme::card::simple),
)
.style(theme::card::simple),
)
} else {
Container::new(text(format!("[{}]", k)).bold())
};
} else {
Container::new(text(format!("[{}]", k)).bold())
};
if primary_keys.len() == 1 || i == primary_keys.len() - 1 {
row.push(content)
} else if i <= primary_keys.len() - 2 {
Expand All @@ -1211,7 +1218,7 @@ fn display_policy<'a>(
for (i, (sequence, recovery_path)) in recovery_paths.iter().enumerate() {
let (threshold, recovery_keys) = recovery_path.thresh_origins();

// The iteration over an HashMap keys can have a different order at each refresh
// Sort for consistent ordering
let mut recovery_keys: Vec<Fingerprint> = recovery_keys.into_keys().collect();
recovery_keys.sort();

Expand All @@ -1234,22 +1241,19 @@ fn display_policy<'a>(
.push(recovery_keys.iter().enumerate().fold(
Row::new().spacing(5),
|row, (i, k)| {
let content = if let Some(alias) = keys_aliases
.iter()
.find(|(fg, a)| fg == k && !a.value.is_empty())
.map(|(_, f)| &f.value)
{
Container::new(
iced_tooltip::Tooltip::new(
text(alias).bold(),
text(k.to_string()),
iced_tooltip::Position::Bottom,
let content =
if let Some(alias) = keys_aliases.get(k).filter(|a| !a.is_empty()) {
Container::new(
iced_tooltip::Tooltip::new(
text(alias).bold(),
text(k.to_string()),
iced_tooltip::Position::Bottom,
)
.style(theme::card::simple),
)
.style(theme::card::simple),
)
} else {
Container::new(text(format!("[{}]", k)).bold())
};
} else {
Container::new(text(format!("[{}]", k)).bold())
};
if recovery_keys.len() == 1 || i == recovery_keys.len() - 1 {
row.push(content)
} else if i <= recovery_keys.len() - 2 {
Expand Down Expand Up @@ -1291,8 +1295,8 @@ fn display_policy<'a>(
.into()
}

/// returns y,m,d
fn expire_message_units(sequence: u32) -> Vec<String> {
/// Converts a block sequence to human-readable time units (y, m, d, h, m).
pub fn expire_message_units(sequence: u32) -> Vec<String> {
let mut n_minutes = sequence * 10;
let n_years = n_minutes / 525960;
n_minutes -= n_years * 525960;
Expand Down
Loading