From c76886792b75526bb87fb5440e0cddfef11d49f0 Mon Sep 17 00:00:00 2001 From: Suyog Tandel Date: Sat, 21 Feb 2026 23:15:05 +0530 Subject: [PATCH] feat(ui): Show success/error states in dialogs --- src/ui/components/dialog.rs | 716 +++++++++++++++++++++++++++++------ src/ui/components/sidebar.rs | 1 + src/ui/views/config.rs | 23 +- src/ui/views/logs.rs | 7 +- src/ui/views/passkeys.rs | 98 +++-- 5 files changed, 687 insertions(+), 158 deletions(-) diff --git a/src/ui/components/dialog.rs b/src/ui/components/dialog.rs index c590fc2..9d8975a 100644 --- a/src/ui/components/dialog.rs +++ b/src/ui/components/dialog.rs @@ -1,21 +1,191 @@ use gpui::*; use gpui_component::{ - WindowExt, + ActiveTheme, Disableable, Sizable, WindowExt, button::{Button, ButtonVariant, ButtonVariants}, - dialog::DialogButtonProps, + h_flex, input::{Input, InputState}, v_flex, }; +#[derive(Clone)] +enum DialogPhase { + Input, + Loading, + Success(String), + Error(String), +} + +pub struct PinPromptContent { + phase: DialogPhase, + title: SharedString, + description: SharedString, + confirm_label: SharedString, + pin_input: Entity, + on_confirm: std::rc::Rc, &mut App)>, +} + +impl PinPromptContent { + fn set_loading(&mut self, cx: &mut Context) { + self.phase = DialogPhase::Loading; + cx.notify(); + } + + pub fn set_success(&mut self, msg: String, cx: &mut Context) { + self.phase = DialogPhase::Success(msg); + cx.notify(); + } + + pub fn set_error(&mut self, msg: String, cx: &mut Context) { + self.phase = DialogPhase::Error(msg); + cx.notify(); + } +} + +impl Render for PinPromptContent { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let phase = self.phase.clone(); + + match &phase { + DialogPhase::Success(msg) => v_flex() + .gap_4() + .child( + h_flex() + .gap_2() + .items_center() + .child( + gpui_component::Icon::new(gpui_component::IconName::CircleCheck) + .text_color(cx.theme().green) + .with_size(gpui_component::Size::Large), + ) + .child(self.title.clone()), + ) + .child(msg.clone()) + .child( + h_flex().justify_end().child( + Button::new("done") + .primary() + .label("Done") + .on_click(|_, window, cx| { + window.close_dialog(cx); + }), + ), + ) + .into_any_element(), + + DialogPhase::Loading => v_flex() + .gap_4() + .child(self.description.clone()) + .child(Input::new(&self.pin_input).disabled(true)) + .child( + h_flex() + .justify_end() + .gap_2() + .child(Button::new("cancel").label("Cancel").disabled(true)) + .child( + Button::new("confirm") + .primary() + .label("Loading...") + .loading(true), + ), + ) + .into_any_element(), + + DialogPhase::Error(err_msg) => { + let pin_input = self.pin_input.clone(); + let confirm_label = self.confirm_label.clone(); + let on_confirm = self.on_confirm.clone(); + let handle = cx.entity().downgrade(); + + v_flex() + .gap_4() + .child(self.description.clone()) + .child( + div() + .px_3() + .py_2() + .rounded_md() + .bg(cx.theme().danger.opacity(0.1)) + .text_color(cx.theme().danger) + .text_sm() + .child(err_msg.clone()), + ) + .child(Input::new(&pin_input)) + .child( + h_flex() + .justify_end() + .gap_2() + .child(Button::new("cancel").label("Cancel").on_click( + |_, window, cx| { + window.close_dialog(cx); + }, + )) + .child( + Button::new("confirm") + .primary() + .label(confirm_label) + .on_click(move |_, _, cx| { + let pin = pin_input.read(cx).text().to_string(); + if !pin.is_empty() { + if let Some(h) = handle.upgrade() { + h.update(cx, |this, cx| this.set_loading(cx)); + } + on_confirm(pin, handle.clone(), cx); + } + }), + ), + ) + .into_any_element() + } + + DialogPhase::Input => { + let pin_input = self.pin_input.clone(); + let confirm_label = self.confirm_label.clone(); + let on_confirm = self.on_confirm.clone(); + let handle = cx.entity().downgrade(); + + v_flex() + .gap_4() + .child(self.description.clone()) + .child(Input::new(&pin_input)) + .child( + h_flex() + .justify_end() + .gap_2() + .child(Button::new("cancel").label("Cancel").on_click( + |_, window, cx| { + window.close_dialog(cx); + }, + )) + .child( + Button::new("confirm") + .primary() + .label(confirm_label) + .on_click(move |_, _, cx| { + let pin = pin_input.read(cx).text().to_string(); + if !pin.is_empty() { + if let Some(h) = handle.upgrade() { + h.update(cx, |this, cx| this.set_loading(cx)); + } + on_confirm(pin, handle.clone(), cx); + } + }), + ), + ) + .into_any_element() + } + } + } +} + pub fn open_pin_prompt( title: &str, description: &str, confirm_label: &str, window: &mut Window, cx: &mut App, - on_confirm: impl Fn(String, &mut Window, &mut App) + 'static, + on_confirm: impl Fn(String, WeakEntity, &mut App) + 'static, ) { - let title = SharedString::from(title.to_string()); + let title_str = SharedString::from(title.to_string()); let description = SharedString::from(description.to_string()); let confirm_label = SharedString::from(confirm_label.to_string()); @@ -25,45 +195,177 @@ pub fn open_pin_prompt( .masked(true) }); - let on_confirm = std::rc::Rc::new(on_confirm); + let dialog_title = title_str.clone(); + + let content = cx.new(|_cx| PinPromptContent { + phase: DialogPhase::Input, + title: title_str, + description, + confirm_label, + pin_input, + on_confirm: std::rc::Rc::new(on_confirm), + }); window.open_dialog(cx, move |dialog, _, _| { - let pin_input_for_footer = pin_input.clone(); - let confirm_label = confirm_label.clone(); - let on_confirm = on_confirm.clone(); - dialog - .title(title.clone()) - .child( + .title(dialog_title.clone()) + .child(content.clone()) + .overlay_closable(false) + .close_button(false) + }); +} + +pub struct ConfirmContent { + phase: DialogPhase, + title: SharedString, + message: String, + ok_label: SharedString, + ok_variant: ButtonVariant, + on_ok: std::rc::Rc, &mut App)>, +} + +impl ConfirmContent { + fn set_loading(&mut self, cx: &mut Context) { + self.phase = DialogPhase::Loading; + cx.notify(); + } + + pub fn set_success(&mut self, msg: String, cx: &mut Context) { + self.phase = DialogPhase::Success(msg); + cx.notify(); + } + + pub fn set_error(&mut self, msg: String, cx: &mut Context) { + self.phase = DialogPhase::Error(msg); + cx.notify(); + } +} + +impl Render for ConfirmContent { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let phase = self.phase.clone(); + + match &phase { + DialogPhase::Success(msg) => v_flex() + .gap_4() + .child( + h_flex() + .gap_2() + .items_center() + .child( + gpui_component::Icon::new(gpui_component::IconName::CircleCheck) + .text_color(cx.theme().green) + .with_size(gpui_component::Size::Large), + ) + .child(self.title.clone()), + ) + .child(msg.clone()) + .child( + h_flex().justify_end().child( + Button::new("done") + .primary() + .label("Done") + .on_click(|_, window, cx| { + window.close_dialog(cx); + }), + ), + ) + .into_any_element(), + + DialogPhase::Loading => v_flex() + .gap_4() + .child(self.message.clone()) + .child( + h_flex() + .justify_end() + .gap_2() + .child(Button::new("cancel").label("Cancel").disabled(true)) + .child( + Button::new("ok") + .with_variant(self.ok_variant) + .label("Loading...") + .loading(true), + ), + ) + .into_any_element(), + + DialogPhase::Error(err_msg) => { + let ok_label = self.ok_label.clone(); + let ok_variant = self.ok_variant; + let on_ok = self.on_ok.clone(); + let handle = cx.entity().downgrade(); + v_flex() .gap_4() - .pb_4() - .child(description.clone()) - .child(Input::new(&pin_input)), - ) - .footer(move |_, _, _, _| { - let input = pin_input_for_footer.clone(); - let on_confirm = on_confirm.clone(); + .child(self.message.clone()) + .child( + div() + .px_3() + .py_2() + .rounded_md() + .bg(cx.theme().danger.opacity(0.1)) + .text_color(cx.theme().danger) + .text_sm() + .child(err_msg.clone()), + ) + .child( + h_flex() + .justify_end() + .gap_2() + .child(Button::new("cancel").label("Cancel").on_click( + |_, window, cx| { + window.close_dialog(cx); + }, + )) + .child( + Button::new("ok") + .with_variant(ok_variant) + .label(ok_label) + .on_click(move |_, _, cx| { + if let Some(h) = handle.upgrade() { + h.update(cx, |this, cx| this.set_loading(cx)); + } + on_ok(handle.clone(), cx); + }), + ), + ) + .into_any_element() + } - vec![ - Button::new("cancel") - .label("Cancel") - .on_click(|_, window, cx| { - window.close_dialog(cx); - }), - Button::new("confirm") - .primary() - .label(confirm_label.clone()) - .on_click(move |_, window, cx| { - let pin = input.read(cx).text().to_string(); - if !pin.is_empty() { - window.close_dialog(cx); - on_confirm(pin, window, cx); - } - }), - ] - }) - }); + DialogPhase::Input => { + let ok_label = self.ok_label.clone(); + let ok_variant = self.ok_variant; + let on_ok = self.on_ok.clone(); + let handle = cx.entity().downgrade(); + + v_flex() + .gap_4() + .child(self.message.clone()) + .child( + h_flex() + .justify_end() + .gap_2() + .child(Button::new("cancel").label("Cancel").on_click( + |_, window, cx| { + window.close_dialog(cx); + }, + )) + .child( + Button::new("ok") + .with_variant(ok_variant) + .label(ok_label) + .on_click(move |_, _, cx| { + if let Some(h) = handle.upgrade() { + h.update(cx, |this, cx| this.set_loading(cx)); + } + on_ok(handle.clone(), cx); + }), + ), + ) + .into_any_element() + } + } + } } pub fn open_confirm( @@ -73,37 +375,269 @@ pub fn open_confirm( ok_variant: ButtonVariant, window: &mut Window, cx: &mut App, - on_ok: impl Fn(&mut Window, &mut App) + 'static, + on_ok: impl Fn(WeakEntity, &mut App) + 'static, ) { - let title = SharedString::from(title.to_string()); - let ok_label = SharedString::from(ok_label.to_string()); - let on_ok = std::rc::Rc::new(on_ok); + let title_str = SharedString::from(title.to_string()); + let dialog_title = title_str.clone(); + + let content = cx.new(|_cx| ConfirmContent { + phase: DialogPhase::Input, + title: title_str, + message, + ok_label: SharedString::from(ok_label.to_string()), + ok_variant, + on_ok: std::rc::Rc::new(on_ok), + }); window.open_dialog(cx, move |dialog, _, _| { - let on_ok = on_ok.clone(); - dialog - .confirm() - .title(title.clone()) - .child(div().pb_4().child(message.clone())) - .on_ok(move |_, window, cx| { - on_ok(window, cx); - false - }) - .on_cancel(|_, _, _| true) - .button_props( - DialogButtonProps::default() - .ok_text(ok_label.clone()) - .ok_variant(ok_variant), - ) + .title(dialog_title.clone()) + .child(content.clone()) + .overlay_closable(false) + .close_button(false) }); } +pub struct ChangePinContent { + phase: DialogPhase, + current_pin: Entity, + new_pin: Entity, + confirm_pin: Entity, + on_confirm: std::rc::Rc, &mut App)>, +} + +impl ChangePinContent { + fn set_loading(&mut self, cx: &mut Context) { + self.phase = DialogPhase::Loading; + cx.notify(); + } + + pub fn set_success(&mut self, msg: String, cx: &mut Context) { + self.phase = DialogPhase::Success(msg); + cx.notify(); + } + + pub fn set_error(&mut self, msg: String, cx: &mut Context) { + self.phase = DialogPhase::Error(msg); + cx.notify(); + } +} + +impl Render for ChangePinContent { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let phase = self.phase.clone(); + + match &phase { + DialogPhase::Success(msg) => v_flex() + .gap_4() + .child( + h_flex() + .gap_2() + .items_center() + .child( + gpui_component::Icon::new(gpui_component::IconName::CircleCheck) + .text_color(cx.theme().green) + .with_size(gpui_component::Size::Large), + ) + .child("Change PIN"), + ) + .child(msg.clone()) + .child( + h_flex().justify_end().child( + Button::new("done") + .primary() + .label("Done") + .on_click(|_, window, cx| { + window.close_dialog(cx); + }), + ), + ) + .into_any_element(), + + DialogPhase::Loading => v_flex() + .gap_4() + .child("Enter your current PIN and choose a new one.") + .child( + v_flex() + .gap_4() + .child("Current PIN") + .child(Input::new(&self.current_pin).disabled(true)) + .child("New PIN") + .child(Input::new(&self.new_pin).disabled(true)) + .child("Confirm New PIN") + .child(Input::new(&self.confirm_pin).disabled(true)), + ) + .child( + h_flex() + .justify_end() + .gap_2() + .child(Button::new("cancel").label("Cancel").disabled(true)) + .child( + Button::new("confirm") + .primary() + .label("Changing PIN...") + .loading(true), + ), + ) + .into_any_element(), + + DialogPhase::Error(err_msg) => { + let current = self.current_pin.clone(); + let new = self.new_pin.clone(); + let confirm = self.confirm_pin.clone(); + let on_confirm = self.on_confirm.clone(); + let handle = cx.entity().downgrade(); + + v_flex() + .gap_4() + .child("Enter your current PIN and choose a new one.") + .child( + div() + .px_3() + .py_2() + .rounded_md() + .bg(cx.theme().danger.opacity(0.1)) + .text_color(cx.theme().danger) + .text_sm() + .child(err_msg.clone()), + ) + .child( + v_flex() + .gap_4() + .child("Current PIN") + .child(Input::new(¤t)) + .child("New PIN") + .child(Input::new(&new)) + .child("Confirm New PIN") + .child(Input::new(&confirm)), + ) + .child( + h_flex() + .justify_end() + .gap_2() + .child( + Button::new("cancel") + .label("Cancel") + .on_click(|_, window, cx| window.close_dialog(cx)), + ) + .child(Button::new("confirm").primary().label("Confirm").on_click( + move |_, _, cx| { + let current_val = current.read(cx).text().to_string(); + let new_val = new.read(cx).text().to_string(); + let confirm_val = confirm.read(cx).text().to_string(); + + if current_val.is_empty() { + return; + } + + if new_val != confirm_val { + if let Some(h) = handle.upgrade() { + h.update(cx, |this, cx| { + this.set_error("PINs do not match".to_string(), cx); + }); + } + return; + } + + if new_val.len() < 4 { + if let Some(h) = handle.upgrade() { + h.update(cx, |this, cx| { + this.set_error( + "PIN must be at least 4 characters".to_string(), + cx, + ); + }); + } + return; + } + + if let Some(h) = handle.upgrade() { + h.update(cx, |this, cx| this.set_loading(cx)); + } + on_confirm(current_val, new_val, handle.clone(), cx); + }, + )), + ) + .into_any_element() + } + + DialogPhase::Input => { + let current = self.current_pin.clone(); + let new = self.new_pin.clone(); + let confirm = self.confirm_pin.clone(); + let on_confirm = self.on_confirm.clone(); + let handle = cx.entity().downgrade(); + + v_flex() + .gap_4() + .child("Enter your current PIN and choose a new one.") + .child( + v_flex() + .gap_4() + .child("Current PIN") + .child(Input::new(¤t)) + .child("New PIN") + .child(Input::new(&new)) + .child("Confirm New PIN") + .child(Input::new(&confirm)), + ) + .child( + h_flex() + .justify_end() + .gap_2() + .child( + Button::new("cancel") + .label("Cancel") + .on_click(|_, window, cx| window.close_dialog(cx)), + ) + .child(Button::new("confirm").primary().label("Confirm").on_click( + move |_, _, cx| { + let current_val = current.read(cx).text().to_string(); + let new_val = new.read(cx).text().to_string(); + let confirm_val = confirm.read(cx).text().to_string(); + + if current_val.is_empty() { + return; + } + + if new_val != confirm_val { + if let Some(h) = handle.upgrade() { + h.update(cx, |this, cx| { + this.set_error("PINs do not match".to_string(), cx); + }); + } + return; + } + + if new_val.len() < 4 { + if let Some(h) = handle.upgrade() { + h.update(cx, |this, cx| { + this.set_error( + "PIN must be at least 4 characters".to_string(), + cx, + ); + }); + } + return; + } + + if let Some(h) = handle.upgrade() { + h.update(cx, |this, cx| this.set_loading(cx)); + } + on_confirm(current_val, new_val, handle.clone(), cx); + }, + )), + ) + .into_any_element() + } + } + } +} + pub fn open_change_pin( window: &mut Window, cx: &mut App, - on_error: impl Fn(&str, &mut App) + 'static + Clone, - on_confirm: impl Fn(String, String, &mut App) + 'static, + on_confirm: impl Fn(String, String, WeakEntity, &mut App) + 'static, ) { let current_pin = cx.new(|cx| { InputState::new(window, cx) @@ -121,65 +655,19 @@ pub fn open_change_pin( .masked(true) }); - let on_confirm = std::rc::Rc::new(on_confirm); + let content = cx.new(|_cx| ChangePinContent { + phase: DialogPhase::Input, + current_pin, + new_pin, + confirm_pin, + on_confirm: std::rc::Rc::new(on_confirm), + }); window.open_dialog(cx, move |dialog, _, _| { - let current = current_pin.clone(); - let new = new_pin.clone(); - let confirm = confirm_pin.clone(); - let on_error = on_error.clone(); - let on_confirm = on_confirm.clone(); - dialog .title("Change PIN") - .child("Enter your current PIN and choose a new one.") - .child( - v_flex() - .gap_4() - .pb_4() - .child("Current PIN") - .child(Input::new(¤t)) - .child("New PIN") - .child(Input::new(&new)) - .child("Confirm New PIN") - .child(Input::new(&confirm)), - ) - .footer(move |_, _window, _cx, _| { - let current = current.clone(); - let new = new.clone(); - let confirm = confirm.clone(); - let on_error = on_error.clone(); - let on_confirm = on_confirm.clone(); - - vec![ - Button::new("cancel") - .label("Cancel") - .on_click(|_, window, cx| window.close_dialog(cx)), - Button::new("confirm") - .primary() - .label("Confirm") - .on_click(move |_, _, cx| { - let current_val = current.read(cx).text().to_string(); - let new_val = new.read(cx).text().to_string(); - let confirm_val = confirm.read(cx).text().to_string(); - - if current_val.is_empty() { - return; - } - - if new_val != confirm_val { - on_error("PINs do not match", cx); - return; - } - - if new_val.len() < 4 { - on_error("PIN too short", cx); - return; - } - - on_confirm(current_val, new_val, cx); - }), - ] - }) + .child(content.clone()) + .overlay_closable(false) + .close_button(false) }); } diff --git a/src/ui/components/sidebar.rs b/src/ui/components/sidebar.rs index 5a96d6e..f4b780f 100644 --- a/src/ui/components/sidebar.rs +++ b/src/ui/components/sidebar.rs @@ -176,6 +176,7 @@ impl AppSidebar { .w_full() .bg(rgb(0x111113)) // .border_r_1() + .mt_1() .border_t_1() .border_color(border_color) .p_2() diff --git a/src/ui/views/config.rs b/src/ui/views/config.rs index 1119aed..3ad46df 100644 --- a/src/ui/views/config.rs +++ b/src/ui/views/config.rs @@ -1,6 +1,6 @@ use crate::device::io; use crate::device::types::{AppConfigInput, FullDeviceStatus}; -use crate::ui::components::{card::Card, dialog, page_view::PageView}; +use crate::ui::components::{card::Card, dialog, dialog::PinPromptContent, page_view::PageView}; use crate::ui::types::{LedDriverType, UsbIdentityPreset}; use gpui::*; use gpui_component::button::{ButtonCustomVariant, ButtonVariants}; @@ -211,6 +211,7 @@ impl ConfigView { changes: AppConfigInput, method: crate::device::types::DeviceMethod, pin: Option, + dialog_handle: Option>, cx: &mut Context, ) { self.loading = true; @@ -255,11 +256,24 @@ impl ConfigView { this.enable_secp256k1 = config.enable_secp256k1; this.device_status = Some(new_status); - cx.notify(); + } + + if let Some(ref dh) = dialog_handle { + let _ = dh.update(cx, |d, cx| { + d.set_success( + "Configuration applied successfully.".to_string(), + cx, + ); + }); } } Err(e) => { log::error!("Error saving config: {}", e); + if let Some(ref dh) = dialog_handle { + let _ = dh.update(cx, |d, cx| { + d.set_error(format!("Failed to apply: {}", e), cx); + }); + } } } @@ -282,12 +296,13 @@ impl ConfigView { "Confirm", window, cx, - move |pin, _, cx| { + move |pin, dialog_handle, cx| { let _ = view_handle.update(cx, |this, cx| { this.write_config_to_device( changes.clone(), crate::device::types::DeviceMethod::Fido, Some(pin), + Some(dialog_handle), cx, ); }); @@ -396,7 +411,7 @@ impl ConfigView { if method == crate::device::types::DeviceMethod::Fido { self.open_pin_dialog(changes, window, cx); } else { - self.write_config_to_device(changes, method, None, cx); + self.write_config_to_device(changes, method, None, None, cx); } } diff --git a/src/ui/views/logs.rs b/src/ui/views/logs.rs index 4f846bc..e0bb153 100644 --- a/src/ui/views/logs.rs +++ b/src/ui/views/logs.rs @@ -64,7 +64,7 @@ impl Render for LogsView { let copy_logs_listener = cx.listener(|this, _, _, cx| { let all_logs = this.logs.join("\n"); - log::info!("Copying {} bytes of logs", all_logs.len()); + log::debug!("Copying {} bytes of logs", all_logs.len()); cx.write_to_clipboard(ClipboardItem::new_string(all_logs)); }); @@ -128,10 +128,7 @@ impl Render for LogsView { theme.foreground.to_rgb() }; - div() - .text_color(color) - .child(log.clone()) - .cursor_text() + div().text_color(color).child(log.clone()) }), )), ) diff --git a/src/ui/views/passkeys.rs b/src/ui/views/passkeys.rs index d80dbd8..055992f 100644 --- a/src/ui/views/passkeys.rs +++ b/src/ui/views/passkeys.rs @@ -4,6 +4,7 @@ use crate::ui::components::{ button::{PFButton, PFIconButton}, card::Card, dialog, + dialog::{ChangePinContent, ConfirmContent, PinPromptContent}, page_view::PageView, }; use gpui::*; @@ -78,13 +79,19 @@ impl PasskeysView { cx.notify(); } - fn unlock_storage(&mut self, pin: String, cx: &mut Context) { + fn unlock_storage( + &mut self, + pin: String, + dialog_handle: WeakEntity, + cx: &mut Context, + ) { if self.loading { return; } self.loading = true; cx.notify(); + log::info!("Unlocking FIDO storage..."); let entity = cx.entity().downgrade(); self._task = Some(cx.spawn(async move |_, cx| { @@ -98,14 +105,19 @@ impl PasskeysView { this.loading = false; match result { Ok(creds) => { + log::info!("Storage unlocked. {} credentials found.", creds.len()); this.unlocked = true; this.cached_pin = Some(pin); this.credentials = creds; - cx.emit(PasskeysEvent::CloseDialog); + let _ = dialog_handle.update(cx, |d, cx| { + d.set_success("Storage unlocked successfully.".to_string(), cx); + }); } Err(e) => { - let msg = format!("Failed to unlock: {}", e); - cx.emit(PasskeysEvent::Notification(msg)); + log::error!("Failed to unlock storage: {}", e); + let _ = dialog_handle.update(cx, |d, cx| { + d.set_error(format!("Failed to unlock: {}", e), cx); + }); } } cx.notify(); @@ -120,13 +132,20 @@ impl PasskeysView { cx.notify(); } - fn execute_delete(&mut self, credential_id: String, pin: String, cx: &mut Context) { + fn execute_delete( + &mut self, + credential_id: String, + pin: String, + dialog_handle: WeakEntity, + cx: &mut Context, + ) { if self.loading { return; } self.loading = true; cx.notify(); + log::info!("Deleting credential..."); let entity = cx.entity().downgrade(); self._task = Some(cx.spawn(async move |_, cx| { @@ -138,16 +157,18 @@ impl PasskeysView { let _ = entity.update(cx, |this, cx| match result { Ok(_) => { + log::info!("Credential deleted successfully."); this.refresh_credentials(pin, cx); - cx.emit(PasskeysEvent::CloseDialog); - cx.emit(PasskeysEvent::Notification( - "Credential deleted".to_string(), - )); + let _ = dialog_handle.update(cx, |d, cx| { + d.set_success("Credential deleted successfully.".to_string(), cx); + }); } Err(e) => { + log::error!("Error deleting credential: {}", e); this.loading = false; - let msg = format!("Error deleting: {}", e); - cx.emit(PasskeysEvent::Notification(msg)); + let _ = dialog_handle.update(cx, |d, cx| { + d.set_error(format!("Error deleting: {}", e), cx); + }); cx.notify(); } }); @@ -181,9 +202,9 @@ impl PasskeysView { "Unlock", window, cx, - move |pin, _, cx| { + move |pin, dialog_handle, cx| { let _ = view_handle.update(cx, |this, cx| { - this.unlock_storage(pin, cx); + this.unlock_storage(pin, dialog_handle, cx); }); }, ); @@ -208,9 +229,9 @@ impl PasskeysView { ButtonVariant::Danger, window, cx, - move |_, cx| { + move |dialog_handle, cx| { let _ = view_handle.update(cx, |this, cx| { - this.execute_delete(cred_id.clone(), pin_str.clone(), cx); + this.execute_delete(cred_id.clone(), pin_str.clone(), dialog_handle, cx); }); }, ); @@ -218,22 +239,12 @@ impl PasskeysView { fn open_change_pin_dialog(&mut self, window: &mut Window, cx: &mut Context) { let view_handle = cx.entity().downgrade(); - let view_for_error = cx.entity().downgrade(); - dialog::open_change_pin( - window, - cx, - move |msg, cx| { - let _ = view_for_error.update(cx, |_, cx| { - cx.emit(PasskeysEvent::Notification(msg.to_string())); - }); - }, - move |current, new, cx| { - let _ = view_handle.update(cx, |this, cx| { - this.change_pin(current, new, cx); - }); - }, - ); + dialog::open_change_pin(window, cx, move |current, new, dialog_handle, cx| { + let _ = view_handle.update(cx, |this, cx| { + this.change_pin(current, new, dialog_handle, cx); + }); + }); } fn open_min_pin_length_dialog(&mut self, window: &mut Window, cx: &mut Context) { @@ -354,12 +365,20 @@ impl PasskeysView { }); } - fn change_pin(&mut self, current: String, new: String, cx: &mut Context) { + fn change_pin( + &mut self, + current: String, + new: String, + dialog_handle: WeakEntity, + cx: &mut Context, + ) { if self.loading { return; } self.loading = true; cx.notify(); + + log::info!("Changing FIDO PIN..."); let entity = cx.entity().downgrade(); self._task = Some(cx.spawn(async move |_, cx| { @@ -372,15 +391,19 @@ impl PasskeysView { this.loading = false; match result { Ok(msg) => { - cx.emit(PasskeysEvent::CloseDialog); - cx.emit(PasskeysEvent::Notification(msg)); - // Refresh device info + log::info!("PIN changed: {}", msg); if let Ok(info) = io::get_fido_info() { this.fido_info = Some(info); } + let _ = dialog_handle.update(cx, |d, cx| { + d.set_success("PIN changed successfully.".to_string(), cx); + }); } Err(e) => { - cx.emit(PasskeysEvent::Notification(format!("Error: {}", e))); + log::error!("PIN change failed: {}", e); + let _ = dialog_handle.update(cx, |d, cx| { + d.set_error(format!("Error: {}", e), cx); + }); } } cx.notify(); @@ -400,6 +423,7 @@ impl PasskeysView { } self.loading = true; cx.notify(); + log::info!("Updating minimum PIN length to {}...", min_len); let entity = cx.entity().downgrade(); self._task = Some(cx.spawn(async move |_, cx| { @@ -411,6 +435,7 @@ impl PasskeysView { .await; if let Err(e) = res_len { + log::error!("Failed to set minimum PIN length: {}", e); let _ = entity.update(cx, |this, cx| { this.loading = false; cx.emit(PasskeysEvent::Notification(format!( @@ -431,6 +456,7 @@ impl PasskeysView { this.loading = false; match res_pin { Ok(_) => { + log::info!("Minimum length and PIN updated successfully."); cx.emit(PasskeysEvent::CloseDialog); cx.emit(PasskeysEvent::Notification( "Minimum length and PIN updated".to_string(), @@ -440,6 +466,7 @@ impl PasskeysView { } } Err(e) => { + log::error!("Length set, but PIN change failed: {}", e); cx.emit(PasskeysEvent::Notification(format!( "Length set, but PIN change failed: {}", e @@ -451,6 +478,7 @@ impl PasskeysView { } else { let _ = entity.update(cx, |this, cx| { this.loading = false; + log::info!("Minimum PIN length updated to {}.", min_len); cx.emit(PasskeysEvent::CloseDialog); cx.emit(PasskeysEvent::Notification(format!( "Minimum length updated to {}",