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:
@@ -10,7 +10,7 @@ path = "src/main.rs"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
gtk = { package = "gtk4", version = "0.8" }
|
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" }
|
authforge-common = { path = "../common" }
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
zbus = { workspace = true }
|
zbus = { workspace = true }
|
||||||
|
|||||||
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(crate) struct KeysPage {
|
||||||
pub root: adw::Bin,
|
pub root: adw::Bin,
|
||||||
daemon: Rc<RefCell<Option<Daemon>>>,
|
daemon: Rc<RefCell<Option<Daemon>>>,
|
||||||
#[allow(dead_code)] // wired through enroll_dialog in Task 6.
|
|
||||||
parent_window: gtk::Window,
|
parent_window: gtk::Window,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,9 +151,17 @@ impl KeysPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn start_enroll(self: &Rc<Self>) {
|
async fn start_enroll(self: &Rc<Self>) {
|
||||||
// Stub until Task 6 lands the TouchDialog. Until then, surface the
|
let daemon = match self.daemon.borrow().clone() {
|
||||||
// intent so manual smoke can see the click was registered.
|
Some(d) => d,
|
||||||
self.show_toast("Enroll: implemented in Task 6");
|
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) {
|
fn show_toast(self: &Rc<Self>, msg: &str) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use adw::prelude::*;
|
|||||||
use gtk::glib;
|
use gtk::glib;
|
||||||
|
|
||||||
mod bus;
|
mod bus;
|
||||||
|
mod enroll_dialog;
|
||||||
mod error;
|
mod error;
|
||||||
mod keys_page;
|
mod keys_page;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user