From b720285c1a79e07d8adc8d8762a4934ccfc44151 Mon Sep 17 00:00:00 2001 From: kralonur Date: Thu, 25 Jun 2026 02:54:58 +0300 Subject: [PATCH 1/2] fix(ui): sync cached FIDO state after mutations --- src/ui/rootview.rs | 6 ++ src/ui/views/passkeys.rs | 212 ++++++++++++++++++++------------------- 2 files changed, 114 insertions(+), 104 deletions(-) diff --git a/src/ui/rootview.rs b/src/ui/rootview.rs index dd182f3..97f232f 100644 --- a/src/ui/rootview.rs +++ b/src/ui/rootview.rs @@ -60,6 +60,10 @@ impl ApplicationRoot { if device_changed { self.views.passkeys = None; + } else if let Some(passkeys_view) = &self.views.passkeys { + passkeys_view.update(cx, |view, cx| { + view.refresh_if_unlocked(cx); + }); } match io::get_fido_info() { @@ -97,6 +101,8 @@ impl ApplicationRoot { self.device.fido_info = None; self.device.led_status = None; self.device.management_apps = None; + // Drop cached passkeys view (and cached PIN) on disconnect. + self.views.passkeys = None; } } self.device.loading = false; diff --git a/src/ui/views/passkeys.rs b/src/ui/views/passkeys.rs index 2e2cd30..a6687f3 100644 --- a/src/ui/views/passkeys.rs +++ b/src/ui/views/passkeys.rs @@ -142,19 +142,18 @@ impl PasskeysView { let entity = cx.entity().downgrade(); self._task = Some(cx.spawn(async move |_, cx| { - let pin_for_bg = pin.clone(); let result = cx .background_executor() - .spawn(async move { io::delete_credential(pin_for_bg, credential_id) }) + .spawn(async move { io::delete_credential(pin, credential_id) }) .await; let _ = entity.update(cx, |this, cx| match result { Ok(_) => { log::info!("Credential deleted successfully."); - this.refresh_credentials(pin, cx); let _ = dialog_handle.update(cx, |d, cx| { d.set_success("Credential deleted successfully.".to_string(), cx); }); + this.sync_fido_state(None, cx); } Err(e) => { log::error!("Error deleting credential: {}", e); @@ -186,6 +185,46 @@ impl PasskeysView { })); } + /// Re-fetch credentials using the cached PIN (used by sidebar refresh). + pub fn refresh_if_unlocked(&mut self, cx: &mut Context) { + if !self.unlocked || self.loading { + return; + } + let Some(pin) = self.cached_pin.clone() else { + return; + }; + self.loading = true; + cx.notify(); + self.refresh_credentials(pin, cx); + } + + /// Re-sync all cached FIDO state after a mutation. + /// + /// Refreshes `fido_info`, optionally replaces `cached_pin`, and re-pulls + /// credentials when unlocked. `loading` is cleared on completion. + fn sync_fido_state(&mut self, new_pin: Option, cx: &mut Context) { + if let Ok(info) = io::get_fido_info() { + let _ = self.root.update(cx, |root, cx| { + root.device.fido_info = Some(info); + cx.notify(); + }); + } + + // Only update cached_pin when the caller changed it. + if let Some(pin) = new_pin { + self.cached_pin = Some(pin); + } + + if self.unlocked + && let Some(pin) = self.cached_pin.clone() + { + self.refresh_credentials(pin, cx); + return; + } + self.loading = false; + cx.notify(); + } + fn open_unlock_dialog(&mut self, window: &mut Window, cx: &mut Context) { let view_handle = cx.entity().downgrade(); @@ -272,29 +311,22 @@ impl PasskeysView { .spawn(async move { io::change_fido_pin(None, new) }) .await; - let _ = entity.update(cx, |this, cx| { - this.loading = false; - match result { - Ok(msg) => { - log::info!("PIN configured: {}", msg); - if let Ok(info) = io::get_fido_info() { - let _ = this.root.update(cx, |root, cx| { - root.device.fido_info = Some(info); - cx.notify(); - }); - } - let _ = dialog_handle.update(cx, |d, cx| { - d.set_success("PIN configured successfully.".to_string(), cx); - }); - } - Err(e) => { - log::error!("PIN setup failed: {}", e); - let _ = dialog_handle.update(cx, |d, cx| { - d.set_error(format!("Error: {}", e), cx); - }); - } + let _ = entity.update(cx, |this, cx| match result { + Ok(msg) => { + log::info!("PIN configured: {}", msg); + let _ = dialog_handle.update(cx, |d, cx| { + d.set_success("PIN configured successfully.".to_string(), cx); + }); + this.sync_fido_state(None, cx); + } + Err(e) => { + log::error!("PIN setup failed: {}", e); + this.loading = false; + let _ = dialog_handle.update(cx, |d, cx| { + d.set_error(format!("Error: {}", e), cx); + }); + cx.notify(); } - cx.notify(); }); })); } @@ -457,6 +489,7 @@ impl PasskeysView { log::info!("Changing FIDO PIN..."); let entity = cx.entity().downgrade(); + let new_for_sync = new.clone(); self._task = Some(cx.spawn(async move |_, cx| { let result = cx @@ -464,29 +497,22 @@ impl PasskeysView { .spawn(async move { io::change_fido_pin(Some(current), new) }) .await; - let _ = entity.update(cx, |this, cx| { - this.loading = false; - match result { - Ok(msg) => { - log::info!("PIN changed: {}", msg); - if let Ok(info) = io::get_fido_info() { - let _ = this.root.update(cx, |root, cx| { - root.device.fido_info = Some(info); - cx.notify(); - }); - } - let _ = dialog_handle.update(cx, |d, cx| { - d.set_success("PIN changed successfully.".to_string(), cx); - }); - } - Err(e) => { - log::error!("PIN change failed: {}", e); - let _ = dialog_handle.update(cx, |d, cx| { - d.set_error(format!("Error: {}", e), cx); - }); - } + let _ = entity.update(cx, |this, cx| match result { + Ok(msg) => { + log::info!("PIN changed: {}", msg); + let _ = dialog_handle.update(cx, |d, cx| { + d.set_success("PIN changed successfully.".to_string(), cx); + }); + this.sync_fido_state(Some(new_for_sync), cx); + } + Err(e) => { + log::error!("PIN change failed: {}", e); + this.loading = false; + let _ = dialog_handle.update(cx, |d, cx| { + d.set_error(format!("Error: {}", e), cx); + }); + cx.notify(); } - cx.notify(); }); })); } @@ -528,51 +554,35 @@ impl PasskeysView { } if !new_pin.is_empty() { + let new_pin_for_sync = new_pin.clone(); let res_pin = cx .background_executor() .spawn(async move { io::change_fido_pin(Some(current), new_pin) }) .await; - let _ = entity.update(cx, |this, cx| { - this.loading = false; - match res_pin { - Ok(_) => { - log::info!("Minimum length and PIN updated successfully."); - if let Ok(info) = io::get_fido_info() { - let _ = this.root.update(cx, |root, cx| { - root.device.fido_info = Some(info); - cx.notify(); - }); - } - let _ = status_handle.update(cx, |s, cx| { - s.set_success("Minimum length and PIN updated.".to_string(), cx); - }); - } - Err(e) => { - log::error!("Length set, but PIN change failed: {}", e); - let _ = status_handle.update(cx, |s, cx| { - s.set_error( - format!("Length set, but PIN change failed: {}", e), - cx, - ); - }); - } + let _ = entity.update(cx, |this, cx| match res_pin { + Ok(_) => { + log::info!("Minimum length and PIN updated successfully."); + let _ = status_handle.update(cx, |s, cx| { + s.set_success("Minimum length and PIN updated.".to_string(), cx); + }); + this.sync_fido_state(Some(new_pin_for_sync), cx); + } + Err(e) => { + log::error!("Length set, but PIN change failed: {}", e); + this.loading = false; + let _ = status_handle.update(cx, |s, cx| { + s.set_error(format!("Length set, but PIN change failed: {}", e), cx); + }); + cx.notify(); } - cx.notify(); }); } else { let _ = entity.update(cx, |this, cx| { - this.loading = false; log::info!("Minimum PIN length updated to {}.", min_len); - if let Ok(info) = io::get_fido_info() { - let _ = this.root.update(cx, |root, cx| { - root.device.fido_info = Some(info); - cx.notify(); - }); - } let _ = status_handle.update(cx, |s, cx| { s.set_success(format!("Minimum length updated to {}.", min_len), cx); }); - cx.notify(); + this.sync_fido_state(None, cx); }); } })); @@ -706,29 +716,22 @@ impl PasskeysView { .spawn(async move { io::enable_enterprise_attestation(pin) }) .await; - let _ = entity.update(cx, |this, cx| { - this.loading = false; - match result { - Ok(msg) => { - log::info!("{}", msg); - if let Ok(info) = io::get_fido_info() { - let _ = this.root.update(cx, |root, cx| { - root.device.fido_info = Some(info); - cx.notify(); - }); - } - let _ = dialog_handle.update(cx, |d, cx| { - d.set_success(msg, cx); - }); - } - Err(e) => { - log::error!("Failed to enable EA: {}", e); - let _ = dialog_handle.update(cx, |d, cx| { - d.set_error(format!("Error: {}", e), cx); - }); - } + let _ = entity.update(cx, |this, cx| match result { + Ok(msg) => { + log::info!("{}", msg); + let _ = dialog_handle.update(cx, |d, cx| { + d.set_success(msg, cx); + }); + this.sync_fido_state(None, cx); + } + Err(e) => { + log::error!("Failed to enable EA: {}", e); + this.loading = false; + let _ = dialog_handle.update(cx, |d, cx| { + d.set_error(format!("Error: {}", e), cx); + }); + cx.notify(); } - cx.notify(); }); })); } @@ -1177,7 +1180,6 @@ impl PasskeysView { .await; let _ = entity.update(cx, |this, cx| { - this.loading = false; match result { Ok(msg) => { log::info!("Device Reset: {}", msg); @@ -1188,15 +1190,17 @@ impl PasskeysView { cx.emit(PasskeysEvent::Notification( "Device reset successfully".into(), )); + this.sync_fido_state(None, cx); } Err(e) => { log::error!("Error resetting device: {}", e); + this.loading = false; let _ = status_handle.update(cx, |d, cx| { d.set_error(format!("Reset failed: {}", e), cx); }); + cx.notify(); } } - cx.notify(); }); })); } From e6f9c4df6dba296bb370313a72f29709d404f798 Mon Sep 17 00:00:00 2001 From: Suyog Tandel Date: Tue, 30 Jun 2026 00:04:31 +0530 Subject: [PATCH 2/2] fix: pr98 review, lock the storage before performing a sync to get new status after device reset --- src/ui/views/passkeys.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ui/views/passkeys.rs b/src/ui/views/passkeys.rs index a6687f3..8551056 100644 --- a/src/ui/views/passkeys.rs +++ b/src/ui/views/passkeys.rs @@ -1190,6 +1190,7 @@ impl PasskeysView { cx.emit(PasskeysEvent::Notification( "Device reset successfully".into(), )); + this.lock_storage(cx); this.sync_fido_state(None, cx); } Err(e) => {