wallet.rs (15057B)
1 use egui::{vec2, CornerRadius, Layout}; 2 use egui_winit::clipboard::Clipboard; 3 use notedeck::{ 4 get_current_wallet_mut, tr, Accounts, DefaultZapMsats, GlobalWallet, Localization, 5 NotedeckTextStyle, PendingDefaultZapState, Wallet, WalletError, WalletUIState, ZapWallet, 6 }; 7 8 use crate::{nav::RouterAction, route::Route}; 9 10 use super::widgets::styled_button; 11 12 #[derive(Debug)] 13 pub enum WalletState<'a> { 14 Wallet { 15 wallet: &'a mut Wallet, 16 default_zap_state: DefaultZapState<'a>, 17 can_create_local_wallet: bool, 18 }, 19 NoWallet { 20 state: &'a mut WalletUIState, 21 show_local_only: bool, 22 }, 23 } 24 25 type Msats = u64; 26 27 #[derive(Debug)] 28 pub enum DefaultZapState<'a> { 29 Pending(&'a mut PendingDefaultZapState), // User input 30 Valid(&'a Msats), // in milisats 31 } 32 33 pub fn get_default_zap_state(default_zap: &mut DefaultZapMsats) -> DefaultZapState { 34 if default_zap.pending.is_rewriting { 35 return DefaultZapState::Pending(&mut default_zap.pending); 36 } 37 38 if let Some(user_selection) = &default_zap.msats { 39 DefaultZapState::Valid(user_selection) 40 } else { 41 DefaultZapState::Pending(&mut default_zap.pending) 42 } 43 } 44 45 #[derive(Debug)] 46 pub enum WalletAction { 47 SaveURI, 48 AddLocalOnly, 49 Delete, 50 SetDefaultZapSats(String), // in sats 51 EditDefaultZaps, 52 } 53 54 impl WalletAction { 55 pub fn process( 56 &self, 57 accounts: &mut Accounts, 58 global_wallet: &mut GlobalWallet, 59 ) -> Option<RouterAction> { 60 let mut action = None; 61 62 match &self { 63 WalletAction::SaveURI => { 64 let ui_state = &mut global_wallet.ui_state; 65 if ui_state.for_local_only { 66 ui_state.for_local_only = false; 67 68 if accounts.get_selected_wallet_mut().is_some() { 69 return None; 70 } 71 72 let wallet = try_create_wallet(ui_state)?; 73 74 accounts.update_current_account(move |acc| { 75 acc.wallet = Some(wallet.into()); 76 }); 77 } else { 78 if global_wallet.wallet.is_some() { 79 return None; 80 } 81 82 let wallet = try_create_wallet(ui_state)?; 83 84 global_wallet.wallet = Some(wallet.into()); 85 global_wallet.save_wallet(); 86 } 87 } 88 WalletAction::AddLocalOnly => { 89 action = Some(RouterAction::route_to(Route::Wallet( 90 notedeck::WalletType::Local, 91 ))); 92 global_wallet.ui_state.for_local_only = true; 93 } 94 WalletAction::Delete => { 95 if accounts.get_selected_account().wallet.is_some() { 96 accounts.update_current_account(|acc| { 97 acc.wallet = None; 98 }); 99 return None; 100 } 101 102 global_wallet.wallet = None; 103 global_wallet.save_wallet(); 104 } 105 WalletAction::SetDefaultZapSats(new_default) => 's: { 106 let sats = { 107 let Some(wallet) = get_current_wallet_mut(accounts, global_wallet) else { 108 break 's; 109 }; 110 111 let Ok(sats) = new_default.parse::<u64>() else { 112 wallet.default_zap.pending.error_message = 113 Some(notedeck::DefaultZapError::InvalidUserInput); 114 break 's; 115 }; 116 sats 117 }; 118 119 let update_wallet = |wallet: &mut ZapWallet| { 120 wallet.default_zap.set_user_selection(sats * 1000); 121 wallet.default_zap.pending = PendingDefaultZapState::default(); 122 }; 123 124 if accounts.selected_account_has_wallet() 125 && accounts.update_current_account(|acc| { 126 if let Some(wallet) = &mut acc.wallet { 127 update_wallet(wallet); 128 } 129 }) 130 { 131 break 's; 132 } 133 134 let Some(wallet) = &mut global_wallet.wallet else { 135 break 's; 136 }; 137 138 update_wallet(wallet); 139 global_wallet.save_wallet(); 140 } 141 WalletAction::EditDefaultZaps => 's: { 142 let Some(wallet) = get_current_wallet_mut(accounts, global_wallet) else { 143 break 's; 144 }; 145 146 wallet.default_zap.pending.is_rewriting = true; 147 wallet.default_zap.pending.amount_sats = 148 (wallet.default_zap.get_default_zap_msats() / 1000).to_string(); 149 } 150 } 151 action 152 } 153 } 154 155 pub struct WalletView<'a> { 156 state: WalletState<'a>, 157 i18n: &'a mut Localization, 158 clipboard: &'a mut Clipboard, 159 } 160 161 impl<'a> WalletView<'a> { 162 pub fn new( 163 state: WalletState<'a>, 164 i18n: &'a mut Localization, 165 clipboard: &'a mut Clipboard, 166 ) -> Self { 167 Self { 168 state, 169 i18n, 170 clipboard, 171 } 172 } 173 174 pub fn ui(&mut self, ui: &mut egui::Ui) -> Option<WalletAction> { 175 egui::Frame::NONE 176 .inner_margin(egui::Margin::same(8)) 177 .show(ui, |ui| self.inner_ui(ui)) 178 .inner 179 } 180 181 fn inner_ui(&mut self, ui: &mut egui::Ui) -> Option<WalletAction> { 182 match &mut self.state { 183 WalletState::Wallet { 184 wallet, 185 default_zap_state, 186 can_create_local_wallet, 187 } => show_with_wallet( 188 ui, 189 self.i18n, 190 wallet, 191 default_zap_state, 192 *can_create_local_wallet, 193 ), 194 WalletState::NoWallet { 195 state, 196 show_local_only, 197 } => show_no_wallet(ui, self.i18n, state, *show_local_only, self.clipboard), 198 } 199 } 200 } 201 202 fn try_create_wallet(state: &mut WalletUIState) -> Option<Wallet> { 203 let uri = &state.buf; 204 205 let Ok(wallet) = Wallet::new(uri.to_owned()) else { 206 state.error_msg = Some(WalletError::InvalidURI); 207 return None; 208 }; 209 210 *state = WalletUIState::default(); 211 Some(wallet) 212 } 213 214 fn show_no_wallet( 215 ui: &mut egui::Ui, 216 i18n: &mut Localization, 217 state: &mut WalletUIState, 218 show_local_only: bool, 219 clipboard: &mut Clipboard, 220 ) -> Option<WalletAction> { 221 ui.horizontal_wrapped(|ui| { 222 use notedeck_ui::context_menu::{input_context, PasteBehavior}; 223 224 let text_edit = egui::TextEdit::singleline(&mut state.buf) 225 .hint_text( 226 egui::RichText::new(tr!( 227 i18n, 228 "Paste your NWC URI here...", 229 "Placeholder text for NWC URI input" 230 )) 231 .text_style(notedeck::NotedeckTextStyle::Body.text_style()), 232 ) 233 .vertical_align(egui::Align::Center) 234 .desired_width(f32::INFINITY) 235 .min_size(egui::Vec2::new(0.0, 40.0)) 236 .margin(egui::Margin::same(12)) 237 .password(true); 238 239 // add paste context menu 240 input_context( 241 &ui.add(text_edit), 242 clipboard, 243 &mut state.buf, 244 PasteBehavior::Clear, 245 ); 246 247 let Some(error_msg) = &state.error_msg else { 248 return; 249 }; 250 251 let error_str = match error_msg { 252 WalletError::InvalidURI => tr!( 253 i18n, 254 "Invalid NWC URI", 255 "Error message for invalid Nostr Wallet Connect URI" 256 ), 257 WalletError::NoWallet => tr!( 258 i18n, 259 "Add a wallet to continue", 260 "Error message for missing wallet" 261 ), 262 }; 263 ui.colored_label(ui.visuals().warn_fg_color, error_str); 264 }); 265 266 ui.add_space(8.0); 267 268 if show_local_only { 269 ui.checkbox( 270 &mut state.for_local_only, 271 tr!( 272 i18n, 273 "Use this wallet for the current account only", 274 "Checkbox label for using wallet only for current account" 275 ), 276 ); 277 ui.add_space(8.0); 278 } 279 280 ui.with_layout(Layout::top_down(egui::Align::Center), |ui| { 281 ui.add(styled_button( 282 tr!(i18n, "Add Wallet", "Button label to add a wallet").as_str(), 283 notedeck_ui::colors::PINK, 284 )) 285 .clicked() 286 .then_some(WalletAction::SaveURI) 287 }) 288 .inner 289 } 290 291 fn show_with_wallet( 292 ui: &mut egui::Ui, 293 i18n: &mut Localization, 294 wallet: &mut Wallet, 295 default_zap_state: &mut DefaultZapState, 296 can_create_local_wallet: bool, 297 ) -> Option<WalletAction> { 298 ui.horizontal_wrapped(|ui| { 299 let balance = wallet.get_balance(); 300 301 if let Some(balance) = balance { 302 match balance { 303 Ok(msats) => show_balance(ui, *msats), 304 Err(e) => ui.colored_label(egui::Color32::RED, format!("error: {e}")), 305 } 306 } else { 307 ui.with_layout(Layout::top_down(egui::Align::Center), |ui| { 308 ui.add(egui::Spinner::new().size(48.0)) 309 }) 310 .inner 311 } 312 }); 313 314 let mut action = show_default_zap(ui, i18n, default_zap_state); 315 316 ui.with_layout(Layout::bottom_up(egui::Align::Min), |ui| 's: { 317 if ui 318 .add(styled_button( 319 tr!(i18n, "Delete Wallet", "Button label to delete a wallet").as_str(), 320 ui.visuals().window_fill, 321 )) 322 .clicked() 323 { 324 action = Some(WalletAction::Delete); 325 break 's; 326 } 327 328 ui.add_space(12.0); 329 if can_create_local_wallet 330 && ui 331 .checkbox( 332 &mut false, 333 tr!( 334 i18n, 335 "Add a different wallet that will only be used for this account", 336 "Button label to add a different wallet" 337 ), 338 ) 339 .clicked() 340 { 341 action = Some(WalletAction::AddLocalOnly); 342 } 343 }); 344 345 action 346 } 347 348 fn show_balance(ui: &mut egui::Ui, msats: u64) -> egui::Response { 349 let sats = human_format::Formatter::new() 350 .with_decimals(2) 351 .format(msats as f64 / 1000.0); 352 353 ui.with_layout(Layout::top_down(egui::Align::Center), |ui| { 354 ui.label(egui::RichText::new(format!("{sats} sats")).size(48.0)) 355 }) 356 .inner 357 } 358 359 fn show_default_zap( 360 ui: &mut egui::Ui, 361 i18n: &mut Localization, 362 state: &mut DefaultZapState, 363 ) -> Option<WalletAction> { 364 let mut action = None; 365 ui.allocate_ui_with_layout( 366 vec2(ui.available_width(), 50.0), 367 egui::Layout::left_to_right(egui::Align::Center).with_main_wrap(true), 368 |ui| { 369 ui.label(tr!(i18n, "Default amount per zap: ", "Label for default zap amount input")); 370 match state { 371 DefaultZapState::Pending(pending_default_zap_state) => { 372 let text = &mut pending_default_zap_state.amount_sats; 373 374 let font = NotedeckTextStyle::Body.get_font_id(ui.ctx()); 375 let desired_width = { 376 let painter = ui.painter(); 377 let galley = painter.layout_no_wrap( 378 text.clone(), 379 font.clone(), 380 ui.visuals().text_color(), 381 ); 382 let rect_width = galley.rect.width(); 383 if rect_width < 5.0 { 384 10.0 385 } else { 386 rect_width 387 } 388 }; 389 390 let id = ui.id().with("default_zap_amount"); 391 ui.add( 392 egui::TextEdit::singleline(text) 393 .desired_width(desired_width) 394 .margin(egui::Margin::same(8)) 395 .font(font) 396 .id(id), 397 ); 398 399 ui.memory_mut(|m| m.request_focus(id)); 400 401 ui.label(tr!(i18n, "sats", "Unit label for satoshis (Bitcoin unit) for configuring default zap amount in wallet settings.")); 402 403 if ui 404 .add(styled_button(tr!(i18n, "Save", "Button to save default zap amount").as_str(), ui.visuals().widgets.active.bg_fill)) 405 .clicked() 406 { 407 action = Some(WalletAction::SetDefaultZapSats(text.to_string())); 408 } 409 } 410 DefaultZapState::Valid(msats) => { 411 if let Some(wallet_action) = show_valid_msats(ui, i18n, **msats) { 412 action = Some(wallet_action); 413 } 414 ui.label(tr!(i18n, "sats", "Unit label for satoshis (Bitcoin unit) for configuring default zap amount in wallet settings.")); 415 } 416 } 417 418 if let DefaultZapState::Pending(pending) = state { 419 if let Some(error_message) = &pending.error_message { 420 let msg_str = match error_message { 421 notedeck::DefaultZapError::InvalidUserInput => tr!(i18n, "Invalid amount", "Error message for invalid zap amount"), 422 }; 423 424 ui.colored_label(ui.visuals().warn_fg_color, msg_str); 425 } 426 } 427 }, 428 ); 429 430 action 431 } 432 433 fn show_valid_msats( 434 ui: &mut egui::Ui, 435 i18n: &mut Localization, 436 msats: u64, 437 ) -> Option<WalletAction> { 438 let galley = { 439 let painter = ui.painter(); 440 441 let sats_str = (msats / 1000).to_string(); 442 painter.layout_no_wrap( 443 sats_str, 444 NotedeckTextStyle::Body.get_font_id(ui.ctx()), 445 ui.visuals().text_color(), 446 ) 447 }; 448 449 let (rect, resp) = ui.allocate_exact_size(galley.rect.expand(8.0).size(), egui::Sense::click()); 450 451 let resp = resp 452 .on_hover_cursor(egui::CursorIcon::PointingHand) 453 .on_hover_text_at_pointer(tr!( 454 i18n, 455 "Click to edit", 456 "Hover text for editable zap amount" 457 )); 458 459 let painter = ui.painter_at(resp.rect); 460 461 painter.rect_filled( 462 rect, 463 CornerRadius::same(8), 464 ui.visuals().noninteractive().bg_fill, 465 ); 466 467 let galley_pos = { 468 let mut next_pos = rect.left_top(); 469 next_pos.x += 8.0; 470 next_pos.y += 8.0; 471 next_pos 472 }; 473 474 painter.galley(galley_pos, galley, notedeck_ui::colors::MID_GRAY); 475 476 if resp.clicked() { 477 Some(WalletAction::EditDefaultZaps) 478 } else { 479 None 480 } 481 }