Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# [unreleased]

- ui: Basic recovery from failed attempts to use devices (e.g. flaky bluetooth)

# 0.3.1 [2026-09-05]

## Improvements
Expand Down
40 changes: 36 additions & 4 deletions credentialsd-common/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,45 @@ pub const BACKGROUND_EVENT_ERROR_PIN_NOT_SET: u32 = 0x80000008;
#[derive(Debug, PartialEq)]
pub enum BackgroundEvent {
CeremonyCompleted,
NeedsPin { attempts_left: Option<u32> },
PinNotSet { error: PinNotSetError },
NeedsUserVerification { attempts_left: Option<u32> },
NeedsPin {
attempts_left: Option<u32>,
},
PinNotSet {
error: PinNotSetError,
},
NeedsUserVerification {
attempts_left: Option<u32>,
},
NeedsUserPresence,
SelectingCredential { creds: Vec<Credential> },
SelectingCredential {
creds: Vec<Credential>,
},

HybridIdle,
HybridStarted(OwnedFd),
HybridConnecting,
HybridConnected,
/// The hybrid ceremony was interrupted by a non-terminating error and a new
/// QR code is about to be issued. The UI should navigate back to the start
/// page so the new QR becomes visible.
HybridRestarting,

NfcIdle,
NfcWaiting,
NfcConnected,
/// The NFC ceremony was interrupted by a non-terminating error and the
/// transport is polling for a new device tap. The UI should navigate back
/// to the start page.
NfcRestarting,

UsbIdle,
UsbWaiting,
UsbSelectingDevice,
UsbConnected,
/// The USB ceremony was interrupted by a non-terminating error and the
/// transport is polling for a device. The UI should navigate back to the
/// start page.
UsbRestarting,

ErrorInternal,
ErrorTimedOut,
Expand Down Expand Up @@ -146,6 +166,18 @@ pub struct NotifyNfcConnectedOptions {}
#[zvariant(signature = "dict")]
pub struct NotifyUsbConnectedOptions {}

#[derive(Debug, SerializeDict, DeserializeDict, Type)]
#[zvariant(signature = "dict")]
pub struct NotifyHybridRestartingOptions {}

#[derive(Debug, SerializeDict, DeserializeDict, Type)]
#[zvariant(signature = "dict")]
pub struct NotifyUsbRestartingOptions {}

#[derive(Debug, SerializeDict, DeserializeDict, Type)]
#[zvariant(signature = "dict")]
pub struct NotifyNfcRestartingOptions {}

#[derive(Clone, Debug, Serialize, Deserialize, Type)]
pub enum Operation {
PublicKeyCreate,
Expand Down
49 changes: 46 additions & 3 deletions credentialsd-ui/src/dbus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,10 @@ use credentialsd_common::model::{
BACKGROUND_EVENT_ERROR_PIN_NOT_SET, BACKGROUND_EVENT_ERROR_TIMED_OUT, BackgroundEvent,
ClientPinEnteredOptions, Credential, CredentialSelectedOptions, Device,
DiscoveryRequestedOptions, NotifyHybridConnectedOptions, NotifyHybridConnectingOptions,
NotifyHybridStartedOptions, NotifyNeedsPinOptions, NotifyNeedsUserPresenceOptions,
NotifyNeedsUserVerificationOptions, NotifyNfcConnectedOptions, NotifyPinNotSetOptions,
NotifySelectingCredentialOptions, NotifyUsbConnectedOptions, Operation, PinNotSetError,
NotifyHybridRestartingOptions, NotifyHybridStartedOptions, NotifyNeedsPinOptions,
NotifyNeedsUserPresenceOptions, NotifyNeedsUserVerificationOptions, NotifyNfcConnectedOptions,
NotifyNfcRestartingOptions, NotifyPinNotSetOptions, NotifySelectingCredentialOptions,
NotifyUsbConnectedOptions, NotifyUsbRestartingOptions, Operation, PinNotSetError,
PortalBackendOptions, SetDevicePinOptions, UserInteractedEvent, WindowHandle,
};

Expand Down Expand Up @@ -307,6 +308,48 @@ impl CredentialPortalBackend {
.await
}

async fn notify_hybrid_restarting(
&self,
#[zbus(object_server)] object_server: &ObjectServer,
session_handle: ObjectPath<'_>,
_options: NotifyHybridRestartingOptions,
) -> fdo::Result<()> {
self.notify_state_changed(
object_server,
session_handle,
BackgroundEvent::HybridRestarting,
)
.await
}

async fn notify_usb_restarting(
&self,
#[zbus(object_server)] object_server: &ObjectServer,
session_handle: ObjectPath<'_>,
_options: NotifyUsbRestartingOptions,
) -> fdo::Result<()> {
self.notify_state_changed(
object_server,
session_handle,
BackgroundEvent::UsbRestarting,
)
.await
}

async fn notify_nfc_restarting(
&self,
#[zbus(object_server)] object_server: &ObjectServer,
session_handle: ObjectPath<'_>,
_options: NotifyNfcRestartingOptions,
) -> fdo::Result<()> {
self.notify_state_changed(
object_server,
session_handle,
BackgroundEvent::NfcRestarting,
)
.await
}

/// Called when the authentication ceremony completes successfully.
async fn notify_ceremony_completed(
&self,
Expand Down
4 changes: 4 additions & 0 deletions credentialsd-ui/src/gui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ pub enum ViewUpdate {
HybridConnecting,
HybridConnected,

/// A transport ceremony was interrupted by a non-terminating error and
/// is restarting. The UI should navigate back to the start page.
TransportRestarting,

Completed,
Cancelled,
Failed(String),
Expand Down
12 changes: 12 additions & 0 deletions credentialsd-ui/src/gui/view_model/gtk/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ mod imp {
#[property(get, set)]
pub qr_spinner_visible: RefCell<bool>,

#[property(get, set)]
pub transport_restarting: RefCell<bool>,

#[property(get, set)]
pub start_setting_new_pin_visible: RefCell<bool>,

Expand Down Expand Up @@ -138,6 +141,7 @@ impl ViewModel {
// TODO: hack so I don't have to unset this in every event manually.
view_model.set_usb_nfc_pin_entry_visible(false);
view_model.set_start_setting_new_pin_visible(false);
view_model.set_transport_restarting(false);
view_model.set_failed(false);
match update {
ViewUpdate::SetTitle {
Expand Down Expand Up @@ -239,6 +243,14 @@ impl ViewModel {
));
view_model.set_qr_spinner_visible(false);
}
ViewUpdate::TransportRestarting => {
// Signal the window to navigate back to start_page.
// The transport will emit a fresh Init/Connected state
// next, which will update the prompt and show the new
// QR code or device-waiting UI from start_page.
view_model.set_qr_spinner_visible(false);
view_model.set_transport_restarting(true);
}
ViewUpdate::Completed => {
view_model.set_qr_spinner_visible(false);
view_model.set_completed(true);
Expand Down
14 changes: 14 additions & 0 deletions credentialsd-ui/src/gui/view_model/gtk/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,20 @@ impl CredentialsUiWindow {
}
));

// When any transport restarts after a non-terminating error, navigate back to
// start_page. For hybrid this ensures the new QR code (which lives on start_page)
// is visible; for USB/NFC it clears stale prompts and lets the user re-plug or
// choose a different transport.
view_model.connect_transport_restarting_notify(clone!(
#[weak]
stack,
move |vm| {
if vm.transport_restarting() {
stack.set_visible_child_name("start_page");
}
}
));

view_model.connect_completed_notify(clone!(
#[weak]
stack,
Expand Down
11 changes: 11 additions & 0 deletions credentialsd-ui/src/gui/view_model/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,17 @@ impl ViewModel {
.await
.unwrap();
}
Event::Background(
BackgroundEvent::HybridRestarting
| BackgroundEvent::UsbRestarting
| BackgroundEvent::NfcRestarting,
) => {
self.hybrid_qr_code_data = None;
self.tx_update
.send(ViewUpdate::TransportRestarting)
.await
.unwrap();
}
Event::Background(BackgroundEvent::ErrorCancelled) => {
self.hybrid_qr_code_data = None;
break;
Expand Down
Loading