Skip to main content

azul_core/
biometric.rs

1//! POD types for the biometric-authentication surface
2//! (SUPER_PLAN_2 §1 feature 4 + research/02).
3//!
4//! Defined here in `azul-core` so the request config and result types
5//! can cross the FFI without `azul-layout` having to be a dependency.
6//! The stateful side (latest result, sync availability, async result
7//! channel) lives in `azul_layout::managers::biometric::BiometricManager`
8//! and re-exports these types for the existing import paths.
9//!
10//! Unlike geolocation (a continuous probe-driven subscription), biometric
11//! auth is **request-driven**: a callback asks `App::request_biometric_auth`
12//! with a [`BiometricPrompt`]; the OS draws its own modal; the platform
13//! backend parks the [`BiometricResult`] in the manager's async channel
14//! when the user responds.
15
16use azul_css::AzString;
17
18/// What biometric hardware the device can authenticate with right now.
19///
20/// This is the *sync availability probe* (iOS `LAContext.biometryType` /
21/// `canEvaluatePolicy`; Android `BiometricManager.canAuthenticate`), not
22/// the outcome of an auth attempt — that is [`BiometricResult`].
23/// `NotAvailable` covers "no sensor", "not enrolled", and "disabled by
24/// policy" alike; callers that need to distinguish those use the richer
25/// per-attempt [`BiometricResult`] variants.
26#[repr(C)]
27#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
28pub enum BiometricKind {
29    /// No usable biometric sensor (absent, unenrolled, or disabled).
30    #[default]
31    NotAvailable,
32    /// Fingerprint reader (Touch ID, Android fingerprint, Windows Hello
33    /// fingerprint).
34    Fingerprint,
35    /// Face recognition (Face ID, Android face unlock, Windows Hello face).
36    Face,
37    /// Iris scanner (Samsung legacy, some Android OEMs).
38    Iris,
39}
40
41impl BiometricKind {
42    /// `true` for any real sensor — i.e. anything except `NotAvailable`.
43    /// Lets the demo gate decide whether to even offer a biometric unlock.
44    #[must_use]
45    pub const fn is_available(&self) -> bool {
46        !matches!(self, Self::NotAvailable)
47    }
48}
49
50/// The outcome of one `request_biometric_auth` attempt, delivered to the
51/// caller's completion callback once the OS prompt resolves.
52///
53/// Maps onto every platform's result enum: iOS `LAError`, Android
54/// `BiometricPrompt.AuthenticationCallback`, Windows
55/// `UserConsentVerificationResult`, Linux polkit / PAM (research/02 §6).
56#[repr(C)]
57#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
58pub enum BiometricResult {
59    /// The user matched their face / finger / iris. Unlock granted.
60    Authenticated,
61    /// The user presented a biometric but it did not match (wrong
62    /// finger / face). Distinct from `Cancelled` — the prompt is still
63    /// up or retries were exhausted without a deliberate cancel.
64    Failed,
65    /// The user dismissed the prompt (tapped Cancel / pressed back).
66    Cancelled,
67    /// Biometrics failed but the user authenticated via the OS passcode
68    /// / PIN / device-credential fallback. Still a successful unlock —
69    /// only delivered when [`BiometricPrompt::allow_device_credential`]
70    /// was set.
71    FellBackToPasscode,
72    /// No usable biometric is enrolled / available on this device, so
73    /// the prompt could not be shown (Linux degraded path, or hardware
74    /// absent). Pairs with [`BiometricKind::NotAvailable`].
75    Unavailable,
76    /// A platform error occurred (sensor busy, lockout, key invalidated,
77    /// or an unmapped native error code).
78    Error,
79}
80
81impl BiometricResult {
82    /// `true` when the user successfully unlocked — either by biometric
83    /// match (`Authenticated`) or by the OS passcode fallback
84    /// (`FellBackToPasscode`). The vault gate keys off this.
85    #[must_use]
86    pub const fn is_success(&self) -> bool {
87        matches!(self, Self::Authenticated | Self::FellBackToPasscode)
88    }
89}
90
91// FFI Option wrapper. `CallbackInfo::get_biometric_result() ->
92// Option<BiometricResult>` returns `None` until the first request
93// completes; this is the no-codegen prerequisite for that accessor
94// (mirrors `OptionLocationFix`). The `availability` accessor returns a
95// bare `BiometricKind` (NotAvailable encodes "none"), so no Option there.
96impl_option!(
97    BiometricResult,
98    OptionBiometricResult,
99    [Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash]
100);
101
102/// Configuration for one biometric-auth request — what the OS prompt
103/// shows and which fallbacks are allowed. Passed to
104/// `App::request_biometric_auth`.
105///
106/// Strings are plain [`AzString`]; an empty string means "use the
107/// platform default label" (so callers only override what they care
108/// about). This keeps the public surface engine-agnostic and codegen
109/// stays a single struct with no nested `Option<String>` wrappers.
110#[repr(C)]
111#[derive(Debug, Clone, PartialEq, Eq)]
112pub struct BiometricPrompt {
113    /// Reason shown in the OS prompt — required on iOS
114    /// (`localizedReason`; the `NSFaceIDUsageDescription` plist key is
115    /// declared separately), shown as the Android subtitle and the
116    /// Windows / Linux message line. Empty is accepted but discouraged.
117    pub reason: AzString,
118    /// Label for the cancel / negative button (Android requires one;
119    /// iOS `localizedCancelTitle`). Empty → platform default ("Cancel").
120    pub cancel_label: AzString,
121    /// Allow the OS passcode / PIN / device-credential fallback when
122    /// biometrics fail or aren't enrolled. When the user takes that
123    /// path the result is [`BiometricResult::FellBackToPasscode`].
124    /// `false` = biometric-only (iOS `…WithBiometrics`, Android
125    /// `BIOMETRIC_STRONG` without `DEVICE_CREDENTIAL`).
126    pub allow_device_credential: bool,
127}
128
129impl Default for BiometricPrompt {
130    fn default() -> Self {
131        Self {
132            reason: AzString::from_const_str(""),
133            cancel_label: AzString::from_const_str(""),
134            allow_device_credential: false,
135        }
136    }
137}
138
139impl BiometricPrompt {
140    /// Convenience constructor: a biometric-only prompt showing `reason`,
141    /// with the platform-default cancel label and no passcode fallback.
142    #[must_use]
143    pub fn new(reason: AzString) -> Self {
144        Self {
145            reason,
146            ..Self::default()
147        }
148    }
149}
150
151#[cfg(test)]
152#[path = "biometric_test.rs"]
153mod biometric_test;