feat(biometric): add macOS Touch ID support via LocalAuthentication framework (#3264)

* feat(biometric): add macOS Touch ID support via LocalAuthentication framework

* cleanup

* fmt

* change file

* fix tests

---------

Co-authored-by: Lucas Nogueira <lucas@tauri.app>
This commit is contained in:
Masayoshi TakahashiandLucas Nogueira authored and GitHub committed 2026-10-11 21:56:15 -03:00
1 parent a819187e12
commit 2ebe54abce
17 files changed
+439 -68

No files matched your search

+6
View File
@@ -0,0 +1,6 @@
---
"biometric": minor:feat
"biometric-js": minor:feat
---
Add macOS Touch ID support via LocalAuthentication framework.
Generated
+15
View File
@@ -4209,6 +4209,17 @@ dependencies = [
"objc2-core-foundation",
]
[[package]]
name = "objc2-local-authentication"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fc4c269ebb5210f5ae57ce061e08ec05cf35a6fb4a5d8287c7d4b1776ea6700"
dependencies = [
"block2 0.6.2",
"objc2 0.6.4",
"objc2-foundation 0.3.2",
]
[[package]]
name = "objc2-metal"
version = "0.2.2"
@@ -6722,7 +6733,11 @@ dependencies = [
name = "tauri-plugin-biometric"
version = "2.4.2"
dependencies = [
"block2 0.6.2",
"log",
"objc2 0.6.4",
"objc2-foundation 0.3.2",
"objc2-local-authentication",
"serde",
"serde_json",
"serde_repr",
+1 -1
View File
@@ -42,6 +42,7 @@ tauri-plugin-sql = { path = "../../../plugins/sql", version = "2.4.1", features
"sqlite",
] }
tauri-plugin-websocket = { path = "../../../plugins/websocket", version = "2.4.3" }
tauri-plugin-biometric = { path = "../../../plugins/biometric/", version = "2.4.1" }
# WebDriver automation bridge, used by the plugins e2e suite (packages/api-e2e).
# Desktop-only and behind the off-by-default `automation` feature so it never ships in a
@@ -76,7 +77,6 @@ tauri-plugin-window-state = { path = "../../../plugins/window-state", version =
[target."cfg(any(target_os = \"android\", target_os = \"ios\"))".dependencies]
tauri-plugin-barcode-scanner = { path = "../../../plugins/barcode-scanner/", version = "2.5.2" }
tauri-plugin-nfc = { path = "../../../plugins/nfc", version = "2.4.2" }
tauri-plugin-biometric = { path = "../../../plugins/biometric/", version = "2.4.2" }
tauri-plugin-geolocation = { path = "../../../plugins/geolocation/", version = "2.4.2" }
tauri-plugin-haptics = { path = "../../../plugins/haptics/", version = "2.4.2" }
@@ -9,6 +9,8 @@
"cli:default",
"positioner:default",
"updater:default",
"biometric:allow-authenticate",
"biometric:allow-status",
"global-shortcut:allow-unregister",
"global-shortcut:allow-register",
"global-shortcut:allow-unregister-all",
+1 -1
View File
@@ -45,6 +45,7 @@ pub fn run() {
.build(),
)
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_biometric::init())
.plugin(tauri_plugin_clipboard_manager::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_http::init())
@@ -92,7 +93,6 @@ pub fn run() {
{
app.handle().plugin(tauri_plugin_barcode_scanner::init())?;
app.handle().plugin(tauri_plugin_nfc::init())?;
app.handle().plugin(tauri_plugin_biometric::init())?;
app.handle().plugin(tauri_plugin_geolocation::init())?;
app.handle().plugin(tauri_plugin_haptics::init())?;
}
+1 -1
View File
@@ -171,7 +171,7 @@
component: Nfc,
icon: 'i-ph-nfc'
},
isMobile && {
{
label: 'Biometric',
component: Biometric,
icon: 'i-ph-scan'
+3 -3
View File
@@ -39,6 +39,7 @@ import type * as WindowState from '@tauri-apps/plugin-window-state'
* only have a default export, so their global is that class itself.
*/
export interface CommonPluginApi {
biometric: typeof Biometric
clipboardManager: typeof ClipboardManager
deepLink: typeof DeepLink
dialog: typeof Dialog
@@ -69,7 +70,6 @@ export interface DesktopPluginApi {
/** The plugin APIs the example only registers on mobile (`#[cfg(mobile)]`). */
export interface MobilePluginApi {
barcodeScanner: typeof BarcodeScanner
biometric: typeof Biometric
geolocation: typeof Geolocation
haptics: typeof Haptics
nfc: typeof Nfc
@@ -265,8 +265,8 @@ export interface DescribePluginOptions {
desktopOnly?: boolean
/**
* The example only registers the plugin on mobile (`barcode-scanner`,
* `biometric`, `geolocation`, `haptics`, `nfc`), so the whole suite is
* skipped on desktop.
* `geolocation`, `haptics`, `nfc`), so the whole suite is skipped on
* desktop.
*/
mobileOnly?: boolean
}
+1 -1
View File
@@ -23,6 +23,7 @@ type Surface<T> = { [P in keyof T]: (keyof T[P])[] }
/** Plugins the example registers on every platform. */
const commonSurface: Surface<CommonPluginApi> = {
biometric: ['BiometryType', 'checkStatus', 'authenticate'],
clipboardManager: [
'writeText',
'readText',
@@ -144,7 +145,6 @@ const mobileSurface: Surface<MobilePluginApi> = {
'requestPermissions',
'openAppSettings'
],
biometric: ['BiometryType', 'checkStatus', 'authenticate'],
geolocation: [
'watchPosition',
'getCurrentPosition',
+16 -3
View File
@@ -1,7 +1,7 @@
[package]
name = "tauri-plugin-biometric"
version = "2.4.2"
description = "Prompt the user for biometric authentication on Android and iOS."
description = "Prompt the user for biometric authentication on Android, iOS, and macOS."
edition = { workspace = true }
rust-version = { workspace = true }
authors = { workspace = true }
@@ -16,11 +16,10 @@ targets = ["x86_64-linux-android"]
[package.metadata.platforms.support]
windows = { level = "none", notes = "" }
linux = { level = "none", notes = "" }
macos = { level = "none", notes = "" }
macos = { level = "full", notes = "" }
android = { level = "full", notes = "" }
ios = { level = "full", notes = "" }
[build-dependencies]
tauri-plugin = { workspace = true, features = ["build"] }
@@ -32,5 +31,19 @@ log = { workspace = true }
thiserror = { workspace = true }
serde_repr = "0.1"
[target."cfg(target_os = \"macos\")".dependencies]
block2 = "0.6"
objc2 = "0.6"
objc2-foundation = { version = "0.3", default-features = false, features = [
"NSError",
"NSString",
] }
objc2-local-authentication = { version = "0.3", default-features = false, features = [
"LAContext",
"LAError",
"LABiometryType",
"block2",
] }
[target.'cfg(target_os = "ios")'.dependencies]
tauri = { workspace = true, features = ["wry"] }
+2 -2
View File
@@ -1,12 +1,12 @@
![biometric](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/biometric/banner.png)
Prompt the user for biometric authentication on Android and iOS.
Prompt the user for biometric authentication on Android, iOS and macOS.
| Platform | Supported |
| -------- | --------- |
| Linux | x |
| Windows | x |
| macOS | x |
| macOS | ✓ |
| Android | ✓ |
| iOS | ✓ |
+6 -6
View File
@@ -3,7 +3,7 @@
// SPDX-License-Identifier: MIT
/**
* Prompt the user for biometric authentication on Android and iOS.
* Prompt the user for biometric authentication on Android, iOS and macOS.
*
* @module
*/
@@ -54,13 +54,13 @@ export interface Status {
* Options for the {@linkcode authenticate} biometric prompt.
*/
export interface AuthOptions {
/** Enables authentication using the device's password or PIN. Available on both Android and iOS. */
/** Enables authentication using the device's password or PIN. Available on Android, iOS and macOS. */
allowDeviceCredential?: boolean
/** Label for the cancel button. Available on both Android and iOS. */
/** Label for the cancel button. Available on Android, iOS and macOS. */
cancelTitle?: string
// iOS options
/** Text displayed on the fallback button if biometric authentication fails. **iOS only.** */
// iOS and macOS options
/** Text displayed on the fallback button if biometric authentication fails. **iOS and macOS only.** */
fallbackTitle?: string
// android options
@@ -94,7 +94,7 @@ export async function checkStatus(): Promise<Status> {
/**
* Prompts the user for authentication using the system interface (touchID, faceID or Android Iris).
* Rejects if the authentication fails.
* Rejects if the authentication fails, with an error object containing a `message` and, when the platform provides one, a `code` (e.g. `userCancel`).
*
* @example
* ```typescript
+44
View File
@@ -0,0 +1,44 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use tauri::{AppHandle, Runtime, State, command};
use crate::{Biometric, BiometricExt, Result, models::*};
#[command]
pub(crate) async fn status<R: Runtime>(
_app: AppHandle<R>,
biometric: State<'_, Biometric<R>>,
) -> Result<Status> {
biometric.status()
}
#[command]
#[allow(clippy::too_many_arguments)]
pub(crate) async fn authenticate<R: Runtime>(
app: AppHandle<R>,
reason: String,
allow_device_credential: Option<bool>,
cancel_title: Option<String>,
fallback_title: Option<String>,
title: Option<String>,
subtitle: Option<String>,
confirmation_required: Option<bool>,
) -> Result<()> {
let options = AuthOptions {
allow_device_credential: allow_device_credential.unwrap_or(false),
cancel_title,
fallback_title,
title,
subtitle,
confirmation_required,
};
// The prompt blocks until the user answers, so keep it off the async runtime workers.
tauri::async_runtime::spawn_blocking(move || app.biometric().authenticate(reason, options))
.await
.map_err(|e| crate::Error::AuthenticationFailed {
message: e.to_string(),
code: None,
})?
}
+191
View File
@@ -0,0 +1,191 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use serde::de::DeserializeOwned;
use tauri::{AppHandle, Runtime, plugin::PluginApi};
use crate::models::*;
pub fn init<R: Runtime, C: DeserializeOwned>(
app: &AppHandle<R>,
_api: PluginApi<R, C>,
) -> crate::Result<Biometric<R>> {
Ok(Biometric(app.clone()))
}
/// Access to the biometric APIs.
pub struct Biometric<R: Runtime>(AppHandle<R>);
impl<R: Runtime> Biometric<R> {
pub fn status(&self) -> crate::Result<Status> {
#[cfg(target_os = "macos")]
{
macos::status()
}
#[cfg(not(target_os = "macos"))]
{
Ok(Status {
is_available: false,
biometry_type: BiometryType::None,
error: Some("Biometric authentication is not supported on this platform".into()),
error_code: Some("biometryNotAvailable".into()),
})
}
}
pub fn authenticate(&self, reason: String, options: AuthOptions) -> crate::Result<()> {
#[cfg(target_os = "macos")]
{
macos::authenticate(reason, options)
}
#[cfg(not(target_os = "macos"))]
{
let _ = (reason, options);
Err(crate::Error::Unavailable {
message: "Biometric authentication is not supported on this platform".into(),
code: Some("biometryNotAvailable".into()),
})
}
}
}
#[cfg(target_os = "macos")]
mod macos {
use super::*;
use block2::RcBlock;
use objc2::runtime::Bool;
use objc2_foundation::{NSError, NSString};
use objc2_local_authentication::{LABiometryType, LAContext, LAError, LAPolicy};
pub fn status() -> crate::Result<Status> {
let context = unsafe { LAContext::new() };
let result = unsafe {
context.canEvaluatePolicy_error(LAPolicy::DeviceOwnerAuthenticationWithBiometrics)
};
let biometry_type = unsafe { context.biometryType() };
match result {
Ok(()) => Ok(Status {
is_available: true,
biometry_type: convert_biometry_type(biometry_type),
error: None,
error_code: None,
}),
Err(error) => {
let desc = error.localizedDescription().to_string();
// Same fallback as the iOS plugin for codes outside the documented set.
let code = map_la_error_code(error.code()).unwrap_or("biometryNotAvailable");
Ok(Status {
is_available: false,
biometry_type: convert_biometry_type(biometry_type),
error: Some(desc),
error_code: Some(code.into()),
})
}
}
}
pub fn authenticate(reason: String, options: AuthOptions) -> crate::Result<()> {
let context = unsafe { LAContext::new() };
// Pre-check: if biometry is unavailable and device credential fallback is disabled,
// return early with the error.
let can_evaluate = unsafe {
context.canEvaluatePolicy_error(LAPolicy::DeviceOwnerAuthenticationWithBiometrics)
};
if let Err(error) = can_evaluate
&& !options.allow_device_credential
{
let code = map_la_error_code(error.code()).unwrap_or("biometryNotAvailable");
return Err(crate::Error::Unavailable {
message: error.localizedDescription().to_string(),
code: Some(code.into()),
});
}
// Set localized titles
if let Some(ref fallback_title) = options.fallback_title {
let title = NSString::from_str(fallback_title);
unsafe { context.setLocalizedFallbackTitle(Some(&title)) };
}
if options.allow_device_credential && matches!(options.fallback_title.as_deref(), Some(""))
{
unsafe { context.setLocalizedFallbackTitle(None) };
}
if let Some(ref cancel_title) = options.cancel_title {
let title = NSString::from_str(cancel_title);
unsafe { context.setLocalizedCancelTitle(Some(&title)) };
}
// Disable authentication reuse
unsafe { context.setTouchIDAuthenticationAllowableReuseDuration(0.0) };
let reason = NSString::from_str(&reason);
let policy = if options.allow_device_credential {
LAPolicy::DeviceOwnerAuthentication
} else {
LAPolicy::DeviceOwnerAuthenticationWithBiometrics
};
let (tx, rx) = std::sync::mpsc::channel();
let block = RcBlock::new(move |success: Bool, error: *mut NSError| {
if success.as_bool() {
tx.send(Ok(())).ok();
} else {
let err = if !error.is_null() {
let e = unsafe { &*error };
crate::Error::AuthenticationFailed {
message: e.localizedDescription().to_string(),
code: map_la_error_code(e.code()).map(Into::into),
}
} else {
crate::Error::AuthenticationFailed {
message: "Unknown error".into(),
code: Some("authenticationFailed".into()),
}
};
tx.send(Err(err)).ok();
}
});
unsafe {
context.evaluatePolicy_localizedReason_reply(policy, &reason, &block);
}
rx.recv().map_err(|_| crate::Error::AuthenticationFailed {
message: "Channel closed".into(),
code: None,
})?
}
fn convert_biometry_type(biometry_type: LABiometryType) -> BiometryType {
if biometry_type == LABiometryType::TouchID {
BiometryType::TouchID
} else if biometry_type == LABiometryType::FaceID {
BiometryType::FaceID
} else {
BiometryType::None
}
}
/// Maps an `LAError` code to the identifiers documented in the JS API, matching the iOS plugin.
fn map_la_error_code(code: isize) -> Option<&'static str> {
let code = match LAError(code) {
LAError::AuthenticationFailed => "authenticationFailed",
LAError::UserCancel => "userCancel",
LAError::UserFallback => "userFallback",
LAError::SystemCancel => "systemCancel",
LAError::PasscodeNotSet => "passcodeNotSet",
LAError::BiometryNotAvailable => "biometryNotAvailable",
LAError::BiometryNotEnrolled => "biometryNotEnrolled",
LAError::BiometryLockout => "biometryLockout",
LAError::AppCancel => "appCancel",
LAError::InvalidContext => "invalidContext",
LAError::NotInteractive => "notInteractive",
_ => return None,
};
Some(code)
}
}
+62 -1
View File
@@ -19,6 +19,24 @@ pub enum Error {
#[cfg(mobile)]
#[error(transparent)]
PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError),
/// The authentication prompt failed or was canceled by the user or the system.
#[cfg(desktop)]
#[error("Biometric authentication failed: {message}")]
AuthenticationFailed {
/// A human-readable description of the failure, as reported by the operating system.
message: String,
/// The error code, using the same identifiers as [`Status::error_code`](crate::Status::error_code).
code: Option<String>,
},
/// Biometric authentication cannot be used on this device.
#[cfg(desktop)]
#[error("Biometric unavailable: {message}")]
Unavailable {
/// A human-readable reason why biometric authentication is unavailable.
message: String,
/// The error code, using the same identifiers as [`Status::error_code`](crate::Status::error_code).
code: Option<String>,
},
}
impl Serialize for Error {
@@ -26,6 +44,49 @@ impl Serialize for Error {
where
S: Serializer,
{
serializer.serialize_str(self.to_string().as_ref())
match self {
// Same `{ message, code }` shape the Android and iOS plugins reject with.
#[cfg(desktop)]
Self::AuthenticationFailed { message, code } | Self::Unavailable { message, code } => {
#[derive(Serialize)]
struct Payload<'a> {
message: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
code: Option<&'a str>,
}
Payload {
message,
code: code.as_deref(),
}
.serialize(serializer)
}
_ => serializer.serialize_str(self.to_string().as_ref()),
}
}
}
#[cfg(all(test, desktop))]
mod tests {
use super::Error;
#[test]
fn desktop_errors_serialize_like_mobile_rejections() {
let err = Error::AuthenticationFailed {
message: "Canceled by user.".into(),
code: Some("userCancel".into()),
};
assert_eq!(
serde_json::to_value(&err).unwrap(),
serde_json::json!({ "message": "Canceled by user.", "code": "userCancel" })
);
let err = Error::Unavailable {
message: "No biometry".into(),
code: None,
};
assert_eq!(
serde_json::to_value(&err).unwrap(),
serde_json::json!({ "message": "No biometry" })
);
}
}
+27 -44
View File
@@ -4,56 +4,30 @@
//! Prompt the user for biometric authentication.
//!
//! - Supported platforms: Android and iOS.
//! - Supported platforms: Android, iOS and macOS.
#![cfg(mobile)]
use serde::Serialize;
use tauri::{
Manager, Runtime,
plugin::{Builder, PluginHandle, TauriPlugin},
plugin::{Builder, TauriPlugin},
};
pub use models::*;
#[cfg(desktop)]
mod commands;
#[cfg(desktop)]
mod desktop;
mod error;
#[cfg(mobile)]
mod mobile;
mod models;
pub use error::{Error, Result};
#[cfg(target_os = "android")]
const PLUGIN_IDENTIFIER: &str = "app.tauri.biometric";
#[cfg(target_os = "ios")]
tauri::ios_plugin_binding!(init_plugin_biometric);
/// Access to the biometric APIs.
pub struct Biometric<R: Runtime>(PluginHandle<R>);
#[derive(Serialize)]
struct AuthenticatePayload {
reason: String,
#[serde(flatten)]
options: AuthOptions,
}
impl<R: Runtime> Biometric<R> {
/// Checks the device's availability and type of biometric authentication, as reported by the
/// operating system. Errors if the underlying mobile plugin invocation fails.
pub fn status(&self) -> crate::Result<Status> {
self.0.run_mobile_plugin("status", ()).map_err(Into::into)
}
/// Prompts the user for biometric authentication using the system UI (Android
/// `BiometricPrompt` or iOS `LocalAuthentication`), showing `reason` as the purpose of the
/// request. Resolves once the user is authenticated and errors if authentication fails, is
/// canceled, or the underlying mobile plugin invocation fails.
pub fn authenticate(&self, reason: String, options: AuthOptions) -> crate::Result<()> {
self.0
.run_mobile_plugin("authenticate", AuthenticatePayload { reason, options })
.map_err(Into::into)
}
}
#[cfg(desktop)]
pub use desktop::Biometric;
#[cfg(mobile)]
pub use mobile::Biometric;
/// Extensions to [`tauri::App`], [`tauri::AppHandle`], [`tauri::WebviewWindow`], [`tauri::Webview`] and [`tauri::Window`] to access the biometric APIs.
pub trait BiometricExt<R: Runtime> {
@@ -69,13 +43,22 @@ impl<R: Runtime, T: Manager<R>> crate::BiometricExt<R> for T {
/// Initializes the plugin.
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("biometric")
let builder = Builder::new("biometric");
// On mobile the commands are routed directly to the native plugin.
#[cfg(desktop)]
let builder = builder.invoke_handler(tauri::generate_handler![
commands::status,
commands::authenticate,
]);
builder
.setup(|app, api| {
#[cfg(target_os = "android")]
let handle = api.register_android_plugin(PLUGIN_IDENTIFIER, "BiometricPlugin")?;
#[cfg(target_os = "ios")]
let handle = api.register_ios_plugin(init_plugin_biometric)?;
app.manage(Biometric(handle));
#[cfg(mobile)]
let biometric = mobile::init(app, api)?;
#[cfg(desktop)]
let biometric = desktop::init(app, api)?;
app.manage(biometric);
Ok(())
})
.build()
+56
View File
@@ -0,0 +1,56 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use serde::{Serialize, de::DeserializeOwned};
use tauri::{
AppHandle, Runtime,
plugin::{PluginApi, PluginHandle},
};
use crate::models::*;
#[cfg(target_os = "android")]
const PLUGIN_IDENTIFIER: &str = "app.tauri.biometric";
#[cfg(target_os = "ios")]
tauri::ios_plugin_binding!(init_plugin_biometric);
pub fn init<R: Runtime, C: DeserializeOwned>(
_app: &AppHandle<R>,
api: PluginApi<R, C>,
) -> crate::Result<Biometric<R>> {
#[cfg(target_os = "android")]
let handle = api.register_android_plugin(PLUGIN_IDENTIFIER, "BiometricPlugin")?;
#[cfg(target_os = "ios")]
let handle = api.register_ios_plugin(init_plugin_biometric)?;
Ok(Biometric(handle))
}
/// Access to the biometric APIs.
pub struct Biometric<R: Runtime>(PluginHandle<R>);
#[derive(Serialize)]
struct AuthenticatePayload {
reason: String,
#[serde(flatten)]
options: AuthOptions,
}
impl<R: Runtime> Biometric<R> {
/// Checks the device's availability and type of biometric authentication, as reported by the
/// operating system. Errors if the underlying mobile plugin invocation fails.
pub fn status(&self) -> crate::Result<Status> {
self.0.run_mobile_plugin("status", ()).map_err(Into::into)
}
/// Prompts the user for biometric authentication using the system UI (Android
/// `BiometricPrompt` or iOS `LocalAuthentication`), showing `reason` as the purpose of the
/// request. Resolves once the user is authenticated and errors if authentication fails, is
/// canceled, or the underlying mobile plugin invocation fails.
pub fn authenticate(&self, reason: String, options: AuthOptions) -> crate::Result<()> {
self.0
.run_mobile_plugin("authenticate", AuthenticatePayload { reason, options })
.map_err(Into::into)
}
}
+5 -5
View File
@@ -8,11 +8,11 @@ use serde::{Deserialize, Serialize};
#[derive(Debug, Default, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AuthOptions {
/// Enables authentication using the device's password. This feature is available on both Android and iOS.
/// Enables authentication using the device's password. This feature is available on Android, iOS and macOS.
pub allow_device_credential: bool,
/// Label for the Cancel button. This feature is available on both Android and iOS.
/// Label for the Cancel button. This feature is available on Android, iOS and macOS.
pub cancel_title: Option<String>,
/// Specifies the text displayed on the fallback button if biometric authentication fails. This feature is available iOS only.
/// Specifies the text displayed on the fallback button if biometric authentication fails. This feature is available on iOS and macOS only.
pub fallback_title: Option<String>,
/// Title indicating the purpose of biometric verification. This feature is available Android only.
pub title: Option<String>,
@@ -23,7 +23,7 @@ pub struct AuthOptions {
}
/// The kind of biometry hardware detected on the device.
#[derive(Debug, Clone, serde_repr::Deserialize_repr)]
#[derive(Debug, Clone, serde_repr::Deserialize_repr, serde_repr::Serialize_repr)]
#[repr(u8)]
pub enum BiometryType {
/// No biometry hardware is available, or it is not enrolled with the operating system.
@@ -36,7 +36,7 @@ pub enum BiometryType {
/// The result of [`Biometric::status`](crate::Biometric::status), describing whether biometric
/// authentication can currently be used.
#[derive(Debug, Clone, Deserialize)]
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Status {
/// Whether the device can currently authenticate using biometrics.