wallet.rs (15417B)
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 let text_edit_resp = ui.add(text_edit); 241 input_context( 242 ui, 243 &text_edit_resp, 244 clipboard, 245 &mut state.buf, 246 PasteBehavior::Clear, 247 ); 248 249 let Some(error_msg) = &state.error_msg else { 250 return; 251 }; 252 253 let error_str = match error_msg { 254 WalletError::InvalidURI => tr!( 255 i18n, 256 "Invalid NWC URI", 257 "Error message for invalid Nostr Wallet Connect URI" 258 ), 259 WalletError::NoWallet => tr!( 260 i18n, 261 "Add a wallet to continue", 262 "Error message for missing wallet" 263 ), 264 }; 265 ui.colored_label(ui.visuals().warn_fg_color, error_str); 266 }); 267 268 ui.add_space(8.0); 269 270 if show_local_only { 271 ui.checkbox( 272 &mut state.for_local_only, 273 tr!( 274 i18n, 275 "Use this wallet for the current account only", 276 "Checkbox label for using wallet only for current account" 277 ), 278 ); 279 ui.add_space(8.0); 280 } 281 282 ui.with_layout(Layout::top_down(egui::Align::Center), |ui| { 283 ui.add(styled_button( 284 tr!(i18n, "Add Wallet", "Button label to add a wallet").as_str(), 285 notedeck_ui::colors::PINK, 286 )) 287 .clicked() 288 .then_some(WalletAction::SaveURI) 289 }) 290 .inner 291 } 292 293 fn show_with_wallet( 294 ui: &mut egui::Ui, 295 i18n: &mut Localization, 296 wallet: &mut Wallet, 297 default_zap_state: &mut DefaultZapState, 298 can_create_local_wallet: bool, 299 ) -> Option<WalletAction> { 300 ui.horizontal_wrapped(|ui| { 301 let balance = wallet.get_balance(); 302 303 if let Some(balance) = balance { 304 match balance { 305 Ok(msats) => show_balance(ui, *msats), 306 Err(e) => ui.colored_label(egui::Color32::RED, format!("error: {e}")), 307 } 308 } else { 309 ui.with_layout(Layout::top_down(egui::Align::Center), |ui| { 310 ui.add(egui::Spinner::new().size(48.0)) 311 }) 312 .inner 313 } 314 }); 315 316 let mut action = show_default_zap(ui, i18n, default_zap_state); 317 318 ui.with_layout(Layout::bottom_up(egui::Align::Min), |ui| 's: { 319 if ui 320 .add(styled_button( 321 tr!(i18n, "Delete Wallet", "Button label to delete a wallet").as_str(), 322 ui.visuals().window_fill, 323 )) 324 .clicked() 325 { 326 action = Some(WalletAction::Delete); 327 break 's; 328 } 329 330 ui.add_space(12.0); 331 if can_create_local_wallet 332 && ui 333 .checkbox( 334 &mut false, 335 tr!( 336 i18n, 337 "Add a different wallet that will only be used for this account", 338 "Button label to add a different wallet" 339 ), 340 ) 341 .clicked() 342 { 343 action = Some(WalletAction::AddLocalOnly); 344 } 345 }); 346 347 action 348 } 349 350 fn show_balance(ui: &mut egui::Ui, msats: u64) -> egui::Response { 351 let sats = human_format::Formatter::new() 352 .with_decimals(2) 353 .format(msats as f64 / 1000.0); 354 355 ui.with_layout(Layout::top_down(egui::Align::Center), |ui| { 356 ui.label(egui::RichText::new(format!("{sats} sats")).size(48.0)) 357 }) 358 .inner 359 } 360 361 fn show_default_zap( 362 ui: &mut egui::Ui, 363 i18n: &mut Localization, 364 state: &mut DefaultZapState, 365 ) -> Option<WalletAction> { 366 let mut action = None; 367 ui.allocate_ui_with_layout( 368 vec2(ui.available_width(), 50.0), 369 egui::Layout::left_to_right(egui::Align::Center).with_main_wrap(true), 370 |ui| { 371 ui.label(tr!(i18n, "Default amount per zap: ", "Label for default zap amount input")); 372 match state { 373 DefaultZapState::Pending(pending_default_zap_state) => { 374 let text = &mut pending_default_zap_state.amount_sats; 375 376 let font = NotedeckTextStyle::Body.get_font_id(ui.ctx()); 377 let desired_width = { 378 let painter = ui.painter(); 379 let galley = painter.layout_no_wrap( 380 text.clone(), 381 font.clone(), 382 ui.visuals().text_color(), 383 ); 384 let rect_width = galley.rect.width(); 385 if rect_width < 5.0 { 386 10.0 387 } else { 388 rect_width 389 } 390 }; 391 392 let id = ui.id().with("default_zap_amount"); 393 394 { 395 let r = ui.add( 396 egui::TextEdit::singleline(text) 397 .desired_width(desired_width) 398 .margin(egui::Margin::same(8)) 399 .font(font) 400 .id(id)); 401 402 notedeck_ui::include_input(ui, &r); 403 } 404 405 if !notedeck::ui::is_narrow(ui.ctx()) { // TODO: this should really be checking if we are using a virtual keyboard instead of narrow 406 ui.memory_mut(|m| m.request_focus(id)); 407 } 408 409 ui.label(tr!(i18n, "sats", "Unit label for satoshis (Bitcoin unit) for configuring default zap amount in wallet settings.")); 410 411 if ui 412 .add(styled_button(tr!(i18n, "Save", "Button to save default zap amount").as_str(), ui.visuals().widgets.active.bg_fill)) 413 .clicked() 414 { 415 action = Some(WalletAction::SetDefaultZapSats(text.to_string())); 416 } 417 } 418 DefaultZapState::Valid(msats) => { 419 if let Some(wallet_action) = show_valid_msats(ui, i18n, **msats) { 420 action = Some(wallet_action); 421 } 422 ui.label(tr!(i18n, "sats", "Unit label for satoshis (Bitcoin unit) for configuring default zap amount in wallet settings.")); 423 } 424 } 425 426 if let DefaultZapState::Pending(pending) = state { 427 if let Some(error_message) = &pending.error_message { 428 let msg_str = match error_message { 429 notedeck::DefaultZapError::InvalidUserInput => tr!(i18n, "Invalid amount", "Error message for invalid zap amount"), 430 }; 431 432 ui.colored_label(ui.visuals().warn_fg_color, msg_str); 433 } 434 } 435 }, 436 ); 437 438 action 439 } 440 441 fn show_valid_msats( 442 ui: &mut egui::Ui, 443 i18n: &mut Localization, 444 msats: u64, 445 ) -> Option<WalletAction> { 446 let galley = { 447 let painter = ui.painter(); 448 449 let sats_str = (msats / 1000).to_string(); 450 painter.layout_no_wrap( 451 sats_str, 452 NotedeckTextStyle::Body.get_font_id(ui.ctx()), 453 ui.visuals().text_color(), 454 ) 455 }; 456 457 let (rect, resp) = ui.allocate_exact_size(galley.rect.expand(8.0).size(), egui::Sense::click()); 458 459 let resp = resp 460 .on_hover_cursor(egui::CursorIcon::PointingHand) 461 .on_hover_text_at_pointer(tr!( 462 i18n, 463 "Click to edit", 464 "Hover text for editable zap amount" 465 )); 466 467 let painter = ui.painter_at(resp.rect); 468 469 painter.rect_filled( 470 rect, 471 CornerRadius::same(8), 472 ui.visuals().noninteractive().bg_fill, 473 ); 474 475 let galley_pos = { 476 let mut next_pos = rect.left_top(); 477 next_pos.x += 8.0; 478 next_pos.y += 8.0; 479 next_pos 480 }; 481 482 painter.galley(galley_pos, galley, notedeck_ui::colors::MID_GRAY); 483 484 if resp.clicked() { 485 Some(WalletAction::EditDefaultZaps) 486 } else { 487 None 488 } 489 }