diff --git a/CHANGELOG.md b/CHANGELOG.md index 08bd8ac..8f28513 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # [unreleased] +## Breaking Changes + +### UI Controller API + +- Renamed the `username` field of `Credential` to `display_name`, matching WebAuthn's `user.displayName`. + # 0.4.0 [2026-11-05] ## Improvements diff --git a/credentialsd-common/src/model.rs b/credentialsd-common/src/model.rs index 1c74184..38e245c 100644 --- a/credentialsd-common/src/model.rs +++ b/credentialsd-common/src/model.rs @@ -82,7 +82,7 @@ pub struct SetDevicePinOptions {} pub struct Credential { pub id: String, pub name: String, - pub username: Option, + pub display_name: Option, } /// Emitted when an an authenticator presents multiple matching credentials, and @@ -373,3 +373,26 @@ impl Display for WindowHandle { } } } + +#[cfg(test)] +mod tests { + use zvariant::Value; + + use super::Credential; + + #[test] + fn credential_dict_uses_webauthn_field_names() { + let cred = Credential { + id: String::from("abc"), + name: String::from("alice@example.com"), + display_name: Some(String::from("Alice")), + }; + let Value::Dict(dict) = Value::from(cred) else { + panic!("Credential should serialize as a dict"); + }; + let get = |key: &str| dict.get::<&str, String>(&key).unwrap(); + assert_eq!(get("name").as_deref(), Some("alice@example.com")); + assert_eq!(get("display_name").as_deref(), Some("Alice")); + assert_eq!(get("username"), None); + } +} diff --git a/credentialsd-ui/src/gui/view_model/gtk/credential.rs b/credentialsd-ui/src/gui/view_model/gtk/credential.rs index ca8e54f..a1c6186 100644 --- a/credentialsd-ui/src/gui/view_model/gtk/credential.rs +++ b/credentialsd-ui/src/gui/view_model/gtk/credential.rs @@ -18,7 +18,7 @@ mod imp { pub name: RefCell, #[property(get, set)] - pub username: RefCell>, + pub display_name: RefCell>, } // The central trait for subclassing a GObject @@ -38,10 +38,10 @@ glib::wrapper! { } impl CredentialObject { - pub fn new(id: &str, name: &str, username: &Option) -> Self { + pub fn new(id: &str, name: &str, display_name: &Option) -> Self { let mut builder = Object::builder().property("id", id).property("name", name); - if let Some(username) = username { - builder = builder.property("username", username); + if let Some(display_name) = display_name { + builder = builder.property("display-name", display_name); } builder.build() } @@ -49,13 +49,13 @@ impl CredentialObject { impl From for CredentialObject { fn from(value: crate::gui::view_model::Credential) -> Self { - Self::new(&value.id, &value.name, &value.username) + Self::new(&value.id, &value.name, &value.display_name) } } impl From<&crate::gui::view_model::Credential> for CredentialObject { fn from(value: &crate::gui::view_model::Credential) -> Self { - Self::new(&value.id, &value.name, &value.username) + Self::new(&value.id, &value.name, &value.display_name) } } @@ -64,7 +64,36 @@ impl From for crate::gui::view_model::Credential { Self { id: value.id(), name: value.name(), - username: value.username(), + display_name: value.display_name(), } } } + +#[cfg(test)] +mod tests { + use super::CredentialObject; + use crate::gui::view_model::Credential; + + #[test] + fn credential_object_round_trips_display_name() { + let cred = Credential { + id: String::from("abc"), + name: String::from("alice@example.com"), + display_name: Some(String::from("Alice")), + }; + let obj = CredentialObject::from(&cred); + assert_eq!(obj.name(), "alice@example.com"); + assert_eq!(obj.display_name().as_deref(), Some("Alice")); + assert_eq!(Credential::from(obj), cred); + } + + #[test] + fn credential_object_allows_missing_display_name() { + let cred = Credential { + id: String::from("abc"), + name: String::from("alice@example.com"), + display_name: None, + }; + assert_eq!(Credential::from(CredentialObject::from(&cred)), cred); + } +} diff --git a/credentialsd-ui/src/gui/view_model/gtk/mod.rs b/credentialsd-ui/src/gui/view_model/gtk/mod.rs index 76afc9e..6e8356d 100644 --- a/credentialsd-ui/src/gui/view_model/gtk/mod.rs +++ b/credentialsd-ui/src/gui/view_model/gtk/mod.rs @@ -309,8 +309,8 @@ impl ViewModel { .build(); let icon = gtk::Image::builder().icon_name(icon_name).build(); let mut display_label = credential.name().to_string(); - if let Some(username) = credential.username() { - display_label += &format!(" ({username})"); + if let Some(display_name) = credential.display_name() { + display_label += &format!(" ({display_name})"); } let label = gtk::Label::builder().label(display_label).build(); b.append(&icon); diff --git a/credentialsd/src/credential_service/nfc.rs b/credentialsd/src/credential_service/nfc.rs index 438a632..5b7a7e7 100644 --- a/credentialsd/src/credential_service/nfc.rs +++ b/credentialsd/src/credential_service/nfc.rs @@ -554,7 +554,7 @@ impl From for NfcState { .as_ref() .and_then(|u| u.name.clone()) .unwrap_or_else(|| String::from("")), - username: x + display_name: x .user .as_ref() .map(|u| u.display_name.clone()) diff --git a/credentialsd/src/credential_service/usb.rs b/credentialsd/src/credential_service/usb.rs index 1207833..526ac4a 100644 --- a/credentialsd/src/credential_service/usb.rs +++ b/credentialsd/src/credential_service/usb.rs @@ -674,7 +674,7 @@ impl From for UsbState { .as_ref() .and_then(|u| u.name.clone()) .unwrap_or_else(|| String::from("")), - username: x + display_name: x .user .as_ref() .map(|u| u.display_name.clone()) diff --git a/doc/api.md b/doc/api.md index d445b61..b1812c9 100644 --- a/doc/api.md +++ b/doc/api.md @@ -54,6 +54,7 @@ sequenceDiagram - (Gateway): Make `origin` and `type` a required method parameter. - (Gateway): Flatten nested D-Bus struct with `request_json` on CreateCredential and GetCredential - (Gateway): Remove Client Capabilities method from Gateway API until further notice. +- (UI Controller): Renamed the `username` field of `Credential` to `display_name`, matching WebAuthn's `user.displayName`. ### Improvements @@ -509,9 +510,6 @@ The device needs evidence of user presence (e.g. touch) to release the credentia ### BackgroundEvent::SelectingCredential -> TODO: field names of Credential type are confusing: "name" is an ID, and -> "username" is a name. We should flip them. - Multiple credentials have been found and the user has to select which to use `tag`: `0x13` @@ -521,8 +519,10 @@ Multiple credentials have been found and the user has to select which to use ``` Credential [a{sv}] { id: string. An opaque ID referring to the credential on the device. - name: string. A human-readable identifier for the account. - username: string. A human-readable name for the account, intended for display. May be empty. + name: string. A human-readable identifier for the account, from the WebAuthn + `user.name` field, for example, "alice@example.com". + display_name: string. A human-readable name for the account, intended for + display, from the WebAuthn `user.displayName` field. May be empty. } ```