Update iOS unlock flow to support both biometrics and pin fallback (#1562)
This commit is contained in:
committed by
Leendert de Borst
parent
749980f58f
commit
b9791a0563
@@ -142,46 +142,64 @@ public class CredentialProviderViewController: ASCredentialProviderViewControlle
|
|||||||
return
|
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 {
|
if pinEnabled {
|
||||||
self.showQuickReturnPinUnlock(vaultStore: vaultStore)
|
self.showQuickReturnPinUnlock(vaultStore: vaultStore)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only biometric is enabled - try to unlock with biometric
|
// This shouldn't happen as we check both above, but handle gracefully
|
||||||
// No loading view shown - Face ID will appear immediately on clean background
|
self.extensionContext.cancelRequest(withError: NSError(
|
||||||
do {
|
domain: ASExtensionErrorDomain,
|
||||||
// Try to unlock the vault with biometric
|
code: ASExtensionError.failed.rawValue,
|
||||||
try vaultStore.unlockVault()
|
userInfo: [NSLocalizedDescriptionKey: NSLocalizedString("auth_required_message", comment: "Please enable Face ID or PIN unlock in the main AliasVault app to use autofill.")]
|
||||||
|
))
|
||||||
// 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]
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,9 +26,8 @@ struct UnlockCoordinatorView: View {
|
|||||||
// Show PIN unlock view
|
// Show PIN unlock view
|
||||||
PinUnlockView(viewModel: pinViewModel)
|
PinUnlockView(viewModel: pinViewModel)
|
||||||
} else {
|
} else {
|
||||||
// Transparent for biometric unlock - Face ID appears with no background
|
// Show logo placeholder while biometric unlock is in progress
|
||||||
Color.clear
|
BiometricUnlockPlaceholderView()
|
||||||
.ignoresSafeArea()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.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
|
/// Coordinator that manages the unlock flow logic
|
||||||
@MainActor
|
@MainActor
|
||||||
class UnlockCoordinator: ObservableObject {
|
class UnlockCoordinator: ObservableObject {
|
||||||
@@ -57,18 +79,20 @@ class UnlockCoordinator: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func startUnlockFlow() {
|
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 pinEnabled = vaultStore.isPinEnabled()
|
||||||
let biometricEnabled = vaultStore.isBiometricAuthEnabled()
|
let biometricEnabled = vaultStore.isBiometricAuthEnabled()
|
||||||
|
|
||||||
if pinEnabled {
|
if biometricEnabled {
|
||||||
// PIN is enabled - show PIN unlock view
|
// Biometric is enabled - attempt biometric unlock first
|
||||||
createPinViewModel()
|
|
||||||
} else if biometricEnabled {
|
|
||||||
// Only biometric is enabled - attempt biometric unlock
|
|
||||||
Task {
|
Task {
|
||||||
await attemptBiometricUnlock()
|
await attemptBiometricUnlock()
|
||||||
}
|
}
|
||||||
|
} else if pinEnabled {
|
||||||
|
// Only PIN is enabled - show PIN unlock view
|
||||||
|
createPinViewModel()
|
||||||
} else {
|
} else {
|
||||||
// No auth method enabled - this shouldn't happen, but cancel the request
|
// No auth method enabled - this shouldn't happen, but cancel the request
|
||||||
cancel()
|
cancel()
|
||||||
|
|||||||
Reference in New Issue
Block a user