diff --git a/gui/Cargo.toml b/gui/Cargo.toml index f1bc887..04f8b0d 100644 --- a/gui/Cargo.toml +++ b/gui/Cargo.toml @@ -10,7 +10,7 @@ path = "src/main.rs" [dependencies] gtk = { package = "gtk4", version = "0.8" } -adw = { package = "libadwaita", version = "0.6" } +adw = { package = "libadwaita", version = "0.6", features = ["v1_5"] } authforge-common = { path = "../common" } anyhow = { workspace = true } zbus = { workspace = true } diff --git a/gui/src/enroll_dialog.rs b/gui/src/enroll_dialog.rs new file mode 100644 index 0000000..9d46ced --- /dev/null +++ b/gui/src/enroll_dialog.rs @@ -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"); +} diff --git a/gui/src/keys_page.rs b/gui/src/keys_page.rs index 35497c1..476c93e 100644 --- a/gui/src/keys_page.rs +++ b/gui/src/keys_page.rs @@ -11,7 +11,6 @@ use std::rc::Rc; pub(crate) struct KeysPage { pub root: adw::Bin, daemon: Rc>>, - #[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) { - // 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, msg: &str) { diff --git a/gui/src/main.rs b/gui/src/main.rs index c66cbdc..6da00f4 100644 --- a/gui/src/main.rs +++ b/gui/src/main.rs @@ -2,6 +2,7 @@ use adw::prelude::*; use gtk::glib; mod bus; +mod enroll_dialog; mod error; mod keys_page;