feat(gui): TouchDialog modal racing EnrollOwn against EnrollmentFailed signal
Adds enroll_dialog::present which opens an adw::AlertDialog with a spinner and races the EnrollOwn call against the EnrollmentFailed signal stream: - Call returns Ok(_cred) -> close dialog, fire on_success refresh callback. - Call returns Err -> swap dialog body to the user-message form. - Signal arrives first -> swap to the signal payload (faster than waiting for the call's typed Err to traverse the bus). KeysPage::start_enroll wires the activated row to present(), passing the parent window for modal anchoring and a refresh closure as on_success. Plan deviation: AlertDialog is libadwaita v1_5-gated, so gui/Cargo.toml enables that feature. Targets Ubuntu 24.04+ (libadwaita 1.5).
This commit is contained in:
79
gui/src/enroll_dialog.rs
Normal file
79
gui/src/enroll_dialog.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
//! Modal touch-prompt dialog. Lifetime is one enrollment attempt.
|
||||
//!
|
||||
//! Races the EnrollOwn call against the EnrollmentFailed signal — the call
|
||||
//! return is the source of truth for success (carries the new Credential),
|
||||
//! the signal lets us surface a useful failure string fast.
|
||||
|
||||
use crate::bus::{current_user, Daemon};
|
||||
use crate::error::user_message;
|
||||
use adw::prelude::*;
|
||||
use futures_util::StreamExt;
|
||||
use gtk::glib;
|
||||
|
||||
/// Present the modal and run an EnrollOwn against the daemon. `on_success`
|
||||
/// fires after a successful enrollment so the caller can refresh its list.
|
||||
pub(crate) fn present(parent: >k::Window, daemon: Daemon, on_success: impl Fn() + 'static) {
|
||||
let dialog = adw::AlertDialog::builder()
|
||||
.heading("Touch your security key")
|
||||
.body("Touch the metal contact on your security key to enroll it.")
|
||||
.build();
|
||||
dialog.add_response("cancel", "Cancel");
|
||||
dialog.set_default_response(Some("cancel"));
|
||||
dialog.set_close_response("cancel");
|
||||
|
||||
let spinner = gtk::Spinner::builder()
|
||||
.spinning(true)
|
||||
.height_request(48)
|
||||
.width_request(48)
|
||||
.halign(gtk::Align::Center)
|
||||
.build();
|
||||
dialog.set_extra_child(Some(&spinner));
|
||||
|
||||
dialog.present(parent);
|
||||
|
||||
let dialog_clone = dialog.clone();
|
||||
glib::MainContext::default().spawn_local(async move {
|
||||
let user = current_user();
|
||||
// Subscribe BEFORE the call so a fast EnrollmentFailed isn't missed.
|
||||
let mut failed = match daemon.subscribe_enrollment_failed().await {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
show_failure(&dialog_clone, &user_message(&e));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let call_fut = daemon.enroll_own(&user, "Security Key");
|
||||
futures_util::pin_mut!(call_fut);
|
||||
|
||||
let outcome = futures_util::future::select(call_fut, failed.next()).await;
|
||||
match outcome {
|
||||
futures_util::future::Either::Left((Ok(_cred), _)) => {
|
||||
dialog_clone.close();
|
||||
on_success();
|
||||
}
|
||||
futures_util::future::Either::Left((Err(e), _)) => {
|
||||
show_failure(&dialog_clone, &user_message(&e));
|
||||
}
|
||||
futures_util::future::Either::Right((Some(msg), _)) => {
|
||||
let reason: String = msg
|
||||
.body()
|
||||
.deserialize()
|
||||
.unwrap_or_else(|_| "enrollment failed".into());
|
||||
show_failure(&dialog_clone, &reason);
|
||||
}
|
||||
futures_util::future::Either::Right((None, _)) => {
|
||||
show_failure(&dialog_clone, "signal stream closed unexpectedly");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn show_failure(dialog: &adw::AlertDialog, msg: &str) {
|
||||
dialog.set_heading(Some("Enrollment failed"));
|
||||
dialog.set_body(msg);
|
||||
dialog.set_extra_child(None::<>k::Widget>);
|
||||
// AlertDialog responses can't be removed dynamically; relabel "cancel"
|
||||
// to "Close" so the surviving button reads correctly.
|
||||
dialog.set_response_label("cancel", "Close");
|
||||
}
|
||||
@@ -11,7 +11,6 @@ use std::rc::Rc;
|
||||
pub(crate) struct KeysPage {
|
||||
pub root: adw::Bin,
|
||||
daemon: Rc<RefCell<Option<Daemon>>>,
|
||||
#[allow(dead_code)] // wired through enroll_dialog in Task 6.
|
||||
parent_window: gtk::Window,
|
||||
}
|
||||
|
||||
@@ -152,9 +151,17 @@ impl KeysPage {
|
||||
}
|
||||
|
||||
async fn start_enroll(self: &Rc<Self>) {
|
||||
// Stub until Task 6 lands the TouchDialog. Until then, surface the
|
||||
// intent so manual smoke can see the click was registered.
|
||||
self.show_toast("Enroll: implemented in Task 6");
|
||||
let daemon = match self.daemon.borrow().clone() {
|
||||
Some(d) => d,
|
||||
None => return,
|
||||
};
|
||||
let me = self.clone();
|
||||
crate::enroll_dialog::present(&self.parent_window, daemon, move || {
|
||||
let me = me.clone();
|
||||
glib::MainContext::default().spawn_local(async move {
|
||||
me.render_connected().await;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn show_toast(self: &Rc<Self>, msg: &str) {
|
||||
|
||||
@@ -2,6 +2,7 @@ use adw::prelude::*;
|
||||
use gtk::glib;
|
||||
|
||||
mod bus;
|
||||
mod enroll_dialog;
|
||||
mod error;
|
||||
mod keys_page;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user