Update iOS unlock flow to support both biometrics and pin fallback (#1562)

This commit is contained in:
Leendert de Borst
2026-01-31 20:25:41 +00:00
committed by Leendert de Borst
parent 749980f58f
commit b9791a0563
2 changed files with 86 additions and 44 deletions
@@ -142,46 +142,64 @@ public class CredentialProviderViewController: ASCredentialProviderViewControlle
return
}
// If PIN is enabled, show PIN unlock immediately (PIN takes priority)
// Priority: Biometric -> PIN -> Error
// Try biometric first if enabled
if biometricEnabled {
do {
// Try to unlock the vault with biometric
try vaultStore.unlockVault()
// Unlock succeeded - process the credential request
if let passkeyRequest = self.quickReturnPasskeyRequest {
self.handleQuickReturnPasskeyCredential(vaultStore: vaultStore, request: passkeyRequest)
} else if let passwordRequest = self.quickReturnPasswordRequest {
self.handleQuickReturnPasswordCredential(vaultStore: vaultStore, request: passwordRequest)
}
return
} catch let error as NSError {
print("Quick return biometric unlock failed: \(error)")
// Biometric failed - try PIN fallback if available
if pinEnabled {
print("Falling back to PIN unlock")
self.showQuickReturnPinUnlock(vaultStore: vaultStore)
return
}
// No PIN fallback - report the error
var errorMessage = error.localizedDescription
if error.domain == "VaultStore" {
switch error.code {
case 3:
errorMessage = NSLocalizedString("no_encryption_key_message", comment: "No encryption key found. Please unlock the vault in the main AliasVault app first.")
case 8, 9:
errorMessage = NSLocalizedString("keychain_error_message", comment: "Failed to retrieve encryption key. This may be due to cancelled biometric authentication.")
default:
break
}
}
self.extensionContext.cancelRequest(withError: NSError(
domain: ASExtensionErrorDomain,
code: ASExtensionError.failed.rawValue,
userInfo: [NSLocalizedDescriptionKey: errorMessage]
))
return
}
}
// Biometric not enabled - try PIN directly
if pinEnabled {
self.showQuickReturnPinUnlock(vaultStore: vaultStore)
return
}
// Only biometric is enabled - try to unlock with biometric
// No loading view shown - Face ID will appear immediately on clean background
do {
// Try to unlock the vault with biometric
try vaultStore.unlockVault()
// Unlock succeeded - process the credential request
if let passkeyRequest = self.quickReturnPasskeyRequest {
self.handleQuickReturnPasskeyCredential(vaultStore: vaultStore, request: passkeyRequest)
} else if let passwordRequest = self.quickReturnPasswordRequest {
self.handleQuickReturnPasswordCredential(vaultStore: vaultStore, request: passwordRequest)
}
} catch let error as NSError {
print("Quick return vault unlock failed: \(error)")
// Provide specific error message based on error code
var errorMessage = error.localizedDescription
if error.domain == "VaultStore" {
switch error.code {
case 3:
errorMessage = NSLocalizedString("no_encryption_key_message", comment: "No encryption key found. Please unlock the vault in the main AliasVault app first.")
case 8, 9:
errorMessage = NSLocalizedString("keychain_error_message", comment: "Failed to retrieve encryption key. This may be due to cancelled biometric authentication.")
default:
break
}
}
self.extensionContext.cancelRequest(withError: NSError(
domain: ASExtensionErrorDomain,
code: ASExtensionError.failed.rawValue,
userInfo: [NSLocalizedDescriptionKey: errorMessage]
))
}
// This shouldn't happen as we check both above, but handle gracefully
self.extensionContext.cancelRequest(withError: NSError(
domain: ASExtensionErrorDomain,
code: ASExtensionError.failed.rawValue,
userInfo: [NSLocalizedDescriptionKey: NSLocalizedString("auth_required_message", comment: "Please enable Face ID or PIN unlock in the main AliasVault app to use autofill.")]
))
}
}
}
@@ -26,9 +26,8 @@ struct UnlockCoordinatorView: View {
// Show PIN unlock view
PinUnlockView(viewModel: pinViewModel)
} else {
// Transparent for biometric unlock - Face ID appears with no background
Color.clear
.ignoresSafeArea()
// Show logo placeholder while biometric unlock is in progress
BiometricUnlockPlaceholderView()
}
}
.onAppear {
@@ -37,6 +36,29 @@ struct UnlockCoordinatorView: View {
}
}
/// Simple placeholder view showing the AliasVault logo while biometric authentication is in progress
struct BiometricUnlockPlaceholderView: View {
@Environment(\.colorScheme) var colorScheme
private var colors: ColorConstants.Colors.Type {
ColorConstants.colors(for: colorScheme)
}
var body: some View {
ZStack {
colors.background
.ignoresSafeArea()
VStack {
Image("Logo", bundle: .vaultUI)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 70, height: 70)
}
}
}
}
/// Coordinator that manages the unlock flow logic
@MainActor
class UnlockCoordinator: ObservableObject {
@@ -57,18 +79,20 @@ class UnlockCoordinator: ObservableObject {
}
func startUnlockFlow() {
// Check if PIN is enabled
// Check which auth methods are enabled
// Priority: Biometric -> PIN -> Cancel
// Biometrics takes priority, PIN serves as fallback if biometrics fails or is unavailable.
let pinEnabled = vaultStore.isPinEnabled()
let biometricEnabled = vaultStore.isBiometricAuthEnabled()
if pinEnabled {
// PIN is enabled - show PIN unlock view
createPinViewModel()
} else if biometricEnabled {
// Only biometric is enabled - attempt biometric unlock
if biometricEnabled {
// Biometric is enabled - attempt biometric unlock first
Task {
await attemptBiometricUnlock()
}
} else if pinEnabled {
// Only PIN is enabled - show PIN unlock view
createPinViewModel()
} else {
// No auth method enabled - this shouldn't happen, but cancel the request
cancel()