mirror of
https://github.com/tauri-apps/plugins-workspace.git
synced 2026-10-18 23:07:27 +02:00
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:
1 parent
a819187e12
commit
2ebe54abce
17 files changed
+439
-68
No files matched your search
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"biometric": minor:feat
|
||||
"biometric-js": minor:feat
|
||||
---
|
||||
|
||||
Add macOS Touch ID support via LocalAuthentication framework.
|
||||
Generated
+15
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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())?;
|
||||
}
|
||||
|
||||
@@ -171,7 +171,7 @@
|
||||
component: Nfc,
|
||||
icon: 'i-ph-nfc'
|
||||
},
|
||||
isMobile && {
|
||||
{
|
||||
label: 'Biometric',
|
||||
component: Biometric,
|
||||
icon: 'i-ph-scan'
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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"] }
|
||||
@@ -1,12 +1,12 @@
|
||||

|
||||
|
||||
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 | ✓ |
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
})?
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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" })
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user