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